systemd_duration/
parser.rs

1use nom::{
2    branch::alt,
3    bytes::complete::tag,
4    character::complete::{char, digit0, digit1, multispace0, one_of},
5    combinator::{all_consuming, complete, cut, map, opt, recognize},
6    error::{ErrorKind::TooLarge, ParseError},
7    multi::many1,
8    sequence::{delimited, tuple},
9    Err::Failure,
10    Finish, IResult,
11};
12
13use crate::{
14    duration::{Container, Duration},
15    error,
16};
17
18// Dimensionless unit constants
19#[derive(Copy, Clone, Debug)]
20enum DurationUnit {
21    Year,
22    Month,
23    Week,
24    Day,
25    Hour,
26    Minute,
27    Second,
28    Millisecond,
29    Microsecond,
30    Nanosecond,
31}
32
33// NOTE: we don't accept full float syntax. Systemd doesn't, so this isn't a problem.
34fn float(input: &str) -> IResult<&str, f64> {
35    map(
36        recognize(tuple((
37            opt(one_of("+-")),
38            opt(tuple((digit0, char('.')))),
39            digit1,
40        ))),
41        |s: &str| s.parse::<f64>().unwrap(),
42    )(input)
43}
44
45fn timespan_word(input: &str) -> IResult<&str, &str> {
46    // XXX - not fantastic but don't have a better way right now
47    recognize(many1(one_of("Macdehiklmnorstuwyµ")))(input)
48}
49
50// This is used to get the longest possible match for a string
51fn all_consuming_tag<'a, E: ParseError<&'a str>>(
52    t: &'a str,
53) -> impl FnMut(&'a str) -> IResult<&'a str, &'a str, E>
54where
55{
56    all_consuming(tag(t))
57}
58
59fn timespan_period_years(input: &str) -> IResult<&str, DurationUnit> {
60    map(
61        alt((
62            all_consuming_tag("years"),
63            all_consuming_tag("year"),
64            all_consuming_tag("yrs"),
65            all_consuming_tag("yr"),
66            all_consuming_tag("y"),
67        )),
68        |_| DurationUnit::Year,
69    )(input)
70}
71
72fn timespan_period_months(input: &str) -> IResult<&str, DurationUnit> {
73    map(
74        alt((
75            all_consuming_tag("months"),
76            all_consuming_tag("month"),
77            all_consuming_tag("mos"),
78            all_consuming_tag("mo"),
79            all_consuming_tag("M"),
80        )),
81        |_| DurationUnit::Month,
82    )(input)
83}
84
85fn timespan_period_weeks(input: &str) -> IResult<&str, DurationUnit> {
86    map(
87        alt((
88            all_consuming_tag("weeks"),
89            all_consuming_tag("week"),
90            all_consuming_tag("wks"),
91            all_consuming_tag("wk"),
92            all_consuming_tag("w"),
93        )),
94        |_| DurationUnit::Week,
95    )(input)
96}
97
98fn timespan_period_days(input: &str) -> IResult<&str, DurationUnit> {
99    map(
100        alt((
101            all_consuming_tag("days"),
102            all_consuming_tag("day"),
103            all_consuming_tag("d"),
104        )),
105        |_| DurationUnit::Day,
106    )(input)
107}
108
109fn timespan_period_hours(input: &str) -> IResult<&str, DurationUnit> {
110    map(
111        alt((
112            all_consuming_tag("hours"),
113            all_consuming_tag("hour"),
114            all_consuming_tag("hrs"),
115            all_consuming_tag("hr"),
116            all_consuming_tag("h"),
117        )),
118        |_| DurationUnit::Hour,
119    )(input)
120}
121
122fn timespan_period_minutes(input: &str) -> IResult<&str, DurationUnit> {
123    map(
124        alt((
125            all_consuming_tag("minutes"),
126            all_consuming_tag("minute"),
127            all_consuming_tag("mins"),
128            all_consuming_tag("min"),
129            all_consuming_tag("m"),
130        )),
131        |_| DurationUnit::Minute,
132    )(input)
133}
134
135fn timespan_period_seconds(input: &str) -> IResult<&str, DurationUnit> {
136    map(
137        alt((
138            all_consuming_tag("seconds"),
139            all_consuming_tag("second"),
140            all_consuming_tag("secs"),
141            all_consuming_tag("sec"),
142            all_consuming_tag("s"),
143        )),
144        |_| DurationUnit::Second,
145    )(input)
146}
147
148fn timespan_period_milliseconds(input: &str) -> IResult<&str, DurationUnit> {
149    map(
150        alt((
151            all_consuming_tag("milliseconds"),
152            all_consuming_tag("millisecond"),
153            all_consuming_tag("msecs"),
154            all_consuming_tag("msec"),
155            all_consuming_tag("ms"),
156        )),
157        |_| DurationUnit::Millisecond,
158    )(input)
159}
160
161fn timespan_period_microseconds(input: &str) -> IResult<&str, DurationUnit> {
162    map(
163        alt((
164            all_consuming_tag("microseconds"),
165            all_consuming_tag("microsecond"),
166            all_consuming_tag("µsecs"),
167            all_consuming_tag("µsec"),
168            all_consuming_tag("µs"),
169            all_consuming_tag("µ"),
170            all_consuming_tag("usecs"),
171            all_consuming_tag("usec"),
172            all_consuming_tag("us"),
173        )),
174        |_| DurationUnit::Microsecond,
175    )(input)
176}
177
178fn timespan_period_nanoseconds(input: &str) -> IResult<&str, DurationUnit> {
179    map(
180        alt((
181            all_consuming_tag("nanoseconds"),
182            all_consuming_tag("nanosecond"),
183            all_consuming_tag("nsecs"),
184            all_consuming_tag("nsec"),
185            all_consuming_tag("ns"),
186        )),
187        |_| DurationUnit::Nanosecond,
188    )(input)
189}
190
191// Match a timespan period, consisting of an entire word
192// If the string isn't consumed, this fails.
193fn timespan_period(input: &str) -> IResult<&str, DurationUnit> {
194    let (input, unit) = timespan_word(input)?;
195    let (_, result) = all_consuming(alt((
196        timespan_period_years,
197        timespan_period_months,
198        timespan_period_weeks,
199        timespan_period_days,
200        timespan_period_hours,
201        timespan_period_minutes,
202        timespan_period_seconds,
203        timespan_period_milliseconds,
204        timespan_period_microseconds,
205        timespan_period_nanoseconds,
206    )))(unit)?;
207
208    Ok((input, result))
209}
210
211// Returns a fragment of the duration
212#[inline(never)]
213fn duration_fragment(input: &str) -> IResult<&str, Duration> {
214    let (input, count) = delimited(multispace0, float, multispace0)(input)?;
215    let (input, unit) = timespan_period(input)?;
216    let val = match unit {
217        DurationUnit::Year => Duration::Year(count),
218        DurationUnit::Month => Duration::Month(count),
219        DurationUnit::Week => Duration::Week(count),
220        DurationUnit::Day => Duration::Day(count),
221        DurationUnit::Hour => Duration::Hour(count),
222        DurationUnit::Minute => Duration::Minute(count),
223        DurationUnit::Second => Duration::Second(count),
224        DurationUnit::Millisecond => Duration::Millisecond(count),
225        DurationUnit::Microsecond => Duration::Microsecond(count),
226        DurationUnit::Nanosecond => {
227            // All numbers are specified as floats, and a 52-bit mantissa is more than enough for
228            // most nanosecond values, so this is fine.
229            #[allow(clippy::cast_precision_loss)]
230            if count < i64::MIN as f64 || count > i64::MAX as f64 {
231                return Err(Failure(ParseError::from_error_kind(input, TooLarge)));
232            }
233            #[allow(clippy::cast_possible_truncation)]
234            Duration::Nanosecond(count as i64)
235        }
236    };
237
238    Ok((input, val))
239}
240
241// If nothing else is input, just interpret it as seconds.
242fn raw_seconds(input: &str) -> IResult<&str, Duration> {
243    let (input, seconds) = all_consuming(delimited(multispace0, float, multispace0))(input)?;
244    Ok((input, Duration::Second(seconds)))
245}
246
247fn full_duration(input: &str) -> IResult<&str, Vec<Duration>> {
248    all_consuming(many1(duration_fragment))(input)
249}
250
251// Parse a duration
252fn duration(input: &str) -> IResult<&str, Container> {
253    complete(cut(alt((
254        map(raw_seconds, |v| Container::new(vec![v])),
255        map(full_duration, Container::new),
256    ))))(input)
257}
258
259macro_rules! impl_parse {
260    ($modname:ident, $typename:ident) => {
261        impl_parse!($modname, $typename, ::$modname::$typename);
262    };
263    ($modname:ident, $typename:ident, $type:ty) => {
264        #[doc = concat!("Parsing systemd-style durations into structs used by [`", stringify!($typename), "`][", stringify!($type), "]")]
265        pub mod $modname {
266            use super::*;
267
268            #[doc = concat!("Parse a duration string into a [`", stringify!($typename), "`][", stringify!($type), "]")]
269            pub fn parse(input: &str) -> Result<$type, error::Error> {
270                let dur = duration(input).map_err(|e| e.to_owned()).finish()?;
271                let ret = dur.1.try_into()?;
272                Ok(ret)
273            }
274        }
275    };
276}
277
278impl_parse!(stdtime, Duration, std::time::Duration);
279
280#[cfg(feature = "with-chrono")]
281impl_parse!(chrono, TimeDelta);
282
283#[cfg(feature = "with-time")]
284impl_parse!(time, Duration);