diff --git a/Cargo.lock b/Cargo.lock
index 1da7930..a4ab27b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -15,15 +15,6 @@ dependencies = [
"const-random",
]
-[[package]]
-name = "aho-corasick"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d"
-dependencies = [
- "memchr",
-]
-
[[package]]
name = "ansi_term"
version = "0.9.0"
@@ -1031,6 +1022,19 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+[[package]]
+name = "lexical-core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7043aa5c05dd34fb73b47acb8c3708eac428de4545ea3682ed2f11293ebd890"
+dependencies = [
+ "arrayvec 0.4.12",
+ "cfg-if",
+ "rustc_version",
+ "ryu",
+ "static_assertions",
+]
+
[[package]]
name = "libc"
version = "0.2.66"
@@ -1214,8 +1218,8 @@ dependencies = [
"lazy_static",
"libc",
"log",
+ "nom 5.1.1",
"parking_lot",
- "regex",
"time 0.1.42",
]
@@ -1243,7 +1247,6 @@ dependencies = [
"prettydiff",
"protobuf",
"protobuf-codegen-pure",
- "regex",
"rusqlite",
"smallvec 1.1.0",
"tempdir",
@@ -1287,11 +1290,11 @@ dependencies = [
"moonfire-ffmpeg",
"mylog",
"nix",
+ "nom 5.1.1",
"openssl",
"parking_lot",
"protobuf",
"reffers",
- "regex",
"reqwest",
"ring",
"rusqlite",
@@ -1386,6 +1389,17 @@ dependencies = [
"version_check 0.1.5",
]
+[[package]]
+name = "nom"
+version = "5.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b471253da97532da4b61552249c521e01e736071f71c1a4f7ebbfbf0a06aad6"
+dependencies = [
+ "lexical-core",
+ "memchr",
+ "version_check 0.9.1",
+]
+
[[package]]
name = "num"
version = "0.2.1"
@@ -1879,18 +1893,6 @@ dependencies = [
"stable_deref_trait",
]
-[[package]]
-name = "regex"
-version = "1.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5508c1941e4e7cb19965abef075d35a9a8b5cdf0846f30b4050e9b55dc55e87"
-dependencies = [
- "aho-corasick",
- "memchr",
- "regex-syntax",
- "thread_local",
-]
-
[[package]]
name = "regex-automata"
version = "0.1.8"
@@ -1900,12 +1902,6 @@ dependencies = [
"byteorder",
]
-[[package]]
-name = "regex-syntax"
-version = "0.6.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
-
[[package]]
name = "remove_dir_all"
version = "0.5.2"
@@ -2298,6 +2294,12 @@ version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
+[[package]]
+name = "static_assertions"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
+
[[package]]
name = "strsim"
version = "0.8.0"
@@ -2463,15 +2465,6 @@ dependencies = [
"unicode-width",
]
-[[package]]
-name = "thread_local"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
-dependencies = [
- "lazy_static",
-]
-
[[package]]
name = "time"
version = "0.1.42"
@@ -2869,7 +2862,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
dependencies = [
- "nom",
+ "nom 4.2.3",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 0f790cd..7e2913f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,11 +43,11 @@ memchr = "2.0.2"
memmap = "0.7"
mylog = { git = "https://github.com/scottlamb/mylog" }
nix = "0.16.1"
+nom = "5.1.1"
openssl = "0.10"
parking_lot = { version = "0.9", features = [] }
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
reffers = "0.6.0"
-regex = "1.0"
ring = "0.14.6"
rusqlite = "0.21.0"
serde = { version = "1.0", features = ["derive"] }
diff --git a/base/Cargo.toml b/base/Cargo.toml
index 790a6cc..c759c39 100644
--- a/base/Cargo.toml
+++ b/base/Cargo.toml
@@ -17,5 +17,5 @@ lazy_static = "1.0"
libc = "0.2"
log = "0.4"
parking_lot = { version = "0.9", features = [] }
-regex = "1.0"
+nom = "5.1.1"
time = "0.1"
diff --git a/base/lib.rs b/base/lib.rs
index be3e65e..c5c38f9 100644
--- a/base/lib.rs
+++ b/base/lib.rs
@@ -29,6 +29,7 @@
// along with this program. If not, see .
pub mod clock;
+pub mod time;
mod error;
pub mod strutil;
diff --git a/base/strutil.rs b/base/strutil.rs
index de48c0b..24ddb90 100644
--- a/base/strutil.rs
+++ b/base/strutil.rs
@@ -28,10 +28,13 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-use lazy_static::lazy_static;
-use regex::Regex;
+use nom::IResult;
+use nom::branch::alt;
+use nom::bytes::complete::{tag, take_while1};
+use nom::character::complete::space0;
+use nom::combinator::{map, map_res, opt};
+use nom::sequence::{delimited, tuple};
use std::fmt::Write as _;
-use std::str::FromStr as _;
static MULTIPLIERS: [(char, u64); 4] = [
// (suffix character, power of 2)
@@ -58,32 +61,33 @@ pub fn encode_size(mut raw: i64) -> String {
encoded
}
+fn decode_sizepart(input: &str) -> IResult<&str, i64> {
+ map(
+ tuple((
+ map_res(take_while1(|c: char| c.is_ascii_digit()),
+ |input: &str| i64::from_str_radix(input, 10)),
+ opt(alt((
+ nom::combinator::value(1<<40, tag("T")),
+ nom::combinator::value(1<<30, tag("G")),
+ nom::combinator::value(1<<20, tag("M")),
+ nom::combinator::value(1<<10, tag("K"))
+ )))
+ )),
+ |(n, opt_unit)| n * opt_unit.unwrap_or(1)
+ )(input)
+}
+
+fn decode_size_internal(input: &str) -> IResult<&str, i64> {
+ nom::multi::fold_many1(
+ delimited(space0, decode_sizepart, space0),
+ 0,
+ |sum, i| sum + i)(input)
+}
+
/// Decodes a human-readable size as output by encode_size.
pub fn decode_size(encoded: &str) -> Result {
- let mut decoded = 0i64;
- lazy_static! {
- static ref RE: Regex = Regex::new(r"\s*([0-9]+)([TGMK])?,?\s*").unwrap();
- }
- let mut last_pos = 0;
- for cap in RE.captures_iter(encoded) {
- let whole_cap = cap.get(0).unwrap();
- if whole_cap.start() > last_pos {
- return Err(());
- }
- last_pos = whole_cap.end();
- let mut piece = i64::from_str(&cap[1]).map_err(|_| ())?;
- if let Some(m) = cap.get(2) {
- let m = m.as_str().as_bytes()[0] as char;
- for &(some_m, n) in &MULTIPLIERS {
- if some_m == m {
- piece *= 1i64<.
+
+//! Time and durations for Moonfire NVR's internal format.
+
+use failure::{Error, bail, format_err};
+use nom::branch::alt;
+use nom::bytes::complete::{tag, take_while_m_n};
+use nom::combinator::{map, map_res, opt};
+use nom::sequence::{preceded, tuple};
+use std::ops;
+use std::fmt;
+use std::str::FromStr;
+use time;
+
+type IResult<'a, I, O> = nom::IResult>;
+
+pub const TIME_UNITS_PER_SEC: i64 = 90_000;
+
+/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
+#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
+pub struct Time(pub i64);
+
+/// Returns a parser for a `len`-digit non-negative number which fits into an i32.
+fn fixed_len_num<'a>(len: usize) -> impl Fn(&'a str) -> IResult<&'a str, i32> {
+ map_res(take_while_m_n(len, len, |c: char| c.is_ascii_digit()),
+ |input: &str| i32::from_str_radix(input, 10))
+}
+
+/// Parses `YYYY-mm-dd` into pieces.
+fn parse_datepart(input: &str) -> IResult<&str, (i32, i32, i32)> {
+ tuple((
+ fixed_len_num(4),
+ preceded(tag("-"), fixed_len_num(2)),
+ preceded(tag("-"), fixed_len_num(2))
+ ))(input)
+}
+
+/// Parses `HH:MM[:SS[:FFFFF]]` into pieces.
+fn parse_timepart(input: &str) -> IResult<&str, (i32, i32, i32, i32)> {
+ let (input, (hr, _, min)) = tuple((fixed_len_num(2), tag(":"), fixed_len_num(2)))(input)?;
+ let (input, stuff) = opt(tuple((
+ preceded(tag(":"), fixed_len_num(2)),
+ opt(preceded(tag(":"), fixed_len_num(5)))
+ )))(input)?;
+ let (sec, opt_subsec) = stuff.unwrap_or((0, None));
+ Ok((input, (hr, min, sec, opt_subsec.unwrap_or(0))))
+}
+
+/// Parses `Z` (UTC) or `{+,-,}HH:MM` into a time zone offset in seconds.
+fn parse_zone(input: &str) -> IResult<&str, i32> {
+ alt((
+ nom::combinator::value(0, tag("Z")),
+ map(
+ tuple((
+ opt(nom::character::complete::one_of(&b"+-"[..])),
+ fixed_len_num(2),
+ tag(":"),
+ fixed_len_num(2)
+ )),
+ |(sign, hr, _, min)| {
+ let off = hr * 3600 + min * 60;
+ if sign == Some('-') { off } else { -off }
+ })
+ ))(input)
+}
+
+impl Time {
+ pub fn new(tm: time::Timespec) -> Self {
+ Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000)
+ }
+
+ pub const fn min_value() -> Self { Time(i64::min_value()) }
+ pub const fn max_value() -> Self { Time(i64::max_value()) }
+
+ /// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
+ ///
+ /// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds.
+ ///
+ /// The latter is a date such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of
+ /// a second such as `:00001`, followed by an optional time zone offset such as `Z` or
+ /// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the
+ /// local time zone.
+ pub fn parse(input: &str) -> Result {
+ // First try parsing as 90,000ths of a second since epoch.
+ match i64::from_str(input) {
+ Ok(i) => return Ok(Time(i)),
+ Err(_) => {},
+ }
+
+ // If that failed, parse as a time string or bust.
+ let (remaining, ((tm_year, tm_mon, tm_mday), opt_time, opt_zone)) =
+ tuple((parse_datepart,
+ opt(preceded(tag("T"), parse_timepart)),
+ opt(parse_zone)))(input)
+ .map_err(|e| match e {
+ nom::Err::Incomplete(_) => format_err!("incomplete"),
+ nom::Err::Error(e) | nom::Err::Failure(e) => {
+ format_err!("{}", nom::error::convert_error(input, e))
+ }
+ })?;
+ if remaining != "" {
+ bail!("unexpected suffix {:?} following time string", remaining);
+ }
+ let (tm_hour, tm_min, tm_sec, subsec) = opt_time.unwrap_or((0, 0, 0, 0));
+ let mut tm = time::Tm {
+ tm_sec,
+ tm_min,
+ tm_hour,
+ tm_mday,
+ tm_mon,
+ tm_year,
+ tm_wday: 0,
+ tm_yday: 0,
+ tm_isdst: -1,
+ tm_utcoff: 0,
+ tm_nsec: 0,
+ };
+ if tm.tm_mon == 0 {
+ bail!("time {:?} has month 0", input);
+ }
+ tm.tm_mon -= 1;
+ if tm.tm_year < 1900 {
+ bail!("time {:?} has year before 1900", input);
+ }
+ tm.tm_year -= 1900;
+
+ // The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0,
+ // mktime() otherwise. If a zone is specified, use the timegm path and a manual offset.
+ // If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the
+ // chrono crate's lead and just use 0 or 1 to choose between these functions.
+ let sec = if let Some(off) = opt_zone {
+ tm.to_timespec().sec + i64::from(off)
+ } else {
+ tm.tm_utcoff = 1;
+ tm.to_timespec().sec
+ };
+ Ok(Time(sec * TIME_UNITS_PER_SEC + i64::from(subsec)))
+ }
+
+ /// Convert to unix seconds by floor method (rounding down).
+ pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
+}
+
+impl std::str::FromStr for Time {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result { Self::parse(s) }
+}
+
+impl ops::Sub for Time {
+ type Output = Duration;
+ fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) }
+}
+
+impl ops::AddAssign for Time {
+ fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
+}
+
+impl ops::Add for Time {
+ type Output = Time;
+ fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
+}
+
+impl ops::Sub for Time {
+ type Output = Time;
+ fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) }
+}
+
+impl fmt::Debug for Time {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ // Write both the raw and display forms.
+ write!(f, "{} /* {} */", self.0, self)
+ }
+}
+
+impl fmt::Display for Time {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0});
+ let zone_minutes = tm.tm_utcoff.abs() / 60;
+ write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?,
+ self.0 % TIME_UNITS_PER_SEC,
+ if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60)
+ }
+}
+
+/// A duration specified in 1/90,000ths of a second.
+/// Durations are typically non-negative, but a `moonfire_db::db::CameraDayValue::duration` may be
+/// negative.
+#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
+pub struct Duration(pub i64);
+
+impl Duration {
+ pub fn to_tm_duration(&self) -> time::Duration {
+ time::Duration::nanoseconds(self.0 * 100000 / 9)
+ }
+}
+
+impl fmt::Display for Duration {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let mut seconds = self.0 / TIME_UNITS_PER_SEC;
+ const MINUTE_IN_SECONDS: i64 = 60;
+ const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS;
+ const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS;
+ let days = seconds / DAY_IN_SECONDS;
+ seconds %= DAY_IN_SECONDS;
+ let hours = seconds / HOUR_IN_SECONDS;
+ seconds %= HOUR_IN_SECONDS;
+ let minutes = seconds / MINUTE_IN_SECONDS;
+ seconds %= MINUTE_IN_SECONDS;
+ let mut have_written = if days > 0 {
+ write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?;
+ true
+ } else {
+ false
+ };
+ if hours > 0 {
+ write!(f, "{}{} hour{}", if have_written { " " } else { "" },
+ hours, if hours == 1 { "" } else { "s" })?;
+ have_written = true;
+ }
+ if minutes > 0 {
+ write!(f, "{}{} minute{}", if have_written { " " } else { "" },
+ minutes, if minutes == 1 { "" } else { "s" })?;
+ have_written = true;
+ }
+ if seconds > 0 || !have_written {
+ write!(f, "{}{} second{}", if have_written { " " } else { "" },
+ seconds, if seconds == 1 { "" } else { "s" })?;
+ }
+ Ok(())
+ }
+}
+
+impl ops::Add for Duration {
+ type Output = Duration;
+ fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) }
+}
+
+impl ops::AddAssign for Duration {
+ fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
+}
+
+impl ops::SubAssign for Duration {
+ fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{Duration, Time, TIME_UNITS_PER_SEC};
+
+ #[test]
+ fn test_parse_time() {
+ std::env::set_var("TZ", "America/Los_Angeles");
+ time::tzset();
+ let tests = &[
+ ("2006-01-02T15:04:05-07:00", 102261550050000),
+ ("2006-01-02T15:04:05:00001-07:00", 102261550050001),
+ ("2006-01-02T15:04:05-08:00", 102261874050000),
+ ("2006-01-02T15:04:05", 102261874050000), // implied -08:00
+ ("2006-01-02T15:04", 102261873600000), // implied -08:00
+ ("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00
+ ("2006-01-02T15:04:05-00:00", 102259282050000),
+ ("2006-01-02T15:04:05Z", 102259282050000),
+ ("2006-01-02-08:00", 102256992000000), // implied -08:00
+ ("2006-01-02", 102256992000000), // implied -08:00
+ ("2006-01-02Z", 102254400000000),
+ ("102261550050000", 102261550050000),
+ ];
+ for test in tests {
+ assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0);
+ }
+ }
+
+ #[test]
+ fn test_format_time() {
+ std::env::set_var("TZ", "America/Los_Angeles");
+ time::tzset();
+ assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)));
+ }
+
+ #[test]
+ fn test_display_duration() {
+ let tests = &[
+ // (output, seconds)
+ ("0 seconds", 0),
+ ("1 second", 1),
+ ("1 minute", 60),
+ ("1 minute 1 second", 61),
+ ("2 minutes", 120),
+ ("1 hour", 3600),
+ ("1 hour 1 minute", 3660),
+ ("2 hours", 7200),
+ ("1 day", 86400),
+ ("1 day 1 hour", 86400 + 3600),
+ ("2 days", 2 * 86400),
+ ];
+ for test in tests {
+ assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC)));
+ }
+ }
+}
diff --git a/db/Cargo.toml b/db/Cargo.toml
index 1ecfe14..a7a16c7 100644
--- a/db/Cargo.toml
+++ b/db/Cargo.toml
@@ -30,7 +30,6 @@ openssl = "0.10"
parking_lot = { version = "0.9", features = [] }
prettydiff = "0.3.1"
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
-regex = "1.0"
rusqlite = "0.21.0"
smallvec = "1.0"
tempdir = "0.3"
diff --git a/db/recording.rs b/db/recording.rs
index efc55bd..c73cafe 100644
--- a/db/recording.rs
+++ b/db/recording.rs
@@ -30,199 +30,17 @@
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
use crate::db;
-use failure::{Error, bail, format_err};
-use lazy_static::lazy_static;
+use failure::{Error, bail};
use log::trace;
-use regex::Regex;
-use std::ops;
-use std::fmt;
use std::ops::Range;
-use std::str::FromStr;
-use time;
-pub const TIME_UNITS_PER_SEC: i64 = 90000;
+pub use base::time::TIME_UNITS_PER_SEC;
+
pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC;
-/// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
-#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
-pub struct Time(pub i64);
-
-impl Time {
- pub fn new(tm: time::Timespec) -> Self {
- Time(tm.sec * TIME_UNITS_PER_SEC + tm.nsec as i64 * TIME_UNITS_PER_SEC / 1_000_000_000)
- }
-
- pub const fn min_value() -> Self { Time(i64::min_value()) }
- pub const fn max_value() -> Self { Time(i64::max_value()) }
-
- /// Parses a time as either 90,000ths of a second since epoch or a RFC 3339-like string.
- ///
- /// The former is 90,000ths of a second since 1970-01-01T00:00:00 UTC, excluding leap seconds.
- ///
- /// The latter is a string such as `2006-01-02T15:04:05`, followed by an optional 90,000ths of
- /// a second such as `:00001`, followed by an optional time zone offset such as `Z` or
- /// `-07:00`. A missing fraction is assumed to be 0. A missing time zone offset implies the
- /// local time zone.
- pub fn parse(s: &str) -> Result {
- lazy_static! {
- static ref RE: Regex = Regex::new(r#"(?x)
- ^
- ([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})
- (?::([0-9]{5}))?
- (Z|[+-]([0-9]{2}):([0-9]{2}))?
- $"#).unwrap();
- }
-
- // First try parsing as 90,000ths of a second since epoch.
- match i64::from_str(s) {
- Ok(i) => return Ok(Time(i)),
- Err(_) => {},
- }
-
- // If that failed, parse as a time string or bust.
- let c = RE.captures(s).ok_or_else(|| format_err!("unparseable time {:?}", s))?;
- let mut tm = time::Tm{
- tm_sec: i32::from_str(c.get(6).unwrap().as_str()).unwrap(),
- tm_min: i32::from_str(c.get(5).unwrap().as_str()).unwrap(),
- tm_hour: i32::from_str(c.get(4).unwrap().as_str()).unwrap(),
- tm_mday: i32::from_str(c.get(3).unwrap().as_str()).unwrap(),
- tm_mon: i32::from_str(c.get(2).unwrap().as_str()).unwrap(),
- tm_year: i32::from_str(c.get(1).unwrap().as_str()).unwrap(),
- tm_wday: 0,
- tm_yday: 0,
- tm_isdst: -1,
- tm_utcoff: 0,
- tm_nsec: 0,
- };
- if tm.tm_mon == 0 {
- bail!("time {:?} has month 0", s);
- }
- tm.tm_mon -= 1;
- if tm.tm_year < 1900 {
- bail!("time {:?} has year before 1900", s);
- }
- tm.tm_year -= 1900;
-
- // The time crate doesn't use tm_utcoff properly; it just calls timegm() if tm_utcoff == 0,
- // mktime() otherwise. If a zone is specified, use the timegm path and a manual offset.
- // If no zone is specified, use the tm_utcoff path. This is pretty lame, but follow the
- // chrono crate's lead and just use 0 or 1 to choose between these functions.
- let sec = if let Some(zone) = c.get(8) {
- tm.to_timespec().sec + if zone.as_str() == "Z" {
- 0
- } else {
- let off = i64::from_str(c.get(9).unwrap().as_str()).unwrap() * 3600 +
- i64::from_str(c.get(10).unwrap().as_str()).unwrap() * 60;
- if zone.as_str().as_bytes()[0] == b'-' { off } else { -off }
- }
- } else {
- tm.tm_utcoff = 1;
- tm.to_timespec().sec
- };
- let fraction = if let Some(f) = c.get(7) { i64::from_str(f.as_str()).unwrap() } else { 0 };
- Ok(Time(sec * TIME_UNITS_PER_SEC + fraction))
- }
-
- /// Convert to unix seconds by floor method (rounding down).
- pub fn unix_seconds(&self) -> i64 { self.0 / TIME_UNITS_PER_SEC }
-}
-
-impl ops::Sub for Time {
- type Output = Duration;
- fn sub(self, rhs: Time) -> Duration { Duration(self.0 - rhs.0) }
-}
-
-impl ops::AddAssign for Time {
- fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
-}
-
-impl ops::Add for Time {
- type Output = Time;
- fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
-}
-
-impl ops::Sub for Time {
- type Output = Time;
- fn sub(self, rhs: Duration) -> Time { Time(self.0 - rhs.0) }
-}
-
-impl fmt::Debug for Time {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- // Write both the raw and display forms.
- write!(f, "{} /* {} */", self.0, self)
- }
-}
-
-impl fmt::Display for Time {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let tm = time::at(time::Timespec{sec: self.0 / TIME_UNITS_PER_SEC, nsec: 0});
- let zone_minutes = tm.tm_utcoff.abs() / 60;
- write!(f, "{}:{:05}{}{:02}:{:02}", tm.strftime("%FT%T").or_else(|_| Err(fmt::Error))?,
- self.0 % TIME_UNITS_PER_SEC,
- if tm.tm_utcoff > 0 { '+' } else { '-' }, zone_minutes / 60, zone_minutes % 60)
- }
-}
-
-/// A duration specified in 1/90,000ths of a second.
-/// Durations are typically non-negative, but a `db::CameraDayValue::duration` may be negative.
-#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
-pub struct Duration(pub i64);
-
-impl Duration {
- pub fn to_tm_duration(&self) -> time::Duration {
- time::Duration::nanoseconds(self.0 * 100000 / 9)
- }
-}
-
-impl fmt::Display for Duration {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- let mut seconds = self.0 / TIME_UNITS_PER_SEC;
- const MINUTE_IN_SECONDS: i64 = 60;
- const HOUR_IN_SECONDS: i64 = 60 * MINUTE_IN_SECONDS;
- const DAY_IN_SECONDS: i64 = 24 * HOUR_IN_SECONDS;
- let days = seconds / DAY_IN_SECONDS;
- seconds %= DAY_IN_SECONDS;
- let hours = seconds / HOUR_IN_SECONDS;
- seconds %= HOUR_IN_SECONDS;
- let minutes = seconds / MINUTE_IN_SECONDS;
- seconds %= MINUTE_IN_SECONDS;
- let mut have_written = if days > 0 {
- write!(f, "{} day{}", days, if days == 1 { "" } else { "s" })?;
- true
- } else {
- false
- };
- if hours > 0 {
- write!(f, "{}{} hour{}", if have_written { " " } else { "" },
- hours, if hours == 1 { "" } else { "s" })?;
- have_written = true;
- }
- if minutes > 0 {
- write!(f, "{}{} minute{}", if have_written { " " } else { "" },
- minutes, if minutes == 1 { "" } else { "s" })?;
- have_written = true;
- }
- if seconds > 0 || !have_written {
- write!(f, "{}{} second{}", if have_written { " " } else { "" },
- seconds, if seconds == 1 { "" } else { "s" })?;
- }
- Ok(())
- }
-}
-
-impl ops::Add for Duration {
- type Output = Duration;
- fn add(self, rhs: Duration) -> Duration { Duration(self.0 + rhs.0) }
-}
-
-impl ops::AddAssign for Duration {
- fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
-}
-
-impl ops::SubAssign for Duration {
- fn sub_assign(&mut self, rhs: Duration) { self.0 -= rhs.0 }
-}
+pub use base::time::Time;
+pub use base::time::Duration;
/// An iterator through a sample index.
/// Initially invalid; call `next()` before each read.
@@ -533,52 +351,6 @@ mod tests {
use super::*;
use crate::testutil::{self, TestDb};
- #[test]
- fn test_parse_time() {
- testutil::init();
- let tests = &[
- ("2006-01-02T15:04:05-07:00", 102261550050000),
- ("2006-01-02T15:04:05:00001-07:00", 102261550050001),
- ("2006-01-02T15:04:05-08:00", 102261874050000),
- ("2006-01-02T15:04:05", 102261874050000), // implied -08:00
- ("2006-01-02T15:04:05:00001", 102261874050001), // implied -08:00
- ("2006-01-02T15:04:05-00:00", 102259282050000),
- ("2006-01-02T15:04:05Z", 102259282050000),
- ("102261550050000", 102261550050000),
- ];
- for test in tests {
- assert_eq!(test.1, Time::parse(test.0).unwrap().0, "parsing {}", test.0);
- }
- }
-
- #[test]
- fn test_format_time() {
- testutil::init();
- assert_eq!("2006-01-02T15:04:05:00000-08:00", format!("{}", Time(102261874050000)));
- }
-
- #[test]
- fn test_display_duration() {
- testutil::init();
- let tests = &[
- // (output, seconds)
- ("0 seconds", 0),
- ("1 second", 1),
- ("1 minute", 60),
- ("1 minute 1 second", 61),
- ("2 minutes", 120),
- ("1 hour", 3600),
- ("1 hour 1 minute", 3660),
- ("2 hours", 7200),
- ("1 day", 86400),
- ("1 day 1 hour", 86400 + 3600),
- ("2 days", 2 * 86400),
- ];
- for test in tests {
- assert_eq!(test.0, format!("{}", Duration(test.1 * TIME_UNITS_PER_SEC)));
- }
- }
-
/// Tests encoding the example from design/schema.md.
#[test]
fn test_encode_example() {
diff --git a/src/h264.rs b/src/h264.rs
index 820cfa6..b2cd039 100644
--- a/src/h264.rs
+++ b/src/h264.rs
@@ -42,8 +42,6 @@
use byteorder::{BigEndian, WriteBytesExt};
use failure::{Error, bail};
-use lazy_static::lazy_static;
-use regex::bytes::Regex;
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit
// type classes.
@@ -60,15 +58,28 @@ const NAL_UNIT_TYPE_MASK: u8 = 0x1F; // bottom 5 bits of first byte of unit.
///
/// TODO: detect invalid byte streams. For example, several 0x00s not followed by a 0x01, a stream
/// stream not starting with 0x00 0x00 0x00 0x01, or an empty NAL unit.
-fn decode_h264_annex_b<'a, F>(data: &'a [u8], mut f: F) -> Result<(), Error>
+fn decode_h264_annex_b<'a, F>(mut data: &'a [u8], mut f: F) -> Result<(), Error>
where F: FnMut(&'a [u8]) -> Result<(), Error> {
- lazy_static! {
- static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap();
- }
- for unit in START_CODE.split(data) {
- if !unit.is_empty() {
- f(unit)?;
+ let start_code = &b"\x00\x00\x01"[..];
+ use nom::FindSubstring;
+ 'outer: while let Some(pos) = data.find_substring(start_code) {
+ let mut unit = &data[0..pos];
+ data = &data[pos + start_code.len() ..];
+ // Have zero or more bytes that end in a start code. Strip out any trailing 0x00s and
+ // process the unit if there's anything left.
+ loop {
+ match unit.last() {
+ None => continue 'outer,
+ Some(b) if *b == 0 => { unit = &unit[..unit.len()-1]; },
+ Some(_) => break,
+ }
}
+ f(unit)?;
+ }
+
+ // No remaining start codes; likely a unit left.
+ if !data.is_empty() {
+ f(data)?;
}
Ok(())
}
diff --git a/src/web.rs b/src/web.rs
index cf852ff..719d75c 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -34,7 +34,6 @@ use bytes::Bytes;
use crate::body::{Body, BoxedError};
use crate::json;
use crate::mp4;
-use base64;
use bytes::{BufMut, BytesMut};
use core::borrow::Borrow;
use core::str::FromStr;
@@ -46,12 +45,12 @@ use futures::sink::SinkExt;
use futures::future::{self, Future, TryFutureExt};
use futures::stream::StreamExt;
use http::{Request, Response, status::StatusCode};
-use http_serve;
use http::header::{self, HeaderValue};
-use lazy_static::lazy_static;
use log::{debug, info, warn};
-use regex::Regex;
-use serde_json;
+use nom::IResult;
+use nom::bytes::complete::{take_while1, tag};
+use nom::combinator::{all_consuming, map, map_res, opt};
+use nom::sequence::{preceded, tuple};
use std::collections::HashMap;
use std::cmp;
use std::fs;
@@ -64,14 +63,6 @@ use tokio_tungstenite::tungstenite;
use url::form_urlencoded;
use uuid::Uuid;
-lazy_static! {
- /// Regex used to parse the `s` query parameter to `view.mp4`.
- /// As described in `design/api.md`, this is of the form
- /// `START_ID[-END_ID][@OPEN_ID][.[REL_START_TIME]-[REL_END_TIME]]`.
- static ref SEGMENTS_RE: Regex =
- Regex::new(r"^(\d+)(-\d+)?(@\d+)?(?:\.(\d+)?-(\d+)?)?$").unwrap();
-}
-
type BoxedFuture = Box, BoxedError>> +
Sync + Send + 'static>;
@@ -202,41 +193,48 @@ struct Segments {
end_time: Option,
}
+fn num<'a, T: FromStr>() -> impl Fn(&'a str) -> IResult<&'a str, T> {
+ map_res(take_while1(|c: char| c.is_ascii_digit()), FromStr::from_str)
+}
+
impl Segments {
- pub fn parse(input: &str) -> Result {
- let caps = SEGMENTS_RE.captures(input).ok_or(())?;
- let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?;
- let ids_end = match caps.get(2) {
- Some(m) => i32::from_str(&m.as_str()[1..]).map_err(|_| ())?,
- None => ids_start,
- } + 1;
- let open_id = match caps.get(3) {
- Some(m) => Some(u32::from_str(&m.as_str()[1..]).map_err(|_| ())?),
- None => None,
- };
- if ids_start < 0 || ids_end <= ids_start {
+ /// Parses the `s` query parameter to `view.mp4` as described in `design/api.md`.
+ /// Doesn't do any validation.
+ fn parse(i: &str) -> IResult<&str, Segments> {
+ // Parse START_ID[-END_ID] into Range.
+ // Note that END_ID is inclusive, but Ranges are half-open.
+ let (i, ids) = map(tuple((num::(), opt(preceded(tag("-"), num::())))),
+ |(start, end)| start .. end.unwrap_or(start) + 1)(i)?;
+
+ // Parse [@OPEN_ID] into Option.
+ let (i, open_id) = opt(preceded(tag("@"), num::()))(i)?;
+
+ // Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option).
+ let (i, (start_time, end_time)) = map(
+ opt(preceded(tag("."), tuple((opt(num::()), tag("-"), opt(num::()))))),
+ |t| {
+ t.map(|(s, _, e)| (s.unwrap_or(0), e))
+ .unwrap_or((0, None))
+ })(i)?;
+
+ Ok((i, Segments { ids, open_id, start_time, end_time, }))
+ }
+}
+
+impl FromStr for Segments {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result {
+ let (_, s) = all_consuming(Segments::parse)(s).map_err(|_| ())?;
+ if s.ids.end <= s.ids.start {
return Err(());
}
- let start_time = caps.get(4).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?;
- if start_time < 0 {
- return Err(());
+ if let Some(e) = s.end_time {
+ if e < s.start_time {
+ return Err(());
+ }
}
- let end_time = match caps.get(5) {
- Some(v) => {
- let e = i64::from_str(v.as_str()).map_err(|_| ())?;
- if e <= start_time {
- return Err(());
- }
- Some(e)
- },
- None => None
- };
- Ok(Segments {
- ids: ids_start .. ids_end,
- open_id,
- start_time,
- end_time,
- })
+ Ok(s)
}
}
@@ -422,7 +420,7 @@ impl ServiceInner {
let (key, value) = (key.borrow(), value.borrow());
match key {
"s" => {
- let s = Segments::parse(value).map_err(
+ let s = Segments::from_str(value).map_err(
|()| plain_response(StatusCode::BAD_REQUEST,
format!("invalid s parameter: {}", value)))?;
debug!("stream_view_mp4: appending s={:?}", s);
@@ -1078,6 +1076,7 @@ mod tests {
use futures::future::FutureExt;
use log::info;
use std::collections::HashMap;
+ use std::str::FromStr;
use super::Segments;
struct Server {
@@ -1233,25 +1232,25 @@ mod tests {
fn test_segments() {
testutil::init();
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: None},
- Segments::parse("1").unwrap());
+ Segments::from_str("1").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 0, end_time: None},
- Segments::parse("1@42").unwrap());
+ Segments::from_str("1@42").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: None},
- Segments::parse("1.26-").unwrap());
+ Segments::from_str("1.26-").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: Some(42), start_time: 26, end_time: None},
- Segments::parse("1@42.26-").unwrap());
+ Segments::from_str("1@42.26-").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: Some(42)},
- Segments::parse("1.-42").unwrap());
+ Segments::from_str("1.-42").unwrap());
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 26, end_time: Some(42)},
- Segments::parse("1.26-42").unwrap());
+ Segments::from_str("1.26-42").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: None},
- Segments::parse("1-5").unwrap());
+ Segments::from_str("1-5").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: None},
- Segments::parse("1-5.26-").unwrap());
+ Segments::from_str("1-5.26-").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 0, end_time: Some(42)},
- Segments::parse("1-5.-42").unwrap());
+ Segments::from_str("1-5.-42").unwrap());
assert_eq!(Segments{ids: 1..6, open_id: None, start_time: 26, end_time: Some(42)},
- Segments::parse("1-5.26-42").unwrap());
+ Segments::from_str("1-5.26-42").unwrap());
}
#[tokio::test]