mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2025-02-03 09:55:59 -05:00
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
This commit is contained in:
parent
e8eb764b90
commit
af9e568344
73
Cargo.lock
generated
73
Cargo.lock
generated
@ -15,15 +15,6 @@ dependencies = [
|
|||||||
"const-random",
|
"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]]
|
[[package]]
|
||||||
name = "ansi_term"
|
name = "ansi_term"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -1031,6 +1022,19 @@ version = "1.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
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]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.66"
|
version = "0.2.66"
|
||||||
@ -1214,8 +1218,8 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"nom 5.1.1",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"regex",
|
|
||||||
"time 0.1.42",
|
"time 0.1.42",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1243,7 +1247,6 @@ dependencies = [
|
|||||||
"prettydiff",
|
"prettydiff",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"protobuf-codegen-pure",
|
"protobuf-codegen-pure",
|
||||||
"regex",
|
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"smallvec 1.1.0",
|
"smallvec 1.1.0",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
@ -1287,11 +1290,11 @@ dependencies = [
|
|||||||
"moonfire-ffmpeg",
|
"moonfire-ffmpeg",
|
||||||
"mylog",
|
"mylog",
|
||||||
"nix",
|
"nix",
|
||||||
|
"nom 5.1.1",
|
||||||
"openssl",
|
"openssl",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"reffers",
|
"reffers",
|
||||||
"regex",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"ring",
|
"ring",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
@ -1386,6 +1389,17 @@ dependencies = [
|
|||||||
"version_check 0.1.5",
|
"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]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -1879,18 +1893,6 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"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]]
|
[[package]]
|
||||||
name = "regex-automata"
|
name = "regex-automata"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@ -1900,12 +1902,6 @@ dependencies = [
|
|||||||
"byteorder",
|
"byteorder",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.6.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e734e891f5b408a29efbf8309e656876276f49ab6a6ac208600b4419bd893d90"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remove_dir_all"
|
name = "remove_dir_all"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@ -2298,6 +2294,12 @@ version = "1.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
|
checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f3eb36b47e512f8f1c9e3d10c2c1965bc992bd9cdb024fa581e2194501c83d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@ -2463,15 +2465,6 @@ dependencies = [
|
|||||||
"unicode-width",
|
"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]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.1.42"
|
version = "0.1.42"
|
||||||
@ -2869,7 +2862,7 @@ version = "0.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
|
checksum = "3bb43f70885151e629e2a19ce9e50bd730fd436cfd4b666894c9ce4de9141164"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom 4.2.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -43,11 +43,11 @@ memchr = "2.0.2"
|
|||||||
memmap = "0.7"
|
memmap = "0.7"
|
||||||
mylog = { git = "https://github.com/scottlamb/mylog" }
|
mylog = { git = "https://github.com/scottlamb/mylog" }
|
||||||
nix = "0.16.1"
|
nix = "0.16.1"
|
||||||
|
nom = "5.1.1"
|
||||||
openssl = "0.10"
|
openssl = "0.10"
|
||||||
parking_lot = { version = "0.9", features = [] }
|
parking_lot = { version = "0.9", features = [] }
|
||||||
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||||
reffers = "0.6.0"
|
reffers = "0.6.0"
|
||||||
regex = "1.0"
|
|
||||||
ring = "0.14.6"
|
ring = "0.14.6"
|
||||||
rusqlite = "0.21.0"
|
rusqlite = "0.21.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -17,5 +17,5 @@ lazy_static = "1.0"
|
|||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
parking_lot = { version = "0.9", features = [] }
|
parking_lot = { version = "0.9", features = [] }
|
||||||
regex = "1.0"
|
nom = "5.1.1"
|
||||||
time = "0.1"
|
time = "0.1"
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
pub mod clock;
|
pub mod clock;
|
||||||
|
pub mod time;
|
||||||
mod error;
|
mod error;
|
||||||
pub mod strutil;
|
pub mod strutil;
|
||||||
|
|
||||||
|
@ -28,10 +28,13 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
use lazy_static::lazy_static;
|
use nom::IResult;
|
||||||
use regex::Regex;
|
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::fmt::Write as _;
|
||||||
use std::str::FromStr as _;
|
|
||||||
|
|
||||||
static MULTIPLIERS: [(char, u64); 4] = [
|
static MULTIPLIERS: [(char, u64); 4] = [
|
||||||
// (suffix character, power of 2)
|
// (suffix character, power of 2)
|
||||||
@ -58,32 +61,33 @@ pub fn encode_size(mut raw: i64) -> String {
|
|||||||
encoded
|
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.
|
/// Decodes a human-readable size as output by encode_size.
|
||||||
pub fn decode_size(encoded: &str) -> Result<i64, ()> {
|
pub fn decode_size(encoded: &str) -> Result<i64, ()> {
|
||||||
let mut decoded = 0i64;
|
let (remaining, decoded) = decode_size_internal(encoded).map_err(|_e| ())?;
|
||||||
lazy_static! {
|
if !remaining.is_empty() {
|
||||||
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<<n;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decoded += piece;
|
|
||||||
}
|
|
||||||
if last_pos < encoded.len() {
|
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
Ok(decoded)
|
Ok(decoded)
|
||||||
@ -130,6 +134,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_decode() {
|
fn test_decode() {
|
||||||
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
|
assert_eq!(super::decode_size("100M").unwrap(), 100i64 << 20);
|
||||||
|
assert_eq!(super::decode_size("100M 42").unwrap(), (100i64 << 20) + 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
329
base/time.rs
Normal file
329
base/time.rs
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
||||||
|
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// In addition, as a special exception, the copyright holders give
|
||||||
|
// permission to link the code of portions of this program with the
|
||||||
|
// OpenSSL library under certain conditions as described in each
|
||||||
|
// individual source file, and distribute linked combinations including
|
||||||
|
// the two.
|
||||||
|
//
|
||||||
|
// You must obey the GNU General Public License in all respects for all
|
||||||
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
||||||
|
// exception, you may extend this exception to your version of the
|
||||||
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
||||||
|
// so, delete this exception statement from your version. If you delete
|
||||||
|
// this exception statement from all source files in the program, then
|
||||||
|
// also delete it here.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//! 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<I, O, nom::error::VerboseError<&'a str>>;
|
||||||
|
|
||||||
|
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<Self, Error> {
|
||||||
|
// 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, Self::Err> { 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<Duration> for Time {
|
||||||
|
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Add<Duration> for Time {
|
||||||
|
type Output = Time;
|
||||||
|
fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Sub<Duration> 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,6 @@ openssl = "0.10"
|
|||||||
parking_lot = { version = "0.9", features = [] }
|
parking_lot = { version = "0.9", features = [] }
|
||||||
prettydiff = "0.3.1"
|
prettydiff = "0.3.1"
|
||||||
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
protobuf = { git = "https://github.com/stepancheg/rust-protobuf" }
|
||||||
regex = "1.0"
|
|
||||||
rusqlite = "0.21.0"
|
rusqlite = "0.21.0"
|
||||||
smallvec = "1.0"
|
smallvec = "1.0"
|
||||||
tempdir = "0.3"
|
tempdir = "0.3"
|
||||||
|
238
db/recording.rs
238
db/recording.rs
@ -30,199 +30,17 @@
|
|||||||
|
|
||||||
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
|
use crate::coding::{append_varint32, decode_varint32, unzigzag32, zigzag32};
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use failure::{Error, bail, format_err};
|
use failure::{Error, bail};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::trace;
|
use log::trace;
|
||||||
use regex::Regex;
|
|
||||||
use std::ops;
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Range;
|
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 DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC;
|
||||||
pub const MAX_RECORDING_DURATION: i64 = 5 * 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.
|
pub use base::time::Time;
|
||||||
#[derive(Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)]
|
pub use base::time::Duration;
|
||||||
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<Self, Error> {
|
|
||||||
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<Duration> for Time {
|
|
||||||
fn add_assign(&mut self, rhs: Duration) { self.0 += rhs.0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Add<Duration> for Time {
|
|
||||||
type Output = Time;
|
|
||||||
fn add(self, rhs: Duration) -> Time { Time(self.0 + rhs.0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ops::Sub<Duration> 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 }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An iterator through a sample index.
|
/// An iterator through a sample index.
|
||||||
/// Initially invalid; call `next()` before each read.
|
/// Initially invalid; call `next()` before each read.
|
||||||
@ -533,52 +351,6 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::testutil::{self, TestDb};
|
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.
|
/// Tests encoding the example from design/schema.md.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encode_example() {
|
fn test_encode_example() {
|
||||||
|
29
src/h264.rs
29
src/h264.rs
@ -42,8 +42,6 @@
|
|||||||
|
|
||||||
use byteorder::{BigEndian, WriteBytesExt};
|
use byteorder::{BigEndian, WriteBytesExt};
|
||||||
use failure::{Error, bail};
|
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
|
// See ISO/IEC 14496-10 table 7-1 - NAL unit type codes, syntax element categories, and NAL unit
|
||||||
// type classes.
|
// 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
|
/// 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.
|
/// 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> {
|
where F: FnMut(&'a [u8]) -> Result<(), Error> {
|
||||||
lazy_static! {
|
let start_code = &b"\x00\x00\x01"[..];
|
||||||
static ref START_CODE: Regex = Regex::new(r"(\x00{2,}\x01)").unwrap();
|
use nom::FindSubstring;
|
||||||
}
|
'outer: while let Some(pos) = data.find_substring(start_code) {
|
||||||
for unit in START_CODE.split(data) {
|
let mut unit = &data[0..pos];
|
||||||
if !unit.is_empty() {
|
data = &data[pos + start_code.len() ..];
|
||||||
f(unit)?;
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
109
src/web.rs
109
src/web.rs
@ -34,7 +34,6 @@ use bytes::Bytes;
|
|||||||
use crate::body::{Body, BoxedError};
|
use crate::body::{Body, BoxedError};
|
||||||
use crate::json;
|
use crate::json;
|
||||||
use crate::mp4;
|
use crate::mp4;
|
||||||
use base64;
|
|
||||||
use bytes::{BufMut, BytesMut};
|
use bytes::{BufMut, BytesMut};
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
@ -46,12 +45,12 @@ use futures::sink::SinkExt;
|
|||||||
use futures::future::{self, Future, TryFutureExt};
|
use futures::future::{self, Future, TryFutureExt};
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use http::{Request, Response, status::StatusCode};
|
use http::{Request, Response, status::StatusCode};
|
||||||
use http_serve;
|
|
||||||
use http::header::{self, HeaderValue};
|
use http::header::{self, HeaderValue};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use regex::Regex;
|
use nom::IResult;
|
||||||
use serde_json;
|
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::collections::HashMap;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -64,14 +63,6 @@ use tokio_tungstenite::tungstenite;
|
|||||||
use url::form_urlencoded;
|
use url::form_urlencoded;
|
||||||
use uuid::Uuid;
|
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<dyn Future<Output = Result<Response<Body>, BoxedError>> +
|
type BoxedFuture = Box<dyn Future<Output = Result<Response<Body>, BoxedError>> +
|
||||||
Sync + Send + 'static>;
|
Sync + Send + 'static>;
|
||||||
|
|
||||||
@ -202,41 +193,48 @@ struct Segments {
|
|||||||
end_time: Option<i64>,
|
end_time: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl Segments {
|
||||||
pub fn parse(input: &str) -> Result<Segments, ()> {
|
/// Parses the `s` query parameter to `view.mp4` as described in `design/api.md`.
|
||||||
let caps = SEGMENTS_RE.captures(input).ok_or(())?;
|
/// Doesn't do any validation.
|
||||||
let ids_start = i32::from_str(caps.get(1).unwrap().as_str()).map_err(|_| ())?;
|
fn parse(i: &str) -> IResult<&str, Segments> {
|
||||||
let ids_end = match caps.get(2) {
|
// Parse START_ID[-END_ID] into Range<i32>.
|
||||||
Some(m) => i32::from_str(&m.as_str()[1..]).map_err(|_| ())?,
|
// Note that END_ID is inclusive, but Ranges are half-open.
|
||||||
None => ids_start,
|
let (i, ids) = map(tuple((num::<i32>(), opt(preceded(tag("-"), num::<i32>())))),
|
||||||
} + 1;
|
|(start, end)| start .. end.unwrap_or(start) + 1)(i)?;
|
||||||
let open_id = match caps.get(3) {
|
|
||||||
Some(m) => Some(u32::from_str(&m.as_str()[1..]).map_err(|_| ())?),
|
// Parse [@OPEN_ID] into Option<u32>.
|
||||||
None => None,
|
let (i, open_id) = opt(preceded(tag("@"), num::<u32>()))(i)?;
|
||||||
};
|
|
||||||
if ids_start < 0 || ids_end <= ids_start {
|
// Parse [.[REL_START_TIME]-[REL_END_TIME]] into (i64, Option<i64>).
|
||||||
|
let (i, (start_time, end_time)) = map(
|
||||||
|
opt(preceded(tag("."), tuple((opt(num::<i64>()), tag("-"), opt(num::<i64>()))))),
|
||||||
|
|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<Self, Self::Err> {
|
||||||
|
let (_, s) = all_consuming(Segments::parse)(s).map_err(|_| ())?;
|
||||||
|
if s.ids.end <= s.ids.start {
|
||||||
return Err(());
|
return Err(());
|
||||||
}
|
}
|
||||||
let start_time = caps.get(4).map_or(Ok(0), |m| i64::from_str(m.as_str())).map_err(|_| ())?;
|
if let Some(e) = s.end_time {
|
||||||
if start_time < 0 {
|
if e < s.start_time {
|
||||||
return Err(());
|
return Err(());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let end_time = match caps.get(5) {
|
Ok(s)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,7 +420,7 @@ impl ServiceInner {
|
|||||||
let (key, value) = (key.borrow(), value.borrow());
|
let (key, value) = (key.borrow(), value.borrow());
|
||||||
match key {
|
match key {
|
||||||
"s" => {
|
"s" => {
|
||||||
let s = Segments::parse(value).map_err(
|
let s = Segments::from_str(value).map_err(
|
||||||
|()| plain_response(StatusCode::BAD_REQUEST,
|
|()| plain_response(StatusCode::BAD_REQUEST,
|
||||||
format!("invalid s parameter: {}", value)))?;
|
format!("invalid s parameter: {}", value)))?;
|
||||||
debug!("stream_view_mp4: appending s={:?}", s);
|
debug!("stream_view_mp4: appending s={:?}", s);
|
||||||
@ -1078,6 +1076,7 @@ mod tests {
|
|||||||
use futures::future::FutureExt;
|
use futures::future::FutureExt;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
use super::Segments;
|
use super::Segments;
|
||||||
|
|
||||||
struct Server {
|
struct Server {
|
||||||
@ -1233,25 +1232,25 @@ mod tests {
|
|||||||
fn test_segments() {
|
fn test_segments() {
|
||||||
testutil::init();
|
testutil::init();
|
||||||
assert_eq!(Segments{ids: 1..2, open_id: None, start_time: 0, end_time: None},
|
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},
|
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},
|
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},
|
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)},
|
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)},
|
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},
|
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},
|
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)},
|
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)},
|
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]
|
#[tokio::test]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user