1use chrono::{DateTime, FixedOffset, NaiveDate, NaiveTime};
2use nom::{
3 branch::alt,
4 bytes::complete::{is_a, tag, tag_no_case, take_while_m_n},
5 character,
6 character::complete::{alphanumeric1, digit0},
7 combinator::{map, opt, value},
8 sequence::{delimited, preceded, terminated, tuple},
9 IResult,
10};
11
12use crate::text::whitespace::{cfws, fws};
13const MIN: i32 = 60;
16const HOUR: i32 = 60 * MIN;
17
18pub fn section(input: &[u8]) -> IResult<&[u8], Option<DateTime<FixedOffset>>> {
47 map(
48 terminated(
49 alt((
50 tuple((
51 opt(terminated(strict_day_of_week, tag(","))),
52 strict_date,
53 strict_time_of_day,
54 strict_zone,
55 )),
56 tuple((
57 opt(terminated(obs_day_of_week, tag(","))),
58 obs_date,
59 obs_time_of_day,
60 alt((strict_zone, obs_zone)),
61 )),
62 )),
63 opt(cfws),
64 ),
65 |res| match res {
66 (_, Some(date), Some(time), Some(tz)) => {
67 date.and_time(time).and_local_timezone(tz).earliest()
68 }
69 _ => None,
70 },
71 )(input)
72}
73
74fn strict_day_of_week(input: &[u8]) -> IResult<&[u8], &[u8]> {
76 preceded(opt(fws), day_name)(input)
77}
78
79fn obs_day_of_week(input: &[u8]) -> IResult<&[u8], &[u8]> {
81 delimited(opt(cfws), day_name, opt(cfws))(input)
82}
83
84fn day_name(input: &[u8]) -> IResult<&[u8], &[u8]> {
87 alt((
88 tag_no_case(b"Mon"),
89 tag_no_case(b"Tue"),
90 tag_no_case(b"Wed"),
91 tag_no_case(b"Thu"),
92 tag_no_case(b"Fri"),
93 tag_no_case(b"Sat"),
94 tag_no_case(b"Sun"),
95 ))(input)
96}
97
98fn strict_date(input: &[u8]) -> IResult<&[u8], Option<NaiveDate>> {
100 map(tuple((strict_day, month, strict_year)), |(d, m, y)| {
101 NaiveDate::from_ymd_opt(y, m, d)
102 })(input)
103}
104
105fn obs_date(input: &[u8]) -> IResult<&[u8], Option<NaiveDate>> {
107 map(tuple((obs_day, month, obs_year)), |(d, m, y)| {
108 NaiveDate::from_ymd_opt(y, m, d)
109 })(input)
110}
111
112fn strict_day(input: &[u8]) -> IResult<&[u8], u32> {
114 delimited(opt(fws), character::complete::u32, fws)(input)
115}
116
117fn obs_day(input: &[u8]) -> IResult<&[u8], u32> {
119 delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
120}
121
122fn month(input: &[u8]) -> IResult<&[u8], u32> {
126 alt((
127 value(1, tag_no_case(b"Jan")),
128 value(2, tag_no_case(b"Feb")),
129 value(3, tag_no_case(b"Mar")),
130 value(4, tag_no_case(b"Apr")),
131 value(5, tag_no_case(b"May")),
132 value(6, tag_no_case(b"Jun")),
133 value(7, tag_no_case(b"Jul")),
134 value(8, tag_no_case(b"Aug")),
135 value(9, tag_no_case(b"Sep")),
136 value(10, tag_no_case(b"Oct")),
137 value(11, tag_no_case(b"Nov")),
138 value(12, tag_no_case(b"Dec")),
139 ))(input)
140}
141
142fn strict_year(input: &[u8]) -> IResult<&[u8], i32> {
144 delimited(
145 fws,
146 map(
147 terminated(take_while_m_n(4, 9, |c| (0x30..=0x39).contains(&c)), digit0),
148 |d: &[u8]| {
149 encoding_rs::UTF_8
150 .decode_without_bom_handling(d)
151 .0
152 .parse::<i32>()
153 .unwrap_or(0)
154 },
155 ),
156 fws,
157 )(input)
158}
159
160fn obs_year(input: &[u8]) -> IResult<&[u8], i32> {
162 map(
163 delimited(
164 opt(cfws),
165 terminated(take_while_m_n(2, 7, |c| (0x30..=0x39).contains(&c)), digit0),
166 opt(cfws),
167 ),
168 |cap: &[u8]| {
169 let year_txt = encoding_rs::UTF_8.decode_without_bom_handling(cap).0;
170 let d = year_txt.parse::<i32>().unwrap_or(0);
171 if (0..=49).contains(&d) {
172 2000 + d
173 } else if (50..=999).contains(&d) {
174 1900 + d
175 } else {
176 d
177 }
178 },
179 )(input)
180}
181
182fn strict_time_of_day(input: &[u8]) -> IResult<&[u8], Option<NaiveTime>> {
184 map(
185 tuple((
186 strict_time_digit,
187 tag(":"),
188 strict_time_digit,
189 opt(preceded(tag(":"), strict_time_digit)),
190 )),
191 |(hour, _, minute, maybe_sec)| {
192 NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0))
193 },
194 )(input)
195}
196
197fn obs_time_of_day(input: &[u8]) -> IResult<&[u8], Option<NaiveTime>> {
199 map(
200 tuple((
201 obs_time_digit,
202 tag(":"),
203 obs_time_digit,
204 opt(preceded(tag(":"), obs_time_digit)),
205 )),
206 |(hour, _, minute, maybe_sec)| {
207 NaiveTime::from_hms_opt(hour, minute, maybe_sec.unwrap_or(0))
208 },
209 )(input)
210}
211
212fn strict_time_digit(input: &[u8]) -> IResult<&[u8], u32> {
213 character::complete::u32(input)
214}
215
216fn obs_time_digit(input: &[u8]) -> IResult<&[u8], u32> {
217 delimited(opt(cfws), character::complete::u32, opt(cfws))(input)
218}
219
220fn strict_zone(input: &[u8]) -> IResult<&[u8], Option<FixedOffset>> {
226 map(
227 tuple((
228 opt(fws),
229 is_a("+-"),
230 take_while_m_n(2, 2, |c| (0x30..=0x39).contains(&c)),
231 take_while_m_n(2, 2, |c| (0x30..=0x39).contains(&c)),
232 )),
233 |(_, op, dig_zone_hour, dig_zone_min)| {
234 let zone_hour: i32 =
235 ((dig_zone_hour[0] - 0x30) * 10 + (dig_zone_hour[1] - 0x30)) as i32 * HOUR;
236 let zone_min: i32 =
237 ((dig_zone_min[0] - 0x30) * 10 + (dig_zone_min[1] - 0x30)) as i32 * MIN;
238 match op {
239 b"+" => FixedOffset::east_opt(zone_hour + zone_min),
240 b"-" => FixedOffset::west_opt(zone_hour + zone_min),
241 _ => unreachable!(),
242 }
243 },
244 )(input)
245}
246
247fn obs_zone(input: &[u8]) -> IResult<&[u8], Option<FixedOffset>> {
264 preceded(
270 opt(fws),
271 alt((
272 value(
274 FixedOffset::west_opt(0 * HOUR),
275 alt((tag_no_case(b"UTC"), tag_no_case(b"UT"), tag_no_case(b"GMT"))),
276 ),
277 value(FixedOffset::west_opt(4 * HOUR), tag_no_case(b"EDT")),
279 value(
280 FixedOffset::west_opt(5 * HOUR),
281 alt((tag_no_case(b"EST"), tag_no_case(b"CDT"))),
282 ),
283 value(
284 FixedOffset::west_opt(6 * HOUR),
285 alt((tag_no_case(b"CST"), tag_no_case(b"MDT"))),
286 ),
287 value(
288 FixedOffset::west_opt(7 * HOUR),
289 alt((tag_no_case(b"MST"), tag_no_case(b"PDT"))),
290 ),
291 value(FixedOffset::west_opt(8 * HOUR), tag_no_case(b"PST")),
292 value(FixedOffset::west_opt(0 * HOUR), tag_no_case(b"Z")),
294 alt((
296 value(FixedOffset::east_opt(HOUR), tag_no_case(b"A")),
297 value(FixedOffset::east_opt(2 * HOUR), tag_no_case(b"B")),
298 value(FixedOffset::east_opt(3 * HOUR), tag_no_case(b"C")),
299 value(FixedOffset::east_opt(4 * HOUR), tag_no_case(b"D")),
300 value(FixedOffset::east_opt(5 * HOUR), tag_no_case(b"E")),
301 value(FixedOffset::east_opt(6 * HOUR), tag_no_case(b"F")),
302 value(FixedOffset::east_opt(7 * HOUR), tag_no_case(b"G")),
303 value(FixedOffset::east_opt(8 * HOUR), tag_no_case(b"H")),
304 value(FixedOffset::east_opt(9 * HOUR), tag_no_case(b"I")),
305 value(FixedOffset::east_opt(10 * HOUR), tag_no_case(b"K")),
306 value(FixedOffset::east_opt(11 * HOUR), tag_no_case(b"L")),
307 value(FixedOffset::east_opt(12 * HOUR), tag_no_case(b"M")),
308 )),
309 alt((
311 value(FixedOffset::west_opt(HOUR), tag_no_case(b"N")),
312 value(FixedOffset::west_opt(2 * HOUR), tag_no_case(b"O")),
313 value(FixedOffset::west_opt(3 * HOUR), tag_no_case(b"P")),
314 value(FixedOffset::west_opt(4 * HOUR), tag_no_case(b"Q")),
315 value(FixedOffset::west_opt(5 * HOUR), tag_no_case(b"R")),
316 value(FixedOffset::west_opt(6 * HOUR), tag_no_case(b"S")),
317 value(FixedOffset::west_opt(7 * HOUR), tag_no_case(b"T")),
318 value(FixedOffset::west_opt(8 * HOUR), tag_no_case(b"U")),
319 value(FixedOffset::west_opt(9 * HOUR), tag_no_case(b"V")),
320 value(FixedOffset::west_opt(10 * HOUR), tag_no_case(b"W")),
321 value(FixedOffset::west_opt(11 * HOUR), tag_no_case(b"X")),
322 value(FixedOffset::west_opt(12 * HOUR), tag_no_case(b"Y")),
323 )),
324 value(FixedOffset::west_opt(0 * HOUR), alphanumeric1),
326 )),
327 )(input)
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use chrono::TimeZone;
334
335 #[test]
336 fn test_section_rfc_strict() {
337 assert_eq!(
338 section(b"Fri, 21 Nov 1997 09:55:06 -0600"),
339 Ok((
340 &b""[..],
341 Some(
342 FixedOffset::west_opt(6 * HOUR)
343 .unwrap()
344 .with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
345 .unwrap()
346 )
347 )),
348 );
349 }
350
351 #[test]
352 fn test_section_received() {
353 assert_eq!(
354 section(b"Sun, 18 Jun 2023 15:39:08 +0200 (CEST)"),
355 Ok((
356 &b""[..],
357 Some(
358 FixedOffset::east_opt(2 * HOUR)
359 .unwrap()
360 .with_ymd_and_hms(2023, 6, 18, 15, 39, 8)
361 .unwrap()
362 )
363 )),
364 );
365 }
366
367 #[test]
368 fn test_section_rfc_ws() {
369 assert_eq!(
370 section(
371 r#"Thu,
372 13
373 Feb
374 1969
375 23:32
376 -0330 (Newfoundland Time)"#
377 .as_bytes()
378 ),
379 Ok((
380 &b""[..],
381 Some(
382 FixedOffset::west_opt(3 * HOUR + 30 * MIN)
383 .unwrap()
384 .with_ymd_and_hms(1969, 2, 13, 23, 32, 00)
385 .unwrap()
386 )
387 )),
388 );
389 }
390
391 #[test]
392 fn test_section_rfc_obs() {
393 assert_eq!(
394 section(b"21 Nov 97 09:55:06 GMT"),
395 Ok((
396 &b""[..],
397 Some(
398 FixedOffset::east_opt(0)
399 .unwrap()
400 .with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
401 .unwrap()
402 )
403 )),
404 );
405 }
406
407 #[test]
408 fn test_section_3digit_year() {
409 assert_eq!(
410 section(b"21 Nov 103 09:55:06 UT"),
411 Ok((
412 &b""[..],
413 Some(
414 FixedOffset::east_opt(0)
415 .unwrap()
416 .with_ymd_and_hms(2003, 11, 21, 9, 55, 6)
417 .unwrap()
418 )
419 )),
420 );
421 }
422
423 #[test]
424 fn test_section_rfc_obs_ws() {
425 assert_eq!(
426 section(b"Fri, 21 Nov 1997 09(comment): 55 : 06 -0600"),
427 Ok((
428 &b""[..],
429 Some(
430 FixedOffset::west_opt(6 * HOUR)
431 .unwrap()
432 .with_ymd_and_hms(1997, 11, 21, 9, 55, 6)
433 .unwrap()
434 )
435 )),
436 );
437 }
438
439 #[test]
440 fn test_section_2digit_year() {
441 assert_eq!(
442 section(b"21 Nov 23 09:55:06Z"),
443 Ok((
444 &b""[..],
445 Some(
446 FixedOffset::east_opt(0)
447 .unwrap()
448 .with_ymd_and_hms(2023, 11, 21, 9, 55, 6)
449 .unwrap()
450 )
451 )),
452 );
453 }
454
455 #[test]
456 fn test_section_military_zone_east() {
457 ["a", "B", "c", "D", "e", "F", "g", "H", "i", "K", "l", "M"]
458 .iter()
459 .enumerate()
460 .for_each(|(i, x)| {
461 assert_eq!(
462 section(format!("1 Jan 22 08:00:00 {}", x).as_bytes()),
463 Ok((
464 &b""[..],
465 Some(
466 FixedOffset::east_opt((i as i32 + 1) * HOUR)
467 .unwrap()
468 .with_ymd_and_hms(2022, 01, 01, 8, 0, 0)
469 .unwrap()
470 )
471 ))
472 );
473 });
474 }
475
476 #[test]
477 fn test_section_military_zone_west() {
478 ["N", "O", "P", "q", "r", "s", "T", "U", "V", "w", "x", "y"]
479 .iter()
480 .enumerate()
481 .for_each(|(i, x)| {
482 assert_eq!(
483 section(format!("1 Jan 22 08:00:00 {}", x).as_bytes()),
484 Ok((
485 &b""[..],
486 Some(
487 FixedOffset::west_opt((i as i32 + 1) * HOUR)
488 .unwrap()
489 .with_ymd_and_hms(2022, 01, 01, 8, 0, 0)
490 .unwrap()
491 )
492 ))
493 );
494 });
495 }
496
497 #[test]
498 fn test_section_gmt() {
499 assert_eq!(
500 section(b"21 Nov 2023 07:07:07 +0000"),
501 Ok((
502 &b""[..],
503 Some(
504 FixedOffset::east_opt(0)
505 .unwrap()
506 .with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
507 .unwrap()
508 )
509 )),
510 );
511 assert_eq!(
512 section(b"21 Nov 2023 07:07:07 -0000"),
513 Ok((
514 &b""[..],
515 Some(
516 FixedOffset::east_opt(0)
517 .unwrap()
518 .with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
519 .unwrap()
520 )
521 )),
522 );
523 assert_eq!(
524 section(b"21 Nov 2023 07:07:07 Z"),
525 Ok((
526 &b""[..],
527 Some(
528 FixedOffset::east_opt(0)
529 .unwrap()
530 .with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
531 .unwrap()
532 )
533 )),
534 );
535 assert_eq!(
536 section(b"21 Nov 2023 07:07:07 GMT"),
537 Ok((
538 &b""[..],
539 Some(
540 FixedOffset::east_opt(0)
541 .unwrap()
542 .with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
543 .unwrap()
544 )
545 )),
546 );
547 assert_eq!(
548 section(b"21 Nov 2023 07:07:07 UT"),
549 Ok((
550 &b""[..],
551 Some(
552 FixedOffset::east_opt(0)
553 .unwrap()
554 .with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
555 .unwrap()
556 )
557 )),
558 );
559 assert_eq!(
560 section(b"21 Nov 2023 07:07:07 UTC"),
561 Ok((
562 &b""[..],
563 Some(
564 FixedOffset::east_opt(0)
565 .unwrap()
566 .with_ymd_and_hms(2023, 11, 21, 7, 7, 7)
567 .unwrap()
568 )
569 )),
570 );
571 }
572
573 #[test]
574 fn test_section_usa() {
575 assert_eq!(
576 section(b"21 Nov 2023 4:4:4 CST"),
577 Ok((
578 &b""[..],
579 Some(
580 FixedOffset::west_opt(6 * HOUR)
581 .unwrap()
582 .with_ymd_and_hms(2023, 11, 21, 4, 4, 4)
583 .unwrap()
584 )
585 )),
586 );
587 }
588}