"pbs-buildcfg",
"pbs-datastore",
"pbs-runtime",
+ "pbs-systemd",
"pbs-tools",
]
pbs-buildcfg = { path = "pbs-buildcfg" }
pbs-datastore = { path = "pbs-datastore" }
pbs-runtime = { path = "pbs-runtime" }
+pbs-systemd = { path = "pbs-systemd" }
pbs-tools = { path = "pbs-tools" }
[features]
pbs-buildcfg \
pbs-datastore \
pbs-runtime \
+ pbs-systemd \
pbs-tools
ifeq ($(BUILD_MODE), release)
--- /dev/null
+[package]
+name = "pbs-systemd"
+version = "0.1.0"
+authors = ["Proxmox Support Team <support@proxmox.com>"]
+edition = "2018"
+description = "common systemd-related helpers, but no unit parsing"
+
+[dependencies]
+anyhow = "1.0"
+bitflags = "1.2.1"
+lazy_static = "1.4"
+nom = "5.1"
+
+proxmox = { version = "0.11.5", default-features = false }
+
+pbs-tools = { path = "../pbs-tools" }
--- /dev/null
+pub mod time;
+
+mod parse_time;
+mod unit;
+pub use unit::*;
--- /dev/null
+use std::collections::HashMap;
+
+use anyhow::{Error};
+use lazy_static::lazy_static;
+
+use super::time::*;
+
+use pbs_tools::nom::{
+ parse_complete_line, parse_u64, parse_error, IResult,
+};
+
+use nom::{
+ error::{context},
+ bytes::complete::{tag, take_while1},
+ combinator::{map_res, opt, recognize},
+ sequence::{pair, preceded, tuple},
+ character::complete::{alpha1, space0, digit1},
+ multi::separated_nonempty_list,
+};
+
+lazy_static! {
+ pub static ref TIME_SPAN_UNITS: HashMap<&'static str, f64> = {
+ let mut map = HashMap::new();
+
+ let second = 1.0;
+
+ map.insert("seconds", second);
+ map.insert("second", second);
+ map.insert("sec", second);
+ map.insert("s", second);
+
+ let msec = second / 1000.0;
+
+ map.insert("msec", msec);
+ map.insert("ms", msec);
+
+ let usec = msec / 1000.0;
+
+ map.insert("usec", usec);
+ map.insert("us", usec);
+ map.insert("µs", usec);
+
+ let nsec = usec / 1000.0;
+
+ map.insert("nsec", nsec);
+ map.insert("ns", nsec);
+
+ let minute = second * 60.0;
+
+ map.insert("minutes", minute);
+ map.insert("minute", minute);
+ map.insert("min", minute);
+ map.insert("m", minute);
+
+ let hour = minute * 60.0;
+
+ map.insert("hours", hour);
+ map.insert("hour", hour);
+ map.insert("hr", hour);
+ map.insert("h", hour);
+
+ let day = hour * 24.0 ;
+
+ map.insert("days", day);
+ map.insert("day", day);
+ map.insert("d", day);
+
+ let week = day * 7.0;
+
+ map.insert("weeks", week);
+ map.insert("week", week);
+ map.insert("w", week);
+
+ let month = 30.44 * day;
+
+ map.insert("months", month);
+ map.insert("month", month);
+ map.insert("M", month);
+
+ let year = 365.25 * day;
+
+ map.insert("years", year);
+ map.insert("year", year);
+ map.insert("y", year);
+
+ map
+ };
+}
+
+struct TimeSpec {
+ hour: Vec<DateTimeValue>,
+ minute: Vec<DateTimeValue>,
+ second: Vec<DateTimeValue>,
+}
+
+struct DateSpec {
+ year: Vec<DateTimeValue>,
+ month: Vec<DateTimeValue>,
+ day: Vec<DateTimeValue>,
+}
+
+fn parse_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, u32> {
+ move |i: &str| {
+ let (i, v) = map_res(recognize(digit1), str::parse)(i)?;
+ if (v as usize) >= max {
+ return Err(parse_error(i, "time value too large"));
+ }
+ Ok((i, v))
+ }
+}
+
+fn parse_weekday(i: &str) -> IResult<&str, WeekDays> {
+ let (i, text) = alpha1(i)?;
+
+ match text.to_ascii_lowercase().as_str() {
+ "monday" | "mon" => Ok((i, WeekDays::MONDAY)),
+ "tuesday" | "tue" => Ok((i, WeekDays::TUESDAY)),
+ "wednesday" | "wed" => Ok((i, WeekDays::WEDNESDAY)),
+ "thursday" | "thu" => Ok((i, WeekDays::THURSDAY)),
+ "friday" | "fri" => Ok((i, WeekDays::FRIDAY)),
+ "saturday" | "sat" => Ok((i, WeekDays::SATURDAY)),
+ "sunday" | "sun" => Ok((i, WeekDays::SUNDAY)),
+ _ => return Err(parse_error(text, "weekday")),
+ }
+}
+
+fn parse_weekdays_range(i: &str) -> IResult<&str, WeekDays> {
+ let (i, startday) = parse_weekday(i)?;
+
+ let generate_range = |start, end| {
+ let mut res = 0;
+ let mut pos = start;
+ loop {
+ res |= pos;
+ if pos >= end { break; }
+ pos <<= 1;
+ }
+ WeekDays::from_bits(res).unwrap()
+ };
+
+ if let (i, Some((_, endday))) = opt(pair(tag(".."),parse_weekday))(i)? {
+ let start = startday.bits();
+ let end = endday.bits();
+ if start > end {
+ let set1 = generate_range(start, WeekDays::SUNDAY.bits());
+ let set2 = generate_range(WeekDays::MONDAY.bits(), end);
+ Ok((i, set1 | set2))
+ } else {
+ Ok((i, generate_range(start, end)))
+ }
+ } else {
+ Ok((i, startday))
+ }
+}
+
+fn parse_date_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, DateTimeValue> {
+ move |i: &str| {
+ let (i, value) = parse_time_comp(max)(i)?;
+
+ if let (i, Some(end)) = opt(preceded(tag(".."), parse_time_comp(max)))(i)? {
+ if value > end {
+ return Err(parse_error(i, "range start is bigger than end"));
+ }
+ return Ok((i, DateTimeValue::Range(value, end)))
+ }
+
+ if let Some(time) = i.strip_prefix('/') {
+ let (time, repeat) = parse_time_comp(max)(time)?;
+ Ok((time, DateTimeValue::Repeated(value, repeat)))
+ } else {
+ Ok((i, DateTimeValue::Single(value)))
+ }
+ }
+}
+
+fn parse_date_time_comp_list(start: u32, max: usize) -> impl Fn(&str) -> IResult<&str, Vec<DateTimeValue>> {
+ move |i: &str| {
+ if let Some(rest) = i.strip_prefix('*') {
+ if let Some(time) = rest.strip_prefix('/') {
+ let (n, repeat) = parse_time_comp(max)(time)?;
+ if repeat > 0 {
+ return Ok((n, vec![DateTimeValue::Repeated(start, repeat)]));
+ }
+ }
+ return Ok((rest, Vec::new()));
+ }
+
+ separated_nonempty_list(tag(","), parse_date_time_comp(max))(i)
+ }
+}
+
+fn parse_time_spec(i: &str) -> IResult<&str, TimeSpec> {
+
+ let (i, (hour, minute, opt_second)) = tuple((
+ parse_date_time_comp_list(0, 24),
+ preceded(tag(":"), parse_date_time_comp_list(0, 60)),
+ opt(preceded(tag(":"), parse_date_time_comp_list(0, 60))),
+ ))(i)?;
+
+ if let Some(second) = opt_second {
+ Ok((i, TimeSpec { hour, minute, second }))
+ } else {
+ Ok((i, TimeSpec { hour, minute, second: vec![DateTimeValue::Single(0)] }))
+ }
+}
+
+fn parse_date_spec(i: &str) -> IResult<&str, DateSpec> {
+
+ // TODO: implement ~ for days (man systemd.time)
+ if let Ok((i, (year, month, day))) = tuple((
+ parse_date_time_comp_list(0, 2200), // the upper limit for systemd, stay compatible
+ preceded(tag("-"), parse_date_time_comp_list(1, 13)),
+ preceded(tag("-"), parse_date_time_comp_list(1, 32)),
+ ))(i) {
+ Ok((i, DateSpec { year, month, day }))
+ } else if let Ok((i, (month, day))) = tuple((
+ parse_date_time_comp_list(1, 13),
+ preceded(tag("-"), parse_date_time_comp_list(1, 32)),
+ ))(i) {
+ Ok((i, DateSpec { year: Vec::new(), month, day }))
+ } else {
+ Err(parse_error(i, "invalid date spec"))
+ }
+}
+
+pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> {
+ parse_complete_line("calendar event", i, parse_calendar_event_incomplete)
+}
+
+fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> {
+
+ let mut has_dayspec = false;
+ let mut has_timespec = false;
+ let mut has_datespec = false;
+
+ let mut event = CalendarEvent::default();
+
+ if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) {
+
+ match i {
+ "minutely" => {
+ return Ok(("", CalendarEvent {
+ second: vec![DateTimeValue::Single(0)],
+ ..Default::default()
+ }));
+ }
+ "hourly" => {
+ return Ok(("", CalendarEvent {
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ ..Default::default()
+ }));
+ }
+ "daily" => {
+ return Ok(("", CalendarEvent {
+ hour: vec![DateTimeValue::Single(0)],
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ ..Default::default()
+ }));
+ }
+ "weekly" => {
+ return Ok(("", CalendarEvent {
+ hour: vec![DateTimeValue::Single(0)],
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ days: WeekDays::MONDAY,
+ ..Default::default()
+ }));
+ }
+ "monthly" => {
+ return Ok(("", CalendarEvent {
+ hour: vec![DateTimeValue::Single(0)],
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ day: vec![DateTimeValue::Single(1)],
+ ..Default::default()
+ }));
+ }
+ "yearly" | "annually" => {
+ return Ok(("", CalendarEvent {
+ hour: vec![DateTimeValue::Single(0)],
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ day: vec![DateTimeValue::Single(1)],
+ month: vec![DateTimeValue::Single(1)],
+ ..Default::default()
+ }));
+ }
+ "quarterly" => {
+ return Ok(("", CalendarEvent {
+ hour: vec![DateTimeValue::Single(0)],
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ day: vec![DateTimeValue::Single(1)],
+ month: vec![
+ DateTimeValue::Single(1),
+ DateTimeValue::Single(4),
+ DateTimeValue::Single(7),
+ DateTimeValue::Single(10),
+ ],
+ ..Default::default()
+ }));
+ }
+ "semiannually" | "semi-annually" => {
+ return Ok(("", CalendarEvent {
+ hour: vec![DateTimeValue::Single(0)],
+ minute: vec![DateTimeValue::Single(0)],
+ second: vec![DateTimeValue::Single(0)],
+ day: vec![DateTimeValue::Single(1)],
+ month: vec![
+ DateTimeValue::Single(1),
+ DateTimeValue::Single(7),
+ ],
+ ..Default::default()
+ }));
+ }
+ _ => { /* continue */ }
+ }
+
+ let (n, range_list) = context(
+ "weekday range list",
+ separated_nonempty_list(tag(","), parse_weekdays_range)
+ )(i)?;
+
+ has_dayspec = true;
+
+ i = space0(n)?.0;
+
+ for range in range_list { event.days.insert(range); }
+ }
+
+ if let (n, Some(date)) = opt(parse_date_spec)(i)? {
+ event.year = date.year;
+ event.month = date.month;
+ event.day = date.day;
+ has_datespec = true;
+ i = space0(n)?.0;
+ }
+
+ if let (n, Some(time)) = opt(parse_time_spec)(i)? {
+ event.hour = time.hour;
+ event.minute = time.minute;
+ event.second = time.second;
+ has_timespec = true;
+ i = n;
+ } else {
+ event.hour = vec![DateTimeValue::Single(0)];
+ event.minute = vec![DateTimeValue::Single(0)];
+ event.second = vec![DateTimeValue::Single(0)];
+ }
+
+ if !(has_dayspec || has_timespec || has_datespec) {
+ return Err(parse_error(i, "date or time specification"));
+ }
+
+ Ok((i, event))
+}
+
+fn parse_time_unit(i: &str) -> IResult<&str, &str> {
+ let (n, text) = take_while1(|c: char| char::is_ascii_alphabetic(&c) || c == 'µ')(i)?;
+ if TIME_SPAN_UNITS.contains_key(&text) {
+ Ok((n, text))
+ } else {
+ Err(parse_error(text, "time unit"))
+ }
+}
+
+
+pub fn parse_time_span(i: &str) -> Result<TimeSpan, Error> {
+ parse_complete_line("time span", i, parse_time_span_incomplete)
+}
+
+fn parse_time_span_incomplete(mut i: &str) -> IResult<&str, TimeSpan> {
+
+ let mut ts = TimeSpan::default();
+
+ loop {
+ i = space0(i)?.0;
+ if i.is_empty() { break; }
+ let (n, num) = parse_u64(i)?;
+ i = space0(n)?.0;
+
+ if let (n, Some(unit)) = opt(parse_time_unit)(i)? {
+ i = n;
+ match unit {
+ "seconds" | "second" | "sec" | "s" => {
+ ts.seconds += num;
+ }
+ "msec" | "ms" => {
+ ts.msec += num;
+ }
+ "usec" | "us" | "µs" => {
+ ts.usec += num;
+ }
+ "nsec" | "ns" => {
+ ts.nsec += num;
+ }
+ "minutes" | "minute" | "min" | "m" => {
+ ts.minutes += num;
+ }
+ "hours" | "hour" | "hr" | "h" => {
+ ts.hours += num;
+ }
+ "days" | "day" | "d" => {
+ ts.days += num;
+ }
+ "weeks" | "week" | "w" => {
+ ts.weeks += num;
+ }
+ "months" | "month" | "M" => {
+ ts.months += num;
+ }
+ "years" | "year" | "y" => {
+ ts.years += num;
+ }
+ _ => return Err(parse_error(unit, "internal error")),
+ }
+ } else {
+ ts.seconds += num;
+ }
+ }
+
+ Ok((i, ts))
+}
--- /dev/null
+use std::convert::TryInto;
+
+use anyhow::Error;
+use bitflags::bitflags;
+
+use proxmox::tools::time::TmEditor;
+
+pub use super::parse_time::*;
+
+bitflags!{
+ #[derive(Default)]
+ pub struct WeekDays: u8 {
+ const MONDAY = 1;
+ const TUESDAY = 2;
+ const WEDNESDAY = 4;
+ const THURSDAY = 8;
+ const FRIDAY = 16;
+ const SATURDAY = 32;
+ const SUNDAY = 64;
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum DateTimeValue {
+ Single(u32),
+ Range(u32, u32),
+ Repeated(u32, u32),
+}
+
+impl DateTimeValue {
+ // Test if the entry contains the value
+ pub fn contains(&self, value: u32) -> bool {
+ match self {
+ DateTimeValue::Single(v) => *v == value,
+ DateTimeValue::Range(start, end) => value >= *start && value <= *end,
+ DateTimeValue::Repeated(start, repetition) => {
+ if value >= *start {
+ if *repetition > 0 {
+ let offset = value - start;
+ offset % repetition == 0
+ } else {
+ *start == value
+ }
+ } else {
+ false
+ }
+ }
+ }
+ }
+
+ pub fn list_contains(list: &[DateTimeValue], value: u32) -> bool {
+ list.iter().any(|spec| spec.contains(value))
+ }
+
+ // Find an return an entry greater than value
+ pub fn find_next(list: &[DateTimeValue], value: u32) -> Option<u32> {
+ let mut next: Option<u32> = None;
+ let mut set_next = |v: u32| {
+ if let Some(n) = next {
+ if v < n { next = Some(v); }
+ } else {
+ next = Some(v);
+ }
+ };
+ for spec in list {
+ match spec {
+ DateTimeValue::Single(v) => {
+ if *v > value { set_next(*v); }
+ }
+ DateTimeValue::Range(start, end) => {
+ if value < *start {
+ set_next(*start);
+ } else {
+ let n = value + 1;
+ if n >= *start && n <= *end {
+ set_next(n);
+ }
+ }
+ }
+ DateTimeValue::Repeated(start, repetition) => {
+ if value < *start {
+ set_next(*start);
+ } else if *repetition > 0 {
+ set_next(start + ((value - start + repetition) / repetition) * repetition);
+ }
+ }
+ }
+ }
+
+ next
+ }
+}
+
+/// Calendar events may be used to refer to one or more points in time in a
+/// single expression. They are designed after the systemd.time Calendar Events
+/// specification, but are not guaranteed to be 100% compatible.
+#[derive(Default, Clone, Debug)]
+pub struct CalendarEvent {
+ /// the days in a week this event should trigger
+ pub days: WeekDays,
+ /// the second(s) this event should trigger
+ pub second: Vec<DateTimeValue>, // todo: support float values
+ /// the minute(s) this event should trigger
+ pub minute: Vec<DateTimeValue>,
+ /// the hour(s) this event should trigger
+ pub hour: Vec<DateTimeValue>,
+ /// the day(s) in a month this event should trigger
+ pub day: Vec<DateTimeValue>,
+ /// the month(s) in a year this event should trigger
+ pub month: Vec<DateTimeValue>,
+ /// the years(s) this event should trigger
+ pub year: Vec<DateTimeValue>,
+}
+
+#[derive(Default, Clone, Debug)]
+pub struct TimeSpan {
+ pub nsec: u64,
+ pub usec: u64,
+ pub msec: u64,
+ pub seconds: u64,
+ pub minutes: u64,
+ pub hours: u64,
+ pub days: u64,
+ pub weeks: u64,
+ pub months: u64,
+ pub years: u64,
+}
+
+impl From<TimeSpan> for f64 {
+ fn from(ts: TimeSpan) -> Self {
+ (ts.seconds as f64) +
+ ((ts.nsec as f64) / 1_000_000_000.0) +
+ ((ts.usec as f64) / 1_000_000.0) +
+ ((ts.msec as f64) / 1_000.0) +
+ ((ts.minutes as f64) * 60.0) +
+ ((ts.hours as f64) * 3600.0) +
+ ((ts.days as f64) * 3600.0 * 24.0) +
+ ((ts.weeks as f64) * 3600.0 * 24.0 * 7.0) +
+ ((ts.months as f64) * 3600.0 * 24.0 * 30.44) +
+ ((ts.years as f64) * 3600.0 * 24.0 * 365.25)
+ }
+}
+
+impl From<std::time::Duration> for TimeSpan {
+ fn from(duration: std::time::Duration) -> Self {
+ let mut duration = duration.as_nanos();
+ let nsec = (duration % 1000) as u64;
+ duration /= 1000;
+ let usec = (duration % 1000) as u64;
+ duration /= 1000;
+ let msec = (duration % 1000) as u64;
+ duration /= 1000;
+ let seconds = (duration % 60) as u64;
+ duration /= 60;
+ let minutes = (duration % 60) as u64;
+ duration /= 60;
+ let hours = (duration % 24) as u64;
+ duration /= 24;
+ let years = (duration as f64 / 365.25) as u64;
+ let ydays = (duration as f64 % 365.25) as u64;
+ let months = (ydays as f64 / 30.44) as u64;
+ let mdays = (ydays as f64 % 30.44) as u64;
+ let weeks = mdays / 7;
+ let days = mdays % 7;
+ Self {
+ nsec,
+ usec,
+ msec,
+ seconds,
+ minutes,
+ hours,
+ days,
+ weeks,
+ months,
+ years,
+ }
+ }
+}
+
+impl std::fmt::Display for TimeSpan {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+ let mut first = true;
+ { // block scope for mutable borrows
+ let mut do_write = |v: u64, unit: &str| -> Result<(), std::fmt::Error> {
+ if !first {
+ write!(f, " ")?;
+ }
+ first = false;
+ write!(f, "{}{}", v, unit)
+ };
+ if self.years > 0 {
+ do_write(self.years, "y")?;
+ }
+ if self.months > 0 {
+ do_write(self.months, "m")?;
+ }
+ if self.weeks > 0 {
+ do_write(self.weeks, "w")?;
+ }
+ if self.days > 0 {
+ do_write(self.days, "d")?;
+ }
+ if self.hours > 0 {
+ do_write(self.hours, "h")?;
+ }
+ if self.minutes > 0 {
+ do_write(self.minutes, "min")?;
+ }
+ }
+ if !first {
+ write!(f, " ")?;
+ }
+ let seconds = self.seconds as f64 + (self.msec as f64 / 1000.0);
+ if seconds >= 0.1 {
+ if seconds >= 1.0 || !first {
+ write!(f, "{:.0}s", seconds)?;
+ } else {
+ write!(f, "{:.1}s", seconds)?;
+ }
+ } else if first {
+ write!(f, "<0.1s")?;
+ }
+ Ok(())
+ }
+}
+
+pub fn verify_time_span(i: &str) -> Result<(), Error> {
+ parse_time_span(i)?;
+ Ok(())
+}
+
+pub fn verify_calendar_event(i: &str) -> Result<(), Error> {
+ parse_calendar_event(i)?;
+ Ok(())
+}
+
+pub fn compute_next_event(
+ event: &CalendarEvent,
+ last: i64,
+ utc: bool,
+) -> Result<Option<i64>, Error> {
+
+ let last = last + 1; // at least one second later
+
+ let all_days = event.days.is_empty() || event.days.is_all();
+
+ let mut t = TmEditor::with_epoch(last, utc)?;
+
+ let mut count = 0;
+
+ loop {
+ // cancel after 1000 loops
+ if count > 1000 {
+ return Ok(None);
+ } else {
+ count += 1;
+ }
+
+ if !event.year.is_empty() {
+ let year: u32 = t.year().try_into()?;
+ if !DateTimeValue::list_contains(&event.year, year) {
+ if let Some(n) = DateTimeValue::find_next(&event.year, year) {
+ t.add_years((n - year).try_into()?)?;
+ continue;
+ } else {
+ // if we have no valid year, we cannot find a correct timestamp
+ return Ok(None);
+ }
+ }
+ }
+
+ if !event.month.is_empty() {
+ let month: u32 = t.month().try_into()?;
+ if !DateTimeValue::list_contains(&event.month, month) {
+ if let Some(n) = DateTimeValue::find_next(&event.month, month) {
+ t.add_months((n - month).try_into()?)?;
+ } else {
+ // if we could not find valid month, retry next year
+ t.add_years(1)?;
+ }
+ continue;
+ }
+ }
+
+ if !event.day.is_empty() {
+ let day: u32 = t.day().try_into()?;
+ if !DateTimeValue::list_contains(&event.day, day) {
+ if let Some(n) = DateTimeValue::find_next(&event.day, day) {
+ t.add_days((n - day).try_into()?)?;
+ } else {
+ // if we could not find valid mday, retry next month
+ t.add_months(1)?;
+ }
+ continue;
+ }
+ }
+
+ if !all_days { // match day first
+ let day_num: u32 = t.day_num().try_into()?;
+ let day = WeekDays::from_bits(1<<day_num).unwrap();
+ if !event.days.contains(day) {
+ if let Some(n) = ((day_num+1)..7)
+ .find(|d| event.days.contains(WeekDays::from_bits(1<<d).unwrap()))
+ {
+ // try next day
+ t.add_days((n - day_num).try_into()?)?;
+ } else {
+ // try next week
+ t.add_days((7 - day_num).try_into()?)?;
+ }
+ continue;
+ }
+ }
+
+ // this day
+ if !event.hour.is_empty() {
+ let hour = t.hour().try_into()?;
+ if !DateTimeValue::list_contains(&event.hour, hour) {
+ if let Some(n) = DateTimeValue::find_next(&event.hour, hour) {
+ // test next hour
+ t.set_time(n.try_into()?, 0, 0)?;
+ } else {
+ // test next day
+ t.add_days(1)?;
+ }
+ continue;
+ }
+ }
+
+ // this hour
+ if !event.minute.is_empty() {
+ let minute = t.min().try_into()?;
+ if !DateTimeValue::list_contains(&event.minute, minute) {
+ if let Some(n) = DateTimeValue::find_next(&event.minute, minute) {
+ // test next minute
+ t.set_min_sec(n.try_into()?, 0)?;
+ } else {
+ // test next hour
+ t.set_time(t.hour() + 1, 0, 0)?;
+ }
+ continue;
+ }
+ }
+
+ // this minute
+ if !event.second.is_empty() {
+ let second = t.sec().try_into()?;
+ if !DateTimeValue::list_contains(&event.second, second) {
+ if let Some(n) = DateTimeValue::find_next(&event.second, second) {
+ // test next second
+ t.set_sec(n.try_into()?)?;
+ } else {
+ // test next min
+ t.set_min_sec(t.min() + 1, 0)?;
+ }
+ continue;
+ }
+ }
+
+ let next = t.into_epoch()?;
+ return Ok(Some(next))
+ }
+}
+
+#[cfg(test)]
+mod test {
+
+ use anyhow::bail;
+
+ use super::*;
+ use proxmox::tools::time::*;
+
+ fn test_event(v: &'static str) -> Result<(), Error> {
+ match parse_calendar_event(v) {
+ Ok(event) => println!("CalendarEvent '{}' => {:?}", v, event),
+ Err(err) => bail!("parsing '{}' failed - {}", v, err),
+ }
+
+ Ok(())
+ }
+
+ const fn make_test_time(mday: i32, hour: i32, min: i32) -> libc::time_t {
+ (mday*3600*24 + hour*3600 + min*60) as libc::time_t
+ }
+
+ #[test]
+ fn test_compute_next_event() -> Result<(), Error> {
+
+ let test_value = |v: &'static str, last: i64, expect: i64| -> Result<i64, Error> {
+ let event = match parse_calendar_event(v) {
+ Ok(event) => event,
+ Err(err) => bail!("parsing '{}' failed - {}", v, err),
+ };
+
+ match compute_next_event(&event, last, true) {
+ Ok(Some(next)) => {
+ if next == expect {
+ println!("next {:?} => {}", event, next);
+ } else {
+ bail!("next {:?} failed\nnext: {:?}\nexpect: {:?}",
+ event, gmtime(next), gmtime(expect));
+ }
+ }
+ Ok(None) => bail!("next {:?} failed to find a timestamp", event),
+ Err(err) => bail!("compute next for '{}' failed - {}", v, err),
+ }
+
+ Ok(expect)
+ };
+
+ let test_never = |v: &'static str, last: i64| -> Result<(), Error> {
+ let event = match parse_calendar_event(v) {
+ Ok(event) => event,
+ Err(err) => bail!("parsing '{}' failed - {}", v, err),
+ };
+
+ match compute_next_event(&event, last, true)? {
+ None => Ok(()),
+ Some(next) => bail!("compute next for '{}' succeeded, but expected fail - result {}", v, next),
+ }
+ };
+
+ const MIN: i64 = 60;
+ const HOUR: i64 = 3600;
+ const DAY: i64 = 3600*24;
+
+ const THURSDAY_00_00: i64 = make_test_time(0, 0, 0);
+ const THURSDAY_15_00: i64 = make_test_time(0, 15, 0);
+
+ const JUL_31_2020: i64 = 1596153600; // Friday, 2020-07-31 00:00:00
+ const DEC_31_2020: i64 = 1609372800; // Thursday, 2020-12-31 00:00:00
+
+ test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
+ test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
+ test_value("*:*:*", THURSDAY_00_00, THURSDAY_00_00 + 1)?;
+ test_value("*:3:5", THURSDAY_00_00, THURSDAY_00_00 + 3*MIN + 5)?;
+
+ test_value("mon *:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY)?;
+ test_value("mon 2:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR)?;
+ test_value("mon 2:50", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR + 50*MIN)?;
+
+ test_value("tue", THURSDAY_00_00, THURSDAY_00_00 + 5*DAY)?;
+ test_value("wed", THURSDAY_00_00, THURSDAY_00_00 + 6*DAY)?;
+ test_value("thu", THURSDAY_00_00, THURSDAY_00_00 + 7*DAY)?;
+ test_value("fri", THURSDAY_00_00, THURSDAY_00_00 + 1*DAY)?;
+ test_value("sat", THURSDAY_00_00, THURSDAY_00_00 + 2*DAY)?;
+ test_value("sun", THURSDAY_00_00, THURSDAY_00_00 + 3*DAY)?;
+
+ // test multiple values for a single field
+ // and test that the order does not matter
+ test_value("5,10:4,8", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
+ test_value("10,5:8,4", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
+ test_value("6,4..10:23,5/5", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
+ test_value("4..10,6:5/5,23", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
+
+ // test month wrapping
+ test_value("sat", JUL_31_2020, JUL_31_2020 + 1*DAY)?;
+ test_value("sun", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
+ test_value("mon", JUL_31_2020, JUL_31_2020 + 3*DAY)?;
+ test_value("tue", JUL_31_2020, JUL_31_2020 + 4*DAY)?;
+ test_value("wed", JUL_31_2020, JUL_31_2020 + 5*DAY)?;
+ test_value("thu", JUL_31_2020, JUL_31_2020 + 6*DAY)?;
+ test_value("fri", JUL_31_2020, JUL_31_2020 + 7*DAY)?;
+
+ // test year wrapping
+ test_value("fri", DEC_31_2020, DEC_31_2020 + 1*DAY)?;
+ test_value("sat", DEC_31_2020, DEC_31_2020 + 2*DAY)?;
+ test_value("sun", DEC_31_2020, DEC_31_2020 + 3*DAY)?;
+ test_value("mon", DEC_31_2020, DEC_31_2020 + 4*DAY)?;
+ test_value("tue", DEC_31_2020, DEC_31_2020 + 5*DAY)?;
+ test_value("wed", DEC_31_2020, DEC_31_2020 + 6*DAY)?;
+ test_value("thu", DEC_31_2020, DEC_31_2020 + 7*DAY)?;
+
+ test_value("daily", THURSDAY_00_00, THURSDAY_00_00 + DAY)?;
+ test_value("daily", THURSDAY_00_00+1, THURSDAY_00_00 + DAY)?;
+
+ let n = test_value("5/2:0", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR)?;
+ let n = test_value("5/2:0", n, THURSDAY_00_00 + 7*HOUR)?;
+ let n = test_value("5/2:0", n, THURSDAY_00_00 + 9*HOUR)?;
+ test_value("5/2:0", n, THURSDAY_00_00 + 11*HOUR)?;
+
+ let mut n = test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
+ for i in 2..100 {
+ n = test_value("*:*", n, THURSDAY_00_00 + i*MIN)?;
+ }
+
+ let mut n = test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
+ for i in 2..100 {
+ n = test_value("*:0", n, THURSDAY_00_00 + i*HOUR)?;
+ }
+
+ let mut n = test_value("1:0", THURSDAY_15_00, THURSDAY_00_00 + DAY + HOUR)?;
+ for i in 2..100 {
+ n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
+ }
+
+ // test date functionality
+
+ test_value("2020-07-31", 0, JUL_31_2020)?;
+ test_value("02-28", 0, (31+27)*DAY)?;
+ test_value("02-29", 0, 2*365*DAY + (31+28)*DAY)?; // 1972-02-29
+ test_value("1965/5-01-01", -1, THURSDAY_00_00)?;
+ test_value("2020-7..9-2/2", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
+ test_value("2020,2021-12-31", JUL_31_2020, DEC_31_2020)?;
+
+ test_value("monthly", 0, 31*DAY)?;
+ test_value("quarterly", 0, (31+28+31)*DAY)?;
+ test_value("semiannually", 0, (31+28+31+30+31+30)*DAY)?;
+ test_value("yearly", 0, (365)*DAY)?;
+
+ test_never("2021-02-29", 0)?;
+ test_never("02-30", 0)?;
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_calendar_event_weekday() -> Result<(), Error> {
+ test_event("mon,wed..fri")?;
+ test_event("fri..mon")?;
+
+ test_event("mon")?;
+ test_event("MON")?;
+ test_event("monDay")?;
+ test_event("tue")?;
+ test_event("Tuesday")?;
+ test_event("wed")?;
+ test_event("wednesday")?;
+ test_event("thu")?;
+ test_event("thursday")?;
+ test_event("fri")?;
+ test_event("friday")?;
+ test_event("sat")?;
+ test_event("saturday")?;
+ test_event("sun")?;
+ test_event("sunday")?;
+
+ test_event("mon..fri")?;
+ test_event("mon,tue,fri")?;
+ test_event("mon,tue..wednesday,fri..sat")?;
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_time_span_parser() -> Result<(), Error> {
+
+ let test_value = |ts_str: &str, expect: f64| -> Result<(), Error> {
+ let ts = parse_time_span(ts_str)?;
+ assert_eq!(f64::from(ts), expect, "{}", ts_str);
+ Ok(())
+ };
+
+ test_value("2", 2.0)?;
+ test_value("2s", 2.0)?;
+ test_value("2sec", 2.0)?;
+ test_value("2second", 2.0)?;
+ test_value("2seconds", 2.0)?;
+
+ test_value(" 2s 2 s 2", 6.0)?;
+
+ test_value("1msec 1ms", 0.002)?;
+ test_value("1usec 1us 1µs", 0.000_003)?;
+ test_value("1nsec 1ns", 0.000_000_002)?;
+ test_value("1minutes 1minute 1min 1m", 4.0*60.0)?;
+ test_value("1hours 1hour 1hr 1h", 4.0*3600.0)?;
+ test_value("1days 1day 1d", 3.0*86400.0)?;
+ test_value("1weeks 1 week 1w", 3.0*86400.0*7.0)?;
+ test_value("1months 1month 1M", 3.0*86400.0*30.44)?;
+ test_value("1years 1year 1y", 3.0*86400.0*365.25)?;
+
+ test_value("2h", 7200.0)?;
+ test_value(" 2 h", 7200.0)?;
+ test_value("2hours", 7200.0)?;
+ test_value("48hr", 48.0*3600.0)?;
+ test_value("1y 12month", 365.25*24.0*3600.0 + 12.0*30.44*24.0*3600.0)?;
+ test_value("55s500ms", 55.5)?;
+ test_value("300ms20s 5day", 5.0*24.0*3600.0 + 20.0 + 0.3)?;
+
+ Ok(())
+ }
+}
--- /dev/null
+use anyhow::{bail, Error};
+
+use pbs_tools::run_command;
+
+/// Escape strings for usage in systemd unit names
+pub fn escape_unit(mut unit: &str, is_path: bool) -> String {
+ if is_path {
+ unit = unit.trim_matches('/');
+ if unit.is_empty() {
+ return String::from("-");
+ }
+ }
+
+ let unit = unit.as_bytes();
+
+ let mut escaped = String::new();
+
+ for (i, c) in unit.iter().enumerate() {
+ if *c == b'/' {
+ escaped.push('-');
+ continue;
+ }
+ if (i == 0 && *c == b'.')
+ || !(*c == b'_'
+ || *c == b'.'
+ || (*c >= b'0' && *c <= b'9')
+ || (*c >= b'A' && *c <= b'Z')
+ || (*c >= b'a' && *c <= b'z'))
+ {
+ escaped.push_str(&format!("\\x{:0x}", c));
+ } else {
+ escaped.push(*c as char);
+ }
+ }
+ escaped
+}
+
+fn parse_hex_digit(d: u8) -> Result<u8, Error> {
+ if d >= b'0' && d <= b'9' {
+ return Ok(d - b'0');
+ }
+ if d >= b'A' && d <= b'F' {
+ return Ok(d - b'A' + 10);
+ }
+ if d >= b'a' && d <= b'f' {
+ return Ok(d - b'a' + 10);
+ }
+ bail!("got invalid hex digit");
+}
+
+/// Unescape strings used in systemd unit names
+pub fn unescape_unit(text: &str) -> Result<String, Error> {
+ let mut i = text.as_bytes();
+
+ let mut data: Vec<u8> = Vec::new();
+
+ loop {
+ if i.is_empty() {
+ break;
+ }
+ let next = i[0];
+ if next == b'\\' {
+ if i.len() < 4 {
+ bail!("short input");
+ }
+ if i[1] != b'x' {
+ bail!("unkwnown escape sequence");
+ }
+ let h1 = parse_hex_digit(i[2])?;
+ let h0 = parse_hex_digit(i[3])?;
+ data.push(h1 << 4 | h0);
+ i = &i[4..]
+ } else if next == b'-' {
+ data.push(b'/');
+ i = &i[1..]
+ } else {
+ data.push(next);
+ i = &i[1..]
+ }
+ }
+
+ let text = String::from_utf8(data)?;
+
+ Ok(text)
+}
+
+pub fn reload_daemon() -> Result<(), Error> {
+ let mut command = std::process::Command::new("systemctl");
+ command.arg("daemon-reload");
+
+ run_command(command, None)?;
+
+ Ok(())
+}
+
+pub fn disable_unit(unit: &str) -> Result<(), Error> {
+ let mut command = std::process::Command::new("systemctl");
+ command.arg("disable");
+ command.arg(unit);
+
+ run_command(command, None)?;
+
+ Ok(())
+}
+
+pub fn enable_unit(unit: &str) -> Result<(), Error> {
+ let mut command = std::process::Command::new("systemctl");
+ command.arg("enable");
+ command.arg(unit);
+
+ run_command(command, None)?;
+
+ Ok(())
+}
+
+pub fn start_unit(unit: &str) -> Result<(), Error> {
+ let mut command = std::process::Command::new("systemctl");
+ command.arg("start");
+ command.arg(unit);
+
+ run_command(command, None)?;
+
+ Ok(())
+}
+
+pub fn stop_unit(unit: &str) -> Result<(), Error> {
+ let mut command = std::process::Command::new("systemctl");
+ command.arg("stop");
+ command.arg(unit);
+
+ run_command(command, None)?;
+
+ Ok(())
+}
+
+pub fn reload_unit(unit: &str) -> Result<(), Error> {
+ let mut command = std::process::Command::new("systemctl");
+ command.arg("try-reload-or-restart");
+ command.arg(unit);
+
+ run_command(command, None)?;
+
+ Ok(())
+}
+
+#[test]
+fn test_escape_unit() -> Result<(), Error> {
+ fn test_escape(i: &str, expected: &str, is_path: bool) {
+ let escaped = escape_unit(i, is_path);
+ assert_eq!(escaped, expected);
+ let unescaped = unescape_unit(&escaped).unwrap();
+ if is_path {
+ let mut p = i.trim_matches('/');
+ if p.is_empty() {
+ p = "/";
+ }
+ assert_eq!(p, unescaped);
+ } else {
+ assert_eq!(i, unescaped);
+ }
+ }
+
+ test_escape(".test", "\\x2etest", false);
+ test_escape("t.est", "t.est", false);
+ test_escape("_test_", "_test_", false);
+
+ test_escape("/", "-", false);
+ test_escape("//", "--", false);
+
+ test_escape("/", "-", true);
+ test_escape("//", "-", true);
+
+ Ok(())
+}
anyhow = "1.0"
libc = "0.2"
nix = "0.19.1"
+nom = "5.1"
regex = "1.2"
serde = "1.0"
serde_json = "1.0"
-pub mod types;
-pub mod config;
-
-mod parse_time;
-pub mod time;
-
-use anyhow::{bail, Error};
-
-/// Escape strings for usage in systemd unit names
-pub fn escape_unit(mut unit: &str, is_path: bool) -> String {
-
- if is_path {
- unit = unit.trim_matches('/');
- if unit.is_empty() {
- return String::from("-");
- }
- }
-
- let unit = unit.as_bytes();
-
- let mut escaped = String::new();
-
- for (i, c) in unit.iter().enumerate() {
- if *c == b'/' {
- escaped.push('-');
- continue;
- }
- if (i == 0 && *c == b'.')
- || !(*c == b'_' ||
- *c == b'.' ||
- (*c >= b'0' && *c <= b'9') ||
- (*c >= b'A' && *c <= b'Z') ||
- (*c >= b'a' && *c <= b'z')) {
- escaped.push_str(&format!("\\x{:0x}", c));
- } else {
- escaped.push(*c as char);
- }
- }
- escaped
-}
-
-fn parse_hex_digit(d: u8) -> Result<u8, Error> {
- if d >= b'0' && d <= b'9' { return Ok(d - b'0'); }
- if d >= b'A' && d <= b'F' { return Ok(d - b'A' + 10); }
- if d >= b'a' && d <= b'f' { return Ok(d - b'a' + 10); }
- bail!("got invalid hex digit");
-}
-
-/// Unescape strings used in systemd unit names
-pub fn unescape_unit(text: &str) -> Result<String, Error> {
-
- let mut i = text.as_bytes();
-
- let mut data: Vec<u8> = Vec::new();
-
- loop {
- if i.is_empty() { break; }
- let next = i[0];
- if next == b'\\' {
- if i.len() < 4 { bail!("short input"); }
- if i[1] != b'x' { bail!("unkwnown escape sequence"); }
- let h1 = parse_hex_digit(i[2])?;
- let h0 = parse_hex_digit(i[3])?;
- data.push(h1<<4|h0);
- i = &i[4..]
- } else if next == b'-' {
- data.push(b'/');
- i = &i[1..]
- } else {
- data.push(next);
- i = &i[1..]
- }
- }
-
- let text = String::from_utf8(data)?;
-
- Ok(text)
-}
-
-pub fn reload_daemon() -> Result<(), Error> {
-
- let mut command = std::process::Command::new("systemctl");
- command.arg("daemon-reload");
-
- crate::tools::run_command(command, None)?;
-
- Ok(())
-}
-
-pub fn disable_unit(unit: &str) -> Result<(), Error> {
-
- let mut command = std::process::Command::new("systemctl");
- command.arg("disable");
- command.arg(unit);
-
- crate::tools::run_command(command, None)?;
-
- Ok(())
-}
+pub use pbs_systemd::reload_daemon;
+pub use pbs_systemd::time;
+pub use pbs_systemd::{disable_unit, enable_unit, reload_unit, start_unit, stop_unit};
+pub use pbs_systemd::{escape_unit, unescape_unit};
-pub fn enable_unit(unit: &str) -> Result<(), Error> {
-
- let mut command = std::process::Command::new("systemctl");
- command.arg("enable");
- command.arg(unit);
-
- crate::tools::run_command(command, None)?;
-
- Ok(())
-}
-
-pub fn start_unit(unit: &str) -> Result<(), Error> {
-
- let mut command = std::process::Command::new("systemctl");
- command.arg("start");
- command.arg(unit);
-
- crate::tools::run_command(command, None)?;
-
- Ok(())
-}
-
-pub fn stop_unit(unit: &str) -> Result<(), Error> {
-
- let mut command = std::process::Command::new("systemctl");
- command.arg("stop");
- command.arg(unit);
-
- crate::tools::run_command(command, None)?;
-
- Ok(())
-}
-
-pub fn reload_unit(unit: &str) -> Result<(), Error> {
-
- let mut command = std::process::Command::new("systemctl");
- command.arg("try-reload-or-restart");
- command.arg(unit);
-
- crate::tools::run_command(command, None)?;
-
- Ok(())
-}
-
-#[test]
-fn test_escape_unit() -> Result<(), Error> {
-
- fn test_escape(i: &str, expected: &str, is_path: bool) {
- let escaped = escape_unit(i, is_path);
- assert_eq!(escaped, expected);
- let unescaped = unescape_unit(&escaped).unwrap();
- if is_path {
- let mut p = i.trim_matches('/');
- if p.is_empty() { p = "/"; }
- assert_eq!(p, unescaped);
- } else {
- assert_eq!(i, unescaped);
- }
- }
-
- test_escape(".test", "\\x2etest", false);
- test_escape("t.est", "t.est", false);
- test_escape("_test_", "_test_", false);
-
- test_escape("/", "-", false);
- test_escape("//", "--", false);
-
- test_escape("/", "-", true);
- test_escape("//", "-", true);
-
- Ok(())
-}
+pub mod config;
+pub mod types;
+++ /dev/null
-use std::collections::HashMap;
-
-use anyhow::{Error};
-use lazy_static::lazy_static;
-
-use super::time::*;
-
-use crate::tools::nom::{
- parse_complete_line, parse_u64, parse_error, IResult,
-};
-
-use nom::{
- error::{context},
- bytes::complete::{tag, take_while1},
- combinator::{map_res, opt, recognize},
- sequence::{pair, preceded, tuple},
- character::complete::{alpha1, space0, digit1},
- multi::separated_nonempty_list,
-};
-
-lazy_static! {
- pub static ref TIME_SPAN_UNITS: HashMap<&'static str, f64> = {
- let mut map = HashMap::new();
-
- let second = 1.0;
-
- map.insert("seconds", second);
- map.insert("second", second);
- map.insert("sec", second);
- map.insert("s", second);
-
- let msec = second / 1000.0;
-
- map.insert("msec", msec);
- map.insert("ms", msec);
-
- let usec = msec / 1000.0;
-
- map.insert("usec", usec);
- map.insert("us", usec);
- map.insert("µs", usec);
-
- let nsec = usec / 1000.0;
-
- map.insert("nsec", nsec);
- map.insert("ns", nsec);
-
- let minute = second * 60.0;
-
- map.insert("minutes", minute);
- map.insert("minute", minute);
- map.insert("min", minute);
- map.insert("m", minute);
-
- let hour = minute * 60.0;
-
- map.insert("hours", hour);
- map.insert("hour", hour);
- map.insert("hr", hour);
- map.insert("h", hour);
-
- let day = hour * 24.0 ;
-
- map.insert("days", day);
- map.insert("day", day);
- map.insert("d", day);
-
- let week = day * 7.0;
-
- map.insert("weeks", week);
- map.insert("week", week);
- map.insert("w", week);
-
- let month = 30.44 * day;
-
- map.insert("months", month);
- map.insert("month", month);
- map.insert("M", month);
-
- let year = 365.25 * day;
-
- map.insert("years", year);
- map.insert("year", year);
- map.insert("y", year);
-
- map
- };
-}
-
-struct TimeSpec {
- hour: Vec<DateTimeValue>,
- minute: Vec<DateTimeValue>,
- second: Vec<DateTimeValue>,
-}
-
-struct DateSpec {
- year: Vec<DateTimeValue>,
- month: Vec<DateTimeValue>,
- day: Vec<DateTimeValue>,
-}
-
-fn parse_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, u32> {
- move |i: &str| {
- let (i, v) = map_res(recognize(digit1), str::parse)(i)?;
- if (v as usize) >= max {
- return Err(parse_error(i, "time value too large"));
- }
- Ok((i, v))
- }
-}
-
-fn parse_weekday(i: &str) -> IResult<&str, WeekDays> {
- let (i, text) = alpha1(i)?;
-
- match text.to_ascii_lowercase().as_str() {
- "monday" | "mon" => Ok((i, WeekDays::MONDAY)),
- "tuesday" | "tue" => Ok((i, WeekDays::TUESDAY)),
- "wednesday" | "wed" => Ok((i, WeekDays::WEDNESDAY)),
- "thursday" | "thu" => Ok((i, WeekDays::THURSDAY)),
- "friday" | "fri" => Ok((i, WeekDays::FRIDAY)),
- "saturday" | "sat" => Ok((i, WeekDays::SATURDAY)),
- "sunday" | "sun" => Ok((i, WeekDays::SUNDAY)),
- _ => return Err(parse_error(text, "weekday")),
- }
-}
-
-fn parse_weekdays_range(i: &str) -> IResult<&str, WeekDays> {
- let (i, startday) = parse_weekday(i)?;
-
- let generate_range = |start, end| {
- let mut res = 0;
- let mut pos = start;
- loop {
- res |= pos;
- if pos >= end { break; }
- pos <<= 1;
- }
- WeekDays::from_bits(res).unwrap()
- };
-
- if let (i, Some((_, endday))) = opt(pair(tag(".."),parse_weekday))(i)? {
- let start = startday.bits();
- let end = endday.bits();
- if start > end {
- let set1 = generate_range(start, WeekDays::SUNDAY.bits());
- let set2 = generate_range(WeekDays::MONDAY.bits(), end);
- Ok((i, set1 | set2))
- } else {
- Ok((i, generate_range(start, end)))
- }
- } else {
- Ok((i, startday))
- }
-}
-
-fn parse_date_time_comp(max: usize) -> impl Fn(&str) -> IResult<&str, DateTimeValue> {
- move |i: &str| {
- let (i, value) = parse_time_comp(max)(i)?;
-
- if let (i, Some(end)) = opt(preceded(tag(".."), parse_time_comp(max)))(i)? {
- if value > end {
- return Err(parse_error(i, "range start is bigger than end"));
- }
- return Ok((i, DateTimeValue::Range(value, end)))
- }
-
- if let Some(time) = i.strip_prefix('/') {
- let (time, repeat) = parse_time_comp(max)(time)?;
- Ok((time, DateTimeValue::Repeated(value, repeat)))
- } else {
- Ok((i, DateTimeValue::Single(value)))
- }
- }
-}
-
-fn parse_date_time_comp_list(start: u32, max: usize) -> impl Fn(&str) -> IResult<&str, Vec<DateTimeValue>> {
- move |i: &str| {
- if let Some(rest) = i.strip_prefix('*') {
- if let Some(time) = rest.strip_prefix('/') {
- let (n, repeat) = parse_time_comp(max)(time)?;
- if repeat > 0 {
- return Ok((n, vec![DateTimeValue::Repeated(start, repeat)]));
- }
- }
- return Ok((rest, Vec::new()));
- }
-
- separated_nonempty_list(tag(","), parse_date_time_comp(max))(i)
- }
-}
-
-fn parse_time_spec(i: &str) -> IResult<&str, TimeSpec> {
-
- let (i, (hour, minute, opt_second)) = tuple((
- parse_date_time_comp_list(0, 24),
- preceded(tag(":"), parse_date_time_comp_list(0, 60)),
- opt(preceded(tag(":"), parse_date_time_comp_list(0, 60))),
- ))(i)?;
-
- if let Some(second) = opt_second {
- Ok((i, TimeSpec { hour, minute, second }))
- } else {
- Ok((i, TimeSpec { hour, minute, second: vec![DateTimeValue::Single(0)] }))
- }
-}
-
-fn parse_date_spec(i: &str) -> IResult<&str, DateSpec> {
-
- // TODO: implement ~ for days (man systemd.time)
- if let Ok((i, (year, month, day))) = tuple((
- parse_date_time_comp_list(0, 2200), // the upper limit for systemd, stay compatible
- preceded(tag("-"), parse_date_time_comp_list(1, 13)),
- preceded(tag("-"), parse_date_time_comp_list(1, 32)),
- ))(i) {
- Ok((i, DateSpec { year, month, day }))
- } else if let Ok((i, (month, day))) = tuple((
- parse_date_time_comp_list(1, 13),
- preceded(tag("-"), parse_date_time_comp_list(1, 32)),
- ))(i) {
- Ok((i, DateSpec { year: Vec::new(), month, day }))
- } else {
- Err(parse_error(i, "invalid date spec"))
- }
-}
-
-pub fn parse_calendar_event(i: &str) -> Result<CalendarEvent, Error> {
- parse_complete_line("calendar event", i, parse_calendar_event_incomplete)
-}
-
-fn parse_calendar_event_incomplete(mut i: &str) -> IResult<&str, CalendarEvent> {
-
- let mut has_dayspec = false;
- let mut has_timespec = false;
- let mut has_datespec = false;
-
- let mut event = CalendarEvent::default();
-
- if i.starts_with(|c: char| char::is_ascii_alphabetic(&c)) {
-
- match i {
- "minutely" => {
- return Ok(("", CalendarEvent {
- second: vec![DateTimeValue::Single(0)],
- ..Default::default()
- }));
- }
- "hourly" => {
- return Ok(("", CalendarEvent {
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- ..Default::default()
- }));
- }
- "daily" => {
- return Ok(("", CalendarEvent {
- hour: vec![DateTimeValue::Single(0)],
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- ..Default::default()
- }));
- }
- "weekly" => {
- return Ok(("", CalendarEvent {
- hour: vec![DateTimeValue::Single(0)],
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- days: WeekDays::MONDAY,
- ..Default::default()
- }));
- }
- "monthly" => {
- return Ok(("", CalendarEvent {
- hour: vec![DateTimeValue::Single(0)],
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- day: vec![DateTimeValue::Single(1)],
- ..Default::default()
- }));
- }
- "yearly" | "annually" => {
- return Ok(("", CalendarEvent {
- hour: vec![DateTimeValue::Single(0)],
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- day: vec![DateTimeValue::Single(1)],
- month: vec![DateTimeValue::Single(1)],
- ..Default::default()
- }));
- }
- "quarterly" => {
- return Ok(("", CalendarEvent {
- hour: vec![DateTimeValue::Single(0)],
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- day: vec![DateTimeValue::Single(1)],
- month: vec![
- DateTimeValue::Single(1),
- DateTimeValue::Single(4),
- DateTimeValue::Single(7),
- DateTimeValue::Single(10),
- ],
- ..Default::default()
- }));
- }
- "semiannually" | "semi-annually" => {
- return Ok(("", CalendarEvent {
- hour: vec![DateTimeValue::Single(0)],
- minute: vec![DateTimeValue::Single(0)],
- second: vec![DateTimeValue::Single(0)],
- day: vec![DateTimeValue::Single(1)],
- month: vec![
- DateTimeValue::Single(1),
- DateTimeValue::Single(7),
- ],
- ..Default::default()
- }));
- }
- _ => { /* continue */ }
- }
-
- let (n, range_list) = context(
- "weekday range list",
- separated_nonempty_list(tag(","), parse_weekdays_range)
- )(i)?;
-
- has_dayspec = true;
-
- i = space0(n)?.0;
-
- for range in range_list { event.days.insert(range); }
- }
-
- if let (n, Some(date)) = opt(parse_date_spec)(i)? {
- event.year = date.year;
- event.month = date.month;
- event.day = date.day;
- has_datespec = true;
- i = space0(n)?.0;
- }
-
- if let (n, Some(time)) = opt(parse_time_spec)(i)? {
- event.hour = time.hour;
- event.minute = time.minute;
- event.second = time.second;
- has_timespec = true;
- i = n;
- } else {
- event.hour = vec![DateTimeValue::Single(0)];
- event.minute = vec![DateTimeValue::Single(0)];
- event.second = vec![DateTimeValue::Single(0)];
- }
-
- if !(has_dayspec || has_timespec || has_datespec) {
- return Err(parse_error(i, "date or time specification"));
- }
-
- Ok((i, event))
-}
-
-fn parse_time_unit(i: &str) -> IResult<&str, &str> {
- let (n, text) = take_while1(|c: char| char::is_ascii_alphabetic(&c) || c == 'µ')(i)?;
- if TIME_SPAN_UNITS.contains_key(&text) {
- Ok((n, text))
- } else {
- Err(parse_error(text, "time unit"))
- }
-}
-
-
-pub fn parse_time_span(i: &str) -> Result<TimeSpan, Error> {
- parse_complete_line("time span", i, parse_time_span_incomplete)
-}
-
-fn parse_time_span_incomplete(mut i: &str) -> IResult<&str, TimeSpan> {
-
- let mut ts = TimeSpan::default();
-
- loop {
- i = space0(i)?.0;
- if i.is_empty() { break; }
- let (n, num) = parse_u64(i)?;
- i = space0(n)?.0;
-
- if let (n, Some(unit)) = opt(parse_time_unit)(i)? {
- i = n;
- match unit {
- "seconds" | "second" | "sec" | "s" => {
- ts.seconds += num;
- }
- "msec" | "ms" => {
- ts.msec += num;
- }
- "usec" | "us" | "µs" => {
- ts.usec += num;
- }
- "nsec" | "ns" => {
- ts.nsec += num;
- }
- "minutes" | "minute" | "min" | "m" => {
- ts.minutes += num;
- }
- "hours" | "hour" | "hr" | "h" => {
- ts.hours += num;
- }
- "days" | "day" | "d" => {
- ts.days += num;
- }
- "weeks" | "week" | "w" => {
- ts.weeks += num;
- }
- "months" | "month" | "M" => {
- ts.months += num;
- }
- "years" | "year" | "y" => {
- ts.years += num;
- }
- _ => return Err(parse_error(unit, "internal error")),
- }
- } else {
- ts.seconds += num;
- }
- }
-
- Ok((i, ts))
-}
+++ /dev/null
-use std::convert::TryInto;
-
-use anyhow::Error;
-use bitflags::bitflags;
-
-use proxmox::tools::time::TmEditor;
-
-pub use super::parse_time::*;
-
-bitflags!{
- #[derive(Default)]
- pub struct WeekDays: u8 {
- const MONDAY = 1;
- const TUESDAY = 2;
- const WEDNESDAY = 4;
- const THURSDAY = 8;
- const FRIDAY = 16;
- const SATURDAY = 32;
- const SUNDAY = 64;
- }
-}
-
-#[derive(Debug, Clone)]
-pub enum DateTimeValue {
- Single(u32),
- Range(u32, u32),
- Repeated(u32, u32),
-}
-
-impl DateTimeValue {
- // Test if the entry contains the value
- pub fn contains(&self, value: u32) -> bool {
- match self {
- DateTimeValue::Single(v) => *v == value,
- DateTimeValue::Range(start, end) => value >= *start && value <= *end,
- DateTimeValue::Repeated(start, repetition) => {
- if value >= *start {
- if *repetition > 0 {
- let offset = value - start;
- offset % repetition == 0
- } else {
- *start == value
- }
- } else {
- false
- }
- }
- }
- }
-
- pub fn list_contains(list: &[DateTimeValue], value: u32) -> bool {
- list.iter().any(|spec| spec.contains(value))
- }
-
- // Find an return an entry greater than value
- pub fn find_next(list: &[DateTimeValue], value: u32) -> Option<u32> {
- let mut next: Option<u32> = None;
- let mut set_next = |v: u32| {
- if let Some(n) = next {
- if v < n { next = Some(v); }
- } else {
- next = Some(v);
- }
- };
- for spec in list {
- match spec {
- DateTimeValue::Single(v) => {
- if *v > value { set_next(*v); }
- }
- DateTimeValue::Range(start, end) => {
- if value < *start {
- set_next(*start);
- } else {
- let n = value + 1;
- if n >= *start && n <= *end {
- set_next(n);
- }
- }
- }
- DateTimeValue::Repeated(start, repetition) => {
- if value < *start {
- set_next(*start);
- } else if *repetition > 0 {
- set_next(start + ((value - start + repetition) / repetition) * repetition);
- }
- }
- }
- }
-
- next
- }
-}
-
-/// Calendar events may be used to refer to one or more points in time in a
-/// single expression. They are designed after the systemd.time Calendar Events
-/// specification, but are not guaranteed to be 100% compatible.
-#[derive(Default, Clone, Debug)]
-pub struct CalendarEvent {
- /// the days in a week this event should trigger
- pub days: WeekDays,
- /// the second(s) this event should trigger
- pub second: Vec<DateTimeValue>, // todo: support float values
- /// the minute(s) this event should trigger
- pub minute: Vec<DateTimeValue>,
- /// the hour(s) this event should trigger
- pub hour: Vec<DateTimeValue>,
- /// the day(s) in a month this event should trigger
- pub day: Vec<DateTimeValue>,
- /// the month(s) in a year this event should trigger
- pub month: Vec<DateTimeValue>,
- /// the years(s) this event should trigger
- pub year: Vec<DateTimeValue>,
-}
-
-#[derive(Default, Clone, Debug)]
-pub struct TimeSpan {
- pub nsec: u64,
- pub usec: u64,
- pub msec: u64,
- pub seconds: u64,
- pub minutes: u64,
- pub hours: u64,
- pub days: u64,
- pub weeks: u64,
- pub months: u64,
- pub years: u64,
-}
-
-impl From<TimeSpan> for f64 {
- fn from(ts: TimeSpan) -> Self {
- (ts.seconds as f64) +
- ((ts.nsec as f64) / 1_000_000_000.0) +
- ((ts.usec as f64) / 1_000_000.0) +
- ((ts.msec as f64) / 1_000.0) +
- ((ts.minutes as f64) * 60.0) +
- ((ts.hours as f64) * 3600.0) +
- ((ts.days as f64) * 3600.0 * 24.0) +
- ((ts.weeks as f64) * 3600.0 * 24.0 * 7.0) +
- ((ts.months as f64) * 3600.0 * 24.0 * 30.44) +
- ((ts.years as f64) * 3600.0 * 24.0 * 365.25)
- }
-}
-
-impl From<std::time::Duration> for TimeSpan {
- fn from(duration: std::time::Duration) -> Self {
- let mut duration = duration.as_nanos();
- let nsec = (duration % 1000) as u64;
- duration /= 1000;
- let usec = (duration % 1000) as u64;
- duration /= 1000;
- let msec = (duration % 1000) as u64;
- duration /= 1000;
- let seconds = (duration % 60) as u64;
- duration /= 60;
- let minutes = (duration % 60) as u64;
- duration /= 60;
- let hours = (duration % 24) as u64;
- duration /= 24;
- let years = (duration as f64 / 365.25) as u64;
- let ydays = (duration as f64 % 365.25) as u64;
- let months = (ydays as f64 / 30.44) as u64;
- let mdays = (ydays as f64 % 30.44) as u64;
- let weeks = mdays / 7;
- let days = mdays % 7;
- Self {
- nsec,
- usec,
- msec,
- seconds,
- minutes,
- hours,
- days,
- weeks,
- months,
- years,
- }
- }
-}
-
-impl std::fmt::Display for TimeSpan {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
- let mut first = true;
- { // block scope for mutable borrows
- let mut do_write = |v: u64, unit: &str| -> Result<(), std::fmt::Error> {
- if !first {
- write!(f, " ")?;
- }
- first = false;
- write!(f, "{}{}", v, unit)
- };
- if self.years > 0 {
- do_write(self.years, "y")?;
- }
- if self.months > 0 {
- do_write(self.months, "m")?;
- }
- if self.weeks > 0 {
- do_write(self.weeks, "w")?;
- }
- if self.days > 0 {
- do_write(self.days, "d")?;
- }
- if self.hours > 0 {
- do_write(self.hours, "h")?;
- }
- if self.minutes > 0 {
- do_write(self.minutes, "min")?;
- }
- }
- if !first {
- write!(f, " ")?;
- }
- let seconds = self.seconds as f64 + (self.msec as f64 / 1000.0);
- if seconds >= 0.1 {
- if seconds >= 1.0 || !first {
- write!(f, "{:.0}s", seconds)?;
- } else {
- write!(f, "{:.1}s", seconds)?;
- }
- } else if first {
- write!(f, "<0.1s")?;
- }
- Ok(())
- }
-}
-
-pub fn verify_time_span(i: &str) -> Result<(), Error> {
- parse_time_span(i)?;
- Ok(())
-}
-
-pub fn verify_calendar_event(i: &str) -> Result<(), Error> {
- parse_calendar_event(i)?;
- Ok(())
-}
-
-pub fn compute_next_event(
- event: &CalendarEvent,
- last: i64,
- utc: bool,
-) -> Result<Option<i64>, Error> {
-
- let last = last + 1; // at least one second later
-
- let all_days = event.days.is_empty() || event.days.is_all();
-
- let mut t = TmEditor::with_epoch(last, utc)?;
-
- let mut count = 0;
-
- loop {
- // cancel after 1000 loops
- if count > 1000 {
- return Ok(None);
- } else {
- count += 1;
- }
-
- if !event.year.is_empty() {
- let year: u32 = t.year().try_into()?;
- if !DateTimeValue::list_contains(&event.year, year) {
- if let Some(n) = DateTimeValue::find_next(&event.year, year) {
- t.add_years((n - year).try_into()?)?;
- continue;
- } else {
- // if we have no valid year, we cannot find a correct timestamp
- return Ok(None);
- }
- }
- }
-
- if !event.month.is_empty() {
- let month: u32 = t.month().try_into()?;
- if !DateTimeValue::list_contains(&event.month, month) {
- if let Some(n) = DateTimeValue::find_next(&event.month, month) {
- t.add_months((n - month).try_into()?)?;
- } else {
- // if we could not find valid month, retry next year
- t.add_years(1)?;
- }
- continue;
- }
- }
-
- if !event.day.is_empty() {
- let day: u32 = t.day().try_into()?;
- if !DateTimeValue::list_contains(&event.day, day) {
- if let Some(n) = DateTimeValue::find_next(&event.day, day) {
- t.add_days((n - day).try_into()?)?;
- } else {
- // if we could not find valid mday, retry next month
- t.add_months(1)?;
- }
- continue;
- }
- }
-
- if !all_days { // match day first
- let day_num: u32 = t.day_num().try_into()?;
- let day = WeekDays::from_bits(1<<day_num).unwrap();
- if !event.days.contains(day) {
- if let Some(n) = ((day_num+1)..7)
- .find(|d| event.days.contains(WeekDays::from_bits(1<<d).unwrap()))
- {
- // try next day
- t.add_days((n - day_num).try_into()?)?;
- } else {
- // try next week
- t.add_days((7 - day_num).try_into()?)?;
- }
- continue;
- }
- }
-
- // this day
- if !event.hour.is_empty() {
- let hour = t.hour().try_into()?;
- if !DateTimeValue::list_contains(&event.hour, hour) {
- if let Some(n) = DateTimeValue::find_next(&event.hour, hour) {
- // test next hour
- t.set_time(n.try_into()?, 0, 0)?;
- } else {
- // test next day
- t.add_days(1)?;
- }
- continue;
- }
- }
-
- // this hour
- if !event.minute.is_empty() {
- let minute = t.min().try_into()?;
- if !DateTimeValue::list_contains(&event.minute, minute) {
- if let Some(n) = DateTimeValue::find_next(&event.minute, minute) {
- // test next minute
- t.set_min_sec(n.try_into()?, 0)?;
- } else {
- // test next hour
- t.set_time(t.hour() + 1, 0, 0)?;
- }
- continue;
- }
- }
-
- // this minute
- if !event.second.is_empty() {
- let second = t.sec().try_into()?;
- if !DateTimeValue::list_contains(&event.second, second) {
- if let Some(n) = DateTimeValue::find_next(&event.second, second) {
- // test next second
- t.set_sec(n.try_into()?)?;
- } else {
- // test next min
- t.set_min_sec(t.min() + 1, 0)?;
- }
- continue;
- }
- }
-
- let next = t.into_epoch()?;
- return Ok(Some(next))
- }
-}
-
-#[cfg(test)]
-mod test {
-
- use anyhow::bail;
-
- use super::*;
- use proxmox::tools::time::*;
-
- fn test_event(v: &'static str) -> Result<(), Error> {
- match parse_calendar_event(v) {
- Ok(event) => println!("CalendarEvent '{}' => {:?}", v, event),
- Err(err) => bail!("parsing '{}' failed - {}", v, err),
- }
-
- Ok(())
- }
-
- const fn make_test_time(mday: i32, hour: i32, min: i32) -> libc::time_t {
- (mday*3600*24 + hour*3600 + min*60) as libc::time_t
- }
-
- #[test]
- fn test_compute_next_event() -> Result<(), Error> {
-
- let test_value = |v: &'static str, last: i64, expect: i64| -> Result<i64, Error> {
- let event = match parse_calendar_event(v) {
- Ok(event) => event,
- Err(err) => bail!("parsing '{}' failed - {}", v, err),
- };
-
- match compute_next_event(&event, last, true) {
- Ok(Some(next)) => {
- if next == expect {
- println!("next {:?} => {}", event, next);
- } else {
- bail!("next {:?} failed\nnext: {:?}\nexpect: {:?}",
- event, gmtime(next), gmtime(expect));
- }
- }
- Ok(None) => bail!("next {:?} failed to find a timestamp", event),
- Err(err) => bail!("compute next for '{}' failed - {}", v, err),
- }
-
- Ok(expect)
- };
-
- let test_never = |v: &'static str, last: i64| -> Result<(), Error> {
- let event = match parse_calendar_event(v) {
- Ok(event) => event,
- Err(err) => bail!("parsing '{}' failed - {}", v, err),
- };
-
- match compute_next_event(&event, last, true)? {
- None => Ok(()),
- Some(next) => bail!("compute next for '{}' succeeded, but expected fail - result {}", v, next),
- }
- };
-
- const MIN: i64 = 60;
- const HOUR: i64 = 3600;
- const DAY: i64 = 3600*24;
-
- const THURSDAY_00_00: i64 = make_test_time(0, 0, 0);
- const THURSDAY_15_00: i64 = make_test_time(0, 15, 0);
-
- const JUL_31_2020: i64 = 1596153600; // Friday, 2020-07-31 00:00:00
- const DEC_31_2020: i64 = 1609372800; // Thursday, 2020-12-31 00:00:00
-
- test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
- test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
- test_value("*:*:*", THURSDAY_00_00, THURSDAY_00_00 + 1)?;
- test_value("*:3:5", THURSDAY_00_00, THURSDAY_00_00 + 3*MIN + 5)?;
-
- test_value("mon *:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY)?;
- test_value("mon 2:*", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR)?;
- test_value("mon 2:50", THURSDAY_00_00, THURSDAY_00_00 + 4*DAY + 2*HOUR + 50*MIN)?;
-
- test_value("tue", THURSDAY_00_00, THURSDAY_00_00 + 5*DAY)?;
- test_value("wed", THURSDAY_00_00, THURSDAY_00_00 + 6*DAY)?;
- test_value("thu", THURSDAY_00_00, THURSDAY_00_00 + 7*DAY)?;
- test_value("fri", THURSDAY_00_00, THURSDAY_00_00 + 1*DAY)?;
- test_value("sat", THURSDAY_00_00, THURSDAY_00_00 + 2*DAY)?;
- test_value("sun", THURSDAY_00_00, THURSDAY_00_00 + 3*DAY)?;
-
- // test multiple values for a single field
- // and test that the order does not matter
- test_value("5,10:4,8", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
- test_value("10,5:8,4", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR + 4*MIN)?;
- test_value("6,4..10:23,5/5", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
- test_value("4..10,6:5/5,23", THURSDAY_00_00, THURSDAY_00_00 + 4*HOUR + 5*MIN)?;
-
- // test month wrapping
- test_value("sat", JUL_31_2020, JUL_31_2020 + 1*DAY)?;
- test_value("sun", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
- test_value("mon", JUL_31_2020, JUL_31_2020 + 3*DAY)?;
- test_value("tue", JUL_31_2020, JUL_31_2020 + 4*DAY)?;
- test_value("wed", JUL_31_2020, JUL_31_2020 + 5*DAY)?;
- test_value("thu", JUL_31_2020, JUL_31_2020 + 6*DAY)?;
- test_value("fri", JUL_31_2020, JUL_31_2020 + 7*DAY)?;
-
- // test year wrapping
- test_value("fri", DEC_31_2020, DEC_31_2020 + 1*DAY)?;
- test_value("sat", DEC_31_2020, DEC_31_2020 + 2*DAY)?;
- test_value("sun", DEC_31_2020, DEC_31_2020 + 3*DAY)?;
- test_value("mon", DEC_31_2020, DEC_31_2020 + 4*DAY)?;
- test_value("tue", DEC_31_2020, DEC_31_2020 + 5*DAY)?;
- test_value("wed", DEC_31_2020, DEC_31_2020 + 6*DAY)?;
- test_value("thu", DEC_31_2020, DEC_31_2020 + 7*DAY)?;
-
- test_value("daily", THURSDAY_00_00, THURSDAY_00_00 + DAY)?;
- test_value("daily", THURSDAY_00_00+1, THURSDAY_00_00 + DAY)?;
-
- let n = test_value("5/2:0", THURSDAY_00_00, THURSDAY_00_00 + 5*HOUR)?;
- let n = test_value("5/2:0", n, THURSDAY_00_00 + 7*HOUR)?;
- let n = test_value("5/2:0", n, THURSDAY_00_00 + 9*HOUR)?;
- test_value("5/2:0", n, THURSDAY_00_00 + 11*HOUR)?;
-
- let mut n = test_value("*:*", THURSDAY_00_00, THURSDAY_00_00 + MIN)?;
- for i in 2..100 {
- n = test_value("*:*", n, THURSDAY_00_00 + i*MIN)?;
- }
-
- let mut n = test_value("*:0", THURSDAY_00_00, THURSDAY_00_00 + HOUR)?;
- for i in 2..100 {
- n = test_value("*:0", n, THURSDAY_00_00 + i*HOUR)?;
- }
-
- let mut n = test_value("1:0", THURSDAY_15_00, THURSDAY_00_00 + DAY + HOUR)?;
- for i in 2..100 {
- n = test_value("1:0", n, THURSDAY_00_00 + i*DAY + HOUR)?;
- }
-
- // test date functionality
-
- test_value("2020-07-31", 0, JUL_31_2020)?;
- test_value("02-28", 0, (31+27)*DAY)?;
- test_value("02-29", 0, 2*365*DAY + (31+28)*DAY)?; // 1972-02-29
- test_value("1965/5-01-01", -1, THURSDAY_00_00)?;
- test_value("2020-7..9-2/2", JUL_31_2020, JUL_31_2020 + 2*DAY)?;
- test_value("2020,2021-12-31", JUL_31_2020, DEC_31_2020)?;
-
- test_value("monthly", 0, 31*DAY)?;
- test_value("quarterly", 0, (31+28+31)*DAY)?;
- test_value("semiannually", 0, (31+28+31+30+31+30)*DAY)?;
- test_value("yearly", 0, (365)*DAY)?;
-
- test_never("2021-02-29", 0)?;
- test_never("02-30", 0)?;
-
- Ok(())
- }
-
- #[test]
- fn test_calendar_event_weekday() -> Result<(), Error> {
- test_event("mon,wed..fri")?;
- test_event("fri..mon")?;
-
- test_event("mon")?;
- test_event("MON")?;
- test_event("monDay")?;
- test_event("tue")?;
- test_event("Tuesday")?;
- test_event("wed")?;
- test_event("wednesday")?;
- test_event("thu")?;
- test_event("thursday")?;
- test_event("fri")?;
- test_event("friday")?;
- test_event("sat")?;
- test_event("saturday")?;
- test_event("sun")?;
- test_event("sunday")?;
-
- test_event("mon..fri")?;
- test_event("mon,tue,fri")?;
- test_event("mon,tue..wednesday,fri..sat")?;
-
- Ok(())
- }
-
- #[test]
- fn test_time_span_parser() -> Result<(), Error> {
-
- let test_value = |ts_str: &str, expect: f64| -> Result<(), Error> {
- let ts = parse_time_span(ts_str)?;
- assert_eq!(f64::from(ts), expect, "{}", ts_str);
- Ok(())
- };
-
- test_value("2", 2.0)?;
- test_value("2s", 2.0)?;
- test_value("2sec", 2.0)?;
- test_value("2second", 2.0)?;
- test_value("2seconds", 2.0)?;
-
- test_value(" 2s 2 s 2", 6.0)?;
-
- test_value("1msec 1ms", 0.002)?;
- test_value("1usec 1us 1µs", 0.000_003)?;
- test_value("1nsec 1ns", 0.000_000_002)?;
- test_value("1minutes 1minute 1min 1m", 4.0*60.0)?;
- test_value("1hours 1hour 1hr 1h", 4.0*3600.0)?;
- test_value("1days 1day 1d", 3.0*86400.0)?;
- test_value("1weeks 1 week 1w", 3.0*86400.0*7.0)?;
- test_value("1months 1month 1M", 3.0*86400.0*30.44)?;
- test_value("1years 1year 1y", 3.0*86400.0*365.25)?;
-
- test_value("2h", 7200.0)?;
- test_value(" 2 h", 7200.0)?;
- test_value("2hours", 7200.0)?;
- test_value("48hr", 48.0*3600.0)?;
- test_value("1y 12month", 365.25*24.0*3600.0 + 12.0*30.44*24.0*3600.0)?;
- test_value("55s500ms", 55.5)?;
- test_value("300ms20s 5day", 5.0*24.0*3600.0 + 20.0 + 0.3)?;
-
- Ok(())
- }
-}