1 use std::ops::RangeInclusive;
2 
3 use crate::parser::error::CustomError;
4 use crate::parser::prelude::*;
5 use crate::parser::trivia::from_utf8_unchecked;
6 
7 use toml_datetime::*;
8 use winnow::combinator::alt;
9 use winnow::combinator::cut_err;
10 use winnow::combinator::opt;
11 use winnow::combinator::preceded;
12 use winnow::stream::Stream as _;
13 use winnow::token::one_of;
14 use winnow::token::take_while;
15 use winnow::trace::trace;
16 
17 // ;; Date and Time (as defined in RFC 3339)
18 
19 // date-time = offset-date-time / local-date-time / local-date / local-time
20 // offset-date-time = full-date time-delim full-time
21 // local-date-time = full-date time-delim partial-time
22 // local-date = full-date
23 // local-time = partial-time
24 // full-time = partial-time time-offset
date_time(input: &mut Input<'_>) -> PResult<Datetime>25 pub(crate) fn date_time(input: &mut Input<'_>) -> PResult<Datetime> {
26     trace(
27         "date-time",
28         alt((
29             (full_date, opt((time_delim, partial_time, opt(time_offset))))
30                 .map(|(date, opt)| {
31                     match opt {
32                         // Offset Date-Time
33                         Some((_, time, offset)) => Datetime {
34                             date: Some(date),
35                             time: Some(time),
36                             offset,
37                         },
38                         // Local Date
39                         None => Datetime {
40                             date: Some(date),
41                             time: None,
42                             offset: None,
43                         },
44                     }
45                 })
46                 .context(StrContext::Label("date-time")),
47             partial_time
48                 .map(|t| t.into())
49                 .context(StrContext::Label("time")),
50         )),
51     )
52     .parse_next(input)
53 }
54 
55 // full-date      = date-fullyear "-" date-month "-" date-mday
full_date(input: &mut Input<'_>) -> PResult<Date>56 pub(crate) fn full_date(input: &mut Input<'_>) -> PResult<Date> {
57     trace("full-date", full_date_).parse_next(input)
58 }
59 
full_date_(input: &mut Input<'_>) -> PResult<Date>60 fn full_date_(input: &mut Input<'_>) -> PResult<Date> {
61     let year = date_fullyear.parse_next(input)?;
62     let _ = b'-'.parse_next(input)?;
63     let month = cut_err(date_month).parse_next(input)?;
64     let _ = cut_err(b'-').parse_next(input)?;
65     let day_start = input.checkpoint();
66     let day = cut_err(date_mday).parse_next(input)?;
67 
68     let is_leap_year = (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
69     let max_days_in_month = match month {
70         2 if is_leap_year => 29,
71         2 => 28,
72         4 | 6 | 9 | 11 => 30,
73         _ => 31,
74     };
75     if max_days_in_month < day {
76         input.reset(day_start);
77         return Err(winnow::error::ErrMode::from_external_error(
78             input,
79             winnow::error::ErrorKind::Verify,
80             CustomError::OutOfRange,
81         )
82         .cut());
83     }
84 
85     Ok(Date { year, month, day })
86 }
87 
88 // partial-time   = time-hour ":" time-minute ":" time-second [time-secfrac]
partial_time(input: &mut Input<'_>) -> PResult<Time>89 pub(crate) fn partial_time(input: &mut Input<'_>) -> PResult<Time> {
90     trace(
91         "partial-time",
92         (
93             time_hour,
94             b':',
95             cut_err((time_minute, b':', time_second, opt(time_secfrac))),
96         )
97             .map(|(hour, _, (minute, _, second, nanosecond))| Time {
98                 hour,
99                 minute,
100                 second,
101                 nanosecond: nanosecond.unwrap_or_default(),
102             }),
103     )
104     .parse_next(input)
105 }
106 
107 // time-offset    = "Z" / time-numoffset
108 // time-numoffset = ( "+" / "-" ) time-hour ":" time-minute
time_offset(input: &mut Input<'_>) -> PResult<Offset>109 pub(crate) fn time_offset(input: &mut Input<'_>) -> PResult<Offset> {
110     trace(
111         "time-offset",
112         alt((
113             one_of((b'Z', b'z')).value(Offset::Z),
114             (
115                 one_of((b'+', b'-')),
116                 cut_err((time_hour, b':', time_minute)),
117             )
118                 .map(|(sign, (hours, _, minutes))| {
119                     let sign = match sign {
120                         b'+' => 1,
121                         b'-' => -1,
122                         _ => unreachable!("Parser prevents this"),
123                     };
124                     sign * (hours as i16 * 60 + minutes as i16)
125                 })
126                 .verify(|minutes| ((-24 * 60)..=(24 * 60)).contains(minutes))
127                 .map(|minutes| Offset::Custom { minutes }),
128         ))
129         .context(StrContext::Label("time offset")),
130     )
131     .parse_next(input)
132 }
133 
134 // date-fullyear  = 4DIGIT
date_fullyear(input: &mut Input<'_>) -> PResult<u16>135 pub(crate) fn date_fullyear(input: &mut Input<'_>) -> PResult<u16> {
136     unsigned_digits::<4, 4>
137         .map(|s: &str| s.parse::<u16>().expect("4DIGIT should match u8"))
138         .parse_next(input)
139 }
140 
141 // date-month     = 2DIGIT  ; 01-12
date_month(input: &mut Input<'_>) -> PResult<u8>142 pub(crate) fn date_month(input: &mut Input<'_>) -> PResult<u8> {
143     unsigned_digits::<2, 2>
144         .try_map(|s: &str| {
145             let d = s.parse::<u8>().expect("2DIGIT should match u8");
146             if (1..=12).contains(&d) {
147                 Ok(d)
148             } else {
149                 Err(CustomError::OutOfRange)
150             }
151         })
152         .parse_next(input)
153 }
154 
155 // date-mday      = 2DIGIT  ; 01-28, 01-29, 01-30, 01-31 based on month/year
date_mday(input: &mut Input<'_>) -> PResult<u8>156 pub(crate) fn date_mday(input: &mut Input<'_>) -> PResult<u8> {
157     unsigned_digits::<2, 2>
158         .try_map(|s: &str| {
159             let d = s.parse::<u8>().expect("2DIGIT should match u8");
160             if (1..=31).contains(&d) {
161                 Ok(d)
162             } else {
163                 Err(CustomError::OutOfRange)
164             }
165         })
166         .parse_next(input)
167 }
168 
169 // time-delim     = "T" / %x20 ; T, t, or space
time_delim(input: &mut Input<'_>) -> PResult<u8>170 pub(crate) fn time_delim(input: &mut Input<'_>) -> PResult<u8> {
171     one_of(TIME_DELIM).parse_next(input)
172 }
173 
174 const TIME_DELIM: (u8, u8, u8) = (b'T', b't', b' ');
175 
176 // time-hour      = 2DIGIT  ; 00-23
time_hour(input: &mut Input<'_>) -> PResult<u8>177 pub(crate) fn time_hour(input: &mut Input<'_>) -> PResult<u8> {
178     unsigned_digits::<2, 2>
179         .try_map(|s: &str| {
180             let d = s.parse::<u8>().expect("2DIGIT should match u8");
181             if (0..=23).contains(&d) {
182                 Ok(d)
183             } else {
184                 Err(CustomError::OutOfRange)
185             }
186         })
187         .parse_next(input)
188 }
189 
190 // time-minute    = 2DIGIT  ; 00-59
time_minute(input: &mut Input<'_>) -> PResult<u8>191 pub(crate) fn time_minute(input: &mut Input<'_>) -> PResult<u8> {
192     unsigned_digits::<2, 2>
193         .try_map(|s: &str| {
194             let d = s.parse::<u8>().expect("2DIGIT should match u8");
195             if (0..=59).contains(&d) {
196                 Ok(d)
197             } else {
198                 Err(CustomError::OutOfRange)
199             }
200         })
201         .parse_next(input)
202 }
203 
204 // time-second    = 2DIGIT  ; 00-58, 00-59, 00-60 based on leap second rules
time_second(input: &mut Input<'_>) -> PResult<u8>205 pub(crate) fn time_second(input: &mut Input<'_>) -> PResult<u8> {
206     unsigned_digits::<2, 2>
207         .try_map(|s: &str| {
208             let d = s.parse::<u8>().expect("2DIGIT should match u8");
209             if (0..=60).contains(&d) {
210                 Ok(d)
211             } else {
212                 Err(CustomError::OutOfRange)
213             }
214         })
215         .parse_next(input)
216 }
217 
218 // time-secfrac   = "." 1*DIGIT
time_secfrac(input: &mut Input<'_>) -> PResult<u32>219 pub(crate) fn time_secfrac(input: &mut Input<'_>) -> PResult<u32> {
220     static SCALE: [u32; 10] = [
221         0,
222         100_000_000,
223         10_000_000,
224         1_000_000,
225         100_000,
226         10_000,
227         1_000,
228         100,
229         10,
230         1,
231     ];
232     const INF: usize = usize::MAX;
233     preceded(b'.', unsigned_digits::<1, INF>)
234         .try_map(|mut repr: &str| -> Result<u32, CustomError> {
235             let max_digits = SCALE.len() - 1;
236             if max_digits < repr.len() {
237                 // Millisecond precision is required. Further precision of fractional seconds is
238                 // implementation-specific. If the value contains greater precision than the
239                 // implementation can support, the additional precision must be truncated, not rounded.
240                 repr = &repr[0..max_digits];
241             }
242 
243             let v = repr.parse::<u32>().map_err(|_| CustomError::OutOfRange)?;
244             let num_digits = repr.len();
245 
246             // scale the number accordingly.
247             let scale = SCALE.get(num_digits).ok_or(CustomError::OutOfRange)?;
248             let v = v.checked_mul(*scale).ok_or(CustomError::OutOfRange)?;
249             Ok(v)
250         })
251         .parse_next(input)
252 }
253 
unsigned_digits<'i, const MIN: usize, const MAX: usize>( input: &mut Input<'i>, ) -> PResult<&'i str>254 pub(crate) fn unsigned_digits<'i, const MIN: usize, const MAX: usize>(
255     input: &mut Input<'i>,
256 ) -> PResult<&'i str> {
257     take_while(MIN..=MAX, DIGIT)
258         .map(|b: &[u8]| unsafe { from_utf8_unchecked(b, "`is_ascii_digit` filters out on-ASCII") })
259         .parse_next(input)
260 }
261 
262 // DIGIT = %x30-39 ; 0-9
263 const DIGIT: RangeInclusive<u8> = b'0'..=b'9';
264 
265 #[cfg(test)]
266 #[cfg(feature = "parse")]
267 #[cfg(feature = "display")]
268 mod test {
269     use super::*;
270 
271     #[test]
offset_date_time()272     fn offset_date_time() {
273         let inputs = [
274             (
275                 "1979-05-27T07:32:00Z",
276                 Datetime {
277                     date: Some(Date {
278                         year: 1979,
279                         month: 5,
280                         day: 27,
281                     }),
282                     time: Some(Time {
283                         hour: 7,
284                         minute: 32,
285                         second: 0,
286                         nanosecond: 0,
287                     }),
288                     offset: Some(Offset::Z),
289                 },
290             ),
291             (
292                 "1979-05-27T00:32:00-07:00",
293                 Datetime {
294                     date: Some(Date {
295                         year: 1979,
296                         month: 5,
297                         day: 27,
298                     }),
299                     time: Some(Time {
300                         hour: 0,
301                         minute: 32,
302                         second: 0,
303                         nanosecond: 0,
304                     }),
305                     offset: Some(Offset::Custom { minutes: -7 * 60 }),
306                 },
307             ),
308             (
309                 "1979-05-27T00:32:00-00:36",
310                 Datetime {
311                     date: Some(Date {
312                         year: 1979,
313                         month: 5,
314                         day: 27,
315                     }),
316                     time: Some(Time {
317                         hour: 0,
318                         minute: 32,
319                         second: 0,
320                         nanosecond: 0,
321                     }),
322                     offset: Some(Offset::Custom { minutes: -36 }),
323                 },
324             ),
325             (
326                 "1979-05-27T00:32:00.999999",
327                 Datetime {
328                     date: Some(Date {
329                         year: 1979,
330                         month: 5,
331                         day: 27,
332                     }),
333                     time: Some(Time {
334                         hour: 0,
335                         minute: 32,
336                         second: 0,
337                         nanosecond: 999999000,
338                     }),
339                     offset: None,
340                 },
341             ),
342         ];
343         for (input, expected) in inputs {
344             dbg!(input);
345             let actual = date_time.parse(new_input(input)).unwrap();
346             assert_eq!(expected, actual);
347         }
348     }
349 
350     #[test]
local_date_time()351     fn local_date_time() {
352         let inputs = [
353             (
354                 "1979-05-27T07:32:00",
355                 Datetime {
356                     date: Some(Date {
357                         year: 1979,
358                         month: 5,
359                         day: 27,
360                     }),
361                     time: Some(Time {
362                         hour: 7,
363                         minute: 32,
364                         second: 0,
365                         nanosecond: 0,
366                     }),
367                     offset: None,
368                 },
369             ),
370             (
371                 "1979-05-27T00:32:00.999999",
372                 Datetime {
373                     date: Some(Date {
374                         year: 1979,
375                         month: 5,
376                         day: 27,
377                     }),
378                     time: Some(Time {
379                         hour: 0,
380                         minute: 32,
381                         second: 0,
382                         nanosecond: 999999000,
383                     }),
384                     offset: None,
385                 },
386             ),
387         ];
388         for (input, expected) in inputs {
389             dbg!(input);
390             let actual = date_time.parse(new_input(input)).unwrap();
391             assert_eq!(expected, actual);
392         }
393     }
394 
395     #[test]
local_date()396     fn local_date() {
397         let inputs = [
398             (
399                 "1979-05-27",
400                 Datetime {
401                     date: Some(Date {
402                         year: 1979,
403                         month: 5,
404                         day: 27,
405                     }),
406                     time: None,
407                     offset: None,
408                 },
409             ),
410             (
411                 "2017-07-20",
412                 Datetime {
413                     date: Some(Date {
414                         year: 2017,
415                         month: 7,
416                         day: 20,
417                     }),
418                     time: None,
419                     offset: None,
420                 },
421             ),
422         ];
423         for (input, expected) in inputs {
424             dbg!(input);
425             let actual = date_time.parse(new_input(input)).unwrap();
426             assert_eq!(expected, actual);
427         }
428     }
429 
430     #[test]
local_time()431     fn local_time() {
432         let inputs = [
433             (
434                 "07:32:00",
435                 Datetime {
436                     date: None,
437                     time: Some(Time {
438                         hour: 7,
439                         minute: 32,
440                         second: 0,
441                         nanosecond: 0,
442                     }),
443                     offset: None,
444                 },
445             ),
446             (
447                 "00:32:00.999999",
448                 Datetime {
449                     date: None,
450                     time: Some(Time {
451                         hour: 0,
452                         minute: 32,
453                         second: 0,
454                         nanosecond: 999999000,
455                     }),
456                     offset: None,
457                 },
458             ),
459         ];
460         for (input, expected) in inputs {
461             dbg!(input);
462             let actual = date_time.parse(new_input(input)).unwrap();
463             assert_eq!(expected, actual);
464         }
465     }
466 
467     #[test]
time_fraction_truncated()468     fn time_fraction_truncated() {
469         let input = "1987-07-05T17:45:00.123456789012345Z";
470         date_time.parse(new_input(input)).unwrap();
471     }
472 }
473