// This file is part of Moonfire NVR, a security camera network video recorder. // Copyright (C) 2020 The Moonfire NVR Authors; see AUTHORS and LICENSE.txt. // SPDX-License-Identifier: GPL-v3.0-or-later WITH GPL-3.0-linking-exception. //! Time and durations for Moonfire NVR's internal format. use crate::{bail, err, Error}; 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 serde::{Deserialize, Serialize}; use std::fmt; use std::ops; use std::str::FromStr; 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, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] 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 FnMut(&'a str) -> IResult<'a, &'a str, i32> { map_res( take_while_m_n(len, len, |c: char| c.is_ascii_digit()), |input: &str| input.parse::(), ) } /// 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 const MIN: Self = Time(i64::MIN); pub const MAX: Self = Time(i64::MAX); 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) } /// 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. if let Ok(i) = i64::from_str(input) { return Ok(Time(i)); } // 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(_) => err!(InvalidArgument, msg("incomplete")), nom::Err::Error(e) | nom::Err::Failure(e) => { err!(InvalidArgument, source(nom::error::convert_error(input, e))) } })?; if !remaining.is_empty() { bail!( InvalidArgument, msg("unexpected suffix {remaining:?} following time string") ); } 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!(InvalidArgument, msg("time {input:?} has month 0")); } tm.tm_mon -= 1; if tm.tm_year < 1900 { bail!(InvalidArgument, msg("time {input:?} has year before 1900")); } 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").map_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, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct Duration(pub i64); impl Duration { pub fn to_tm_duration(&self) -> time::Duration { time::Duration::nanoseconds(self.0 * 100000 / 9) } } impl fmt::Debug for Duration { 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 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 std::convert::TryFrom for Duration { type Error = std::num::TryFromIntError; fn try_from(value: std::time::Duration) -> Result { Ok(Self(i64::try_from(value.as_nanos() * 9 / 100_000)?)) } } impl ops::Mul for Duration { type Output = Self; fn mul(self, rhs: i64) -> Self::Output { Duration(self.0 * rhs) } } impl std::ops::Neg for Duration { type Output = Self; fn neg(self) -> Self::Output { Duration(-self.0) } } 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}; use std::convert::TryFrom; #[test] fn test_parse_time() { std::env::set_var("TZ", "America/Los_Angeles"); time::tzset(); #[rustfmt::skip] 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))); } } #[test] fn test_duration_from_std_duration() { assert_eq!( Duration::try_from(std::time::Duration::new(1, 11111)), Ok(Duration(90_000)) ); assert_eq!( Duration::try_from(std::time::Duration::new(1, 11112)), Ok(Duration(90_001)) ); assert_eq!( Duration::try_from(std::time::Duration::new(60, 0)), Ok(Duration(60 * TIME_UNITS_PER_SEC)) ); Duration::try_from(std::time::Duration::new(u64::MAX, 0)).unwrap_err(); } }