]> git.proxmox.com Git - proxmox-backup.git/commitdiff
add pbs-systemd: move string and unit handling there
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 7 Jul 2021 08:10:33 +0000 (10:10 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Wed, 7 Jul 2021 09:34:56 +0000 (11:34 +0200)
the systemd config/unit parsing stays in pbs for now since
that's not usually required and uses our section config
parser

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
Cargo.toml
Makefile
pbs-systemd/Cargo.toml [new file with mode: 0644]
pbs-systemd/src/lib.rs [new file with mode: 0644]
pbs-systemd/src/parse_time.rs [new file with mode: 0644]
pbs-systemd/src/time.rs [new file with mode: 0644]
pbs-systemd/src/unit.rs [new file with mode: 0644]
pbs-tools/Cargo.toml
src/tools/systemd/mod.rs
src/tools/systemd/parse_time.rs [deleted file]
src/tools/systemd/time.rs [deleted file]

index a9a0d34d82d169bc53189b07da7e991f8a38a1f9..490c17a64a5cf985271c75728402a55645ba8d0b 100644 (file)
@@ -24,6 +24,7 @@ members = [
     "pbs-buildcfg",
     "pbs-datastore",
     "pbs-runtime",
+    "pbs-systemd",
     "pbs-tools",
 ]
 
@@ -98,6 +99,7 @@ pbs-api-types = { path = "pbs-api-types" }
 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]
index d513af07e0fdb2895e17a3087e02508021d503f3..297f0819c8445ab8edd85f6cb9d53d9377ef4a77 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -35,6 +35,7 @@ SUBCRATES := \
        pbs-buildcfg \
        pbs-datastore \
        pbs-runtime \
+       pbs-systemd \
        pbs-tools
 
 ifeq ($(BUILD_MODE), release)
diff --git a/pbs-systemd/Cargo.toml b/pbs-systemd/Cargo.toml
new file mode 100644 (file)
index 0000000..a95aba2
--- /dev/null
@@ -0,0 +1,16 @@
+[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" }
diff --git a/pbs-systemd/src/lib.rs b/pbs-systemd/src/lib.rs
new file mode 100644 (file)
index 0000000..b4ab4b7
--- /dev/null
@@ -0,0 +1,5 @@
+pub mod time;
+
+mod parse_time;
+mod unit;
+pub use unit::*;
diff --git a/pbs-systemd/src/parse_time.rs b/pbs-systemd/src/parse_time.rs
new file mode 100644 (file)
index 0000000..05ac567
--- /dev/null
@@ -0,0 +1,425 @@
+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))
+}
diff --git a/pbs-systemd/src/time.rs b/pbs-systemd/src/time.rs
new file mode 100644 (file)
index 0000000..cbcb23d
--- /dev/null
@@ -0,0 +1,582 @@
+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(())
+    }
+}
diff --git a/pbs-systemd/src/unit.rs b/pbs-systemd/src/unit.rs
new file mode 100644 (file)
index 0000000..811493f
--- /dev/null
@@ -0,0 +1,174 @@
+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(())
+}
index 8087aed56c0ec49d52e90a5710f3a6a917599df4..a041da09efd92b6ef48c5c77743aa023d30b77cd 100644 (file)
@@ -10,6 +10,7 @@ description = "common tools used throughout pbs"
 anyhow = "1.0"
 libc = "0.2"
 nix = "0.19.1"
+nom = "5.1"
 regex = "1.2"
 serde = "1.0"
 serde_json = "1.0"
index c17607819a66cd33bf96906429c3c9900af43661..d0ae2b43164708e91ebeea18a41ba6115fdf6b6b 100644 (file)
@@ -1,172 +1,7 @@
-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;
diff --git a/src/tools/systemd/parse_time.rs b/src/tools/systemd/parse_time.rs
deleted file mode 100644 (file)
index c0f58b0..0000000
+++ /dev/null
@@ -1,425 +0,0 @@
-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))
-}
diff --git a/src/tools/systemd/time.rs b/src/tools/systemd/time.rs
deleted file mode 100644 (file)
index cbcb23d..0000000
+++ /dev/null
@@ -1,582 +0,0 @@
-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(())
-    }
-}