From af9e568344ff3881a81144bf94bb288f4e22a1c1 Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Fri, 17 Apr 2020 23:02:02 -0700 Subject: [PATCH] replace regex use with nom This reduces the binary size noticeably on my macOS machine (#70): unstripped stripped 1 before switching to clap 11.1 MiB 6.7 MiB 2 after switching to clap 11.4 MiB 6.9 MiB 3 without regex 10.1 MiB 5.9 MiB --- Cargo.lock | 73 +++++------ Cargo.toml | 2 +- base/Cargo.toml | 2 +- base/lib.rs | 1 + base/strutil.rs | 59 +++++---- base/time.rs | 329 ++++++++++++++++++++++++++++++++++++++++++++++++ db/Cargo.toml | 1 - db/recording.rs | 238 +---------------------------------- src/h264.rs | 29 +++-- src/web.rs | 109 ++++++++-------- 10 files changed, 476 insertions(+), 367 deletions(-) create mode 100644 base/time.rs 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]