systemd_duration/
parser.rs

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