1use thiserror::Error;
4
5#[derive(Copy, Clone, Debug, Eq, PartialEq, Error)]
7#[error("invalid date string")]
8pub struct DateParseError;
9
10use super::approx::approxidate_careful_at;
11use super::compat::{self, time_t, tm};
12use super::tm::{
13 empty_tm, init_tm_unknown, is_date_known, local_tzoffset, match_string, maybeiso8601, nodate,
14 parse_timestamp_prefix, process_wall_clock_sec, skip_alpha, tm_to_time_t, TIMESTAMP_MAX,
15};
16
17struct TzName {
18 name: &'static str,
19 offset_hours: i32,
20 dst: i32,
21}
22
23const TIMEZONE_NAMES: &[TzName] = &[
24 TzName {
25 name: "IDLW",
26 offset_hours: -12,
27 dst: 0,
28 },
29 TzName {
30 name: "NT",
31 offset_hours: -11,
32 dst: 0,
33 },
34 TzName {
35 name: "CAT",
36 offset_hours: -10,
37 dst: 0,
38 },
39 TzName {
40 name: "HST",
41 offset_hours: -10,
42 dst: 0,
43 },
44 TzName {
45 name: "HDT",
46 offset_hours: -10,
47 dst: 1,
48 },
49 TzName {
50 name: "YST",
51 offset_hours: -9,
52 dst: 0,
53 },
54 TzName {
55 name: "YDT",
56 offset_hours: -9,
57 dst: 1,
58 },
59 TzName {
60 name: "PST",
61 offset_hours: -8,
62 dst: 0,
63 },
64 TzName {
65 name: "PDT",
66 offset_hours: -8,
67 dst: 1,
68 },
69 TzName {
70 name: "MST",
71 offset_hours: -7,
72 dst: 0,
73 },
74 TzName {
75 name: "MDT",
76 offset_hours: -7,
77 dst: 1,
78 },
79 TzName {
80 name: "CST",
81 offset_hours: -6,
82 dst: 0,
83 },
84 TzName {
85 name: "CDT",
86 offset_hours: -6,
87 dst: 1,
88 },
89 TzName {
90 name: "EST",
91 offset_hours: -5,
92 dst: 0,
93 },
94 TzName {
95 name: "EDT",
96 offset_hours: -5,
97 dst: 1,
98 },
99 TzName {
100 name: "AST",
101 offset_hours: -3,
102 dst: 0,
103 },
104 TzName {
105 name: "ADT",
106 offset_hours: -3,
107 dst: 1,
108 },
109 TzName {
110 name: "WAT",
111 offset_hours: -1,
112 dst: 0,
113 },
114 TzName {
115 name: "GMT",
116 offset_hours: 0,
117 dst: 0,
118 },
119 TzName {
120 name: "UTC",
121 offset_hours: 0,
122 dst: 0,
123 },
124 TzName {
125 name: "Z",
126 offset_hours: 0,
127 dst: 0,
128 },
129 TzName {
130 name: "WET",
131 offset_hours: 0,
132 dst: 0,
133 },
134 TzName {
135 name: "BST",
136 offset_hours: 0,
137 dst: 1,
138 },
139 TzName {
140 name: "CET",
141 offset_hours: 1,
142 dst: 0,
143 },
144 TzName {
145 name: "MET",
146 offset_hours: 1,
147 dst: 0,
148 },
149 TzName {
150 name: "MEWT",
151 offset_hours: 1,
152 dst: 0,
153 },
154 TzName {
155 name: "MEST",
156 offset_hours: 1,
157 dst: 1,
158 },
159 TzName {
160 name: "CEST",
161 offset_hours: 1,
162 dst: 1,
163 },
164 TzName {
165 name: "MESZ",
166 offset_hours: 1,
167 dst: 1,
168 },
169 TzName {
170 name: "FWT",
171 offset_hours: 1,
172 dst: 0,
173 },
174 TzName {
175 name: "FST",
176 offset_hours: 1,
177 dst: 1,
178 },
179 TzName {
180 name: "EET",
181 offset_hours: 2,
182 dst: 0,
183 },
184 TzName {
185 name: "EEST",
186 offset_hours: 2,
187 dst: 1,
188 },
189 TzName {
190 name: "WAST",
191 offset_hours: 7,
192 dst: 0,
193 },
194 TzName {
195 name: "WADT",
196 offset_hours: 7,
197 dst: 1,
198 },
199 TzName {
200 name: "CCT",
201 offset_hours: 8,
202 dst: 0,
203 },
204 TzName {
205 name: "JST",
206 offset_hours: 9,
207 dst: 0,
208 },
209 TzName {
210 name: "EAST",
211 offset_hours: 10,
212 dst: 0,
213 },
214 TzName {
215 name: "EADT",
216 offset_hours: 10,
217 dst: 1,
218 },
219 TzName {
220 name: "GST",
221 offset_hours: 10,
222 dst: 0,
223 },
224 TzName {
225 name: "NZT",
226 offset_hours: 12,
227 dst: 0,
228 },
229 TzName {
230 name: "NZST",
231 offset_hours: 12,
232 dst: 0,
233 },
234 TzName {
235 name: "NZDT",
236 offset_hours: 12,
237 dst: 1,
238 },
239 TzName {
240 name: "IDLE",
241 offset_hours: 12,
242 dst: 0,
243 },
244];
245
246const MONTH_NAME_08: &str = match core::str::from_utf8(&[65, 117, 103, 117, 115, 116]) {
247 Ok(name) => name,
248 Err(_) => panic!(),
249};
250
251pub(crate) const MONTH_NAMES: [&str; 12] = [
252 "January",
253 "February",
254 "March",
255 "April",
256 "May",
257 "June",
258 "July",
259 MONTH_NAME_08,
260 "September",
261 "October",
262 "November",
263 "December",
264];
265
266pub(crate) const WEEKDAY_NAMES: [&str; 7] = [
267 "Sundays",
268 "Mondays",
269 "Tuesdays",
270 "Wednesdays",
271 "Thursdays",
272 "Fridays",
273 "Saturdays",
274];
275
276pub fn date_string(date: u64, offset: i32) -> String {
278 let mut sign = '+';
279 let mut o = offset;
280 if o < 0 {
281 o = -o;
282 sign = '-';
283 }
284 format!("{} {}{:02}{:02}", date, sign, o / 60, o % 60)
285}
286
287pub fn parse_date(date: &str) -> Result<String, DateParseError> {
289 parse_date_at(date, process_wall_clock_sec())
290}
291
292pub fn parse_date_at(date: &str, now_sec: i64) -> Result<String, DateParseError> {
294 if let Ok((ts, off)) = parse_date_basic_at(date, now_sec) {
295 return Ok(date_string(ts, off));
296 }
297 let mut err = 0;
298 let ts = approxidate_careful_at(date, now_sec, Some(&mut err));
299 if err != 0 {
300 return Err(DateParseError);
301 }
302 let off = tz_hhmm_to_offset_minutes(local_tzoffset(ts));
303 Ok(date_string(ts, off))
304}
305
306fn tz_hhmm_to_offset_minutes(hhmm: i32) -> i32 {
307 let sign = if hhmm < 0 { -1 } else { 1 };
308 let abs = hhmm.unsigned_abs();
309 sign * ((abs / 100) * 60 + (abs % 100)) as i32
310}
311
312pub fn parse_date_basic(date: &str) -> Result<(u64, i32), DateParseError> {
314 parse_date_basic_at(date, process_wall_clock_sec())
315}
316
317pub fn parse_date_basic_at(date: &str, now_sec: i64) -> Result<(u64, i32), DateParseError> {
319 let bytes = date.as_bytes();
320 let mut tm = init_tm_unknown();
321 let mut offset: i32 = -1;
322 let mut tm_gmt = 0i32;
323 let mut i = 0usize;
324
325 if bytes.first() == Some(&b'@') {
326 if let Some((ts, off)) = match_object_header_date(&bytes[1..]) {
327 return Ok((ts, off));
328 }
329 }
330
331 while i < bytes.len() {
332 let c = bytes[i];
333 if c == 0 || c == b'\n' {
334 break;
335 }
336 let mut m = 0usize;
337 if c.is_ascii_alphabetic() {
338 m = match_alpha(&bytes[i..], &mut tm, &mut offset);
339 } else if c.is_ascii_digit() {
340 m = match_digit(&bytes[i..], &mut tm, &mut offset, &mut tm_gmt, now_sec);
341 } else if (c == b'-' || c == b'+') && bytes.get(i + 1).is_some_and(|x| x.is_ascii_digit()) {
342 m = match_tz(&bytes[i..], &mut offset);
343 }
344 if m == 0 {
345 m = 1;
346 }
347 i += m;
348 }
349
350 let tts = tm_to_time_t(&tm);
351 if tts < 0 {
352 return Err(DateParseError);
353 }
354 let mut ts = tts as u64;
355
356 if offset == -1 {
357 tm.tm_isdst = -1;
358 let temp_time = unsafe { compat::mktime(&mut tm) };
359 let tt = ts as i128;
360 let tloc = temp_time as i128;
361 offset = if tt > tloc {
362 ((tt - tloc) / 60) as i32
363 } else {
364 -(((tloc - tt) / 60) as i32)
365 };
366 }
367
368 if tm_gmt == 0 {
369 if offset > 0 && (offset as i64) * 60 > ts as i64 {
370 return Err(DateParseError);
371 }
372 if offset < 0 && (-(offset as i128)) * 60 > (TIMESTAMP_MAX as i128 - ts as i128) {
373 return Err(DateParseError);
374 }
375 let ts128 = ts as i128;
377 let adj = (offset as i128) * 60;
378 let new_ts = ts128 - adj;
379 if new_ts < 0 {
380 return Err(DateParseError);
381 }
382 ts = new_ts as u64;
383 }
384
385 Ok((ts, offset))
386}
387
388fn match_object_header_date(date: &[u8]) -> Option<(u64, i32)> {
389 if date.is_empty() || !date[0].is_ascii_digit() {
390 return None;
391 }
392 let (stamp, mut rest) = parse_timestamp_prefix(date);
393 if rest >= date.len() || date[rest] != b' ' {
394 return None;
395 }
396 if stamp == u64::MAX {
397 return None;
398 }
399 rest += 1;
400 if rest >= date.len() || (date[rest] != b'+' && date[rest] != b'-') {
401 return None;
402 }
403 let sign = date[rest];
404 rest += 1;
405 if rest + 4 > date.len() {
406 return None;
407 }
408 let tz_digits = std::str::from_utf8(&date[rest..rest + 4]).ok()?;
409 let ofs_raw: i32 = tz_digits.parse().ok()?;
410 let mut ofs = (ofs_raw / 100) * 60 + (ofs_raw % 100);
411 if sign == b'-' {
412 ofs = -ofs;
413 }
414 let end = rest + 4;
415 if end < date.len() && date[end] != b'\n' && date[end] != 0 {
416 return None;
417 }
418 Some((stamp, ofs))
419}
420
421fn match_tz(date: &[u8], offp: &mut i32) -> usize {
423 if date.is_empty() || (date[0] != b'+' && date[0] != b'-') {
424 return 0;
425 }
426 let (hour_ul, n) = parse_timestamp_prefix(&date[1..]);
427 let mut end = 1 + n;
428 let mut min: i32 = 0;
429 let mut hour: i32 = hour_ul as i32;
430 if n == 4 {
431 min = hour % 100;
432 hour /= 100;
433 } else if n != 2 {
434 min = 99;
435 } else if end < date.len() && date[end] == b':' {
436 let (m2, n2) = parse_timestamp_prefix(&date[end + 1..]);
437 if n2 == 0 {
438 min = 99;
439 } else {
440 min = m2 as i32;
441 end += 1 + n2;
442 if end - 1 != 5 {
443 min = 99;
444 }
445 }
446 }
447 if min < 60 && hour < 24 {
448 let mut off = hour * 60 + min;
449 if date[0] == b'-' {
450 off = -off;
451 }
452 *offp = off;
453 }
454 end
455}
456
457fn parse_long_prefix(s: &[u8]) -> (i64, usize) {
459 if s.is_empty() {
460 return (0, 0);
461 }
462 let mut i = 0usize;
463 let neg = s[0] == b'-';
464 if s[0] == b'+' || s[0] == b'-' {
465 i = 1;
466 }
467 let start = i;
468 while i < s.len() && s[i].is_ascii_digit() {
469 i += 1;
470 }
471 if i == start {
472 return (0, 0);
473 }
474 let Ok(slice) = std::str::from_utf8(&s[start..i]) else {
475 return (0, 0);
476 };
477 let Ok(v) = slice.parse::<i64>() else {
478 return (0, 0);
479 };
480 let v = if neg { -v } else { v };
481 (v, i)
482}
483
484fn parse_uint_suffix(s: &[u8]) -> (u64, usize) {
485 parse_timestamp_prefix(s)
486}
487
488fn set_date(year: i32, month: i32, day: i32, now_tm: Option<&tm>, now: i64, tm: &mut tm) -> i32 {
490 if !(month > 0 && month < 13 && day > 0 && day < 32) {
491 return -1;
492 }
493 let Some(nt) = now_tm else {
494 tm.tm_mon = month - 1;
495 tm.tm_mday = day;
496 if year == -1 {
497 return 1;
498 }
499 if (1970..2100).contains(&year) {
500 tm.tm_year = year - 1900;
501 } else if (70..100).contains(&year) {
502 tm.tm_year = year;
503 } else if year < 38 {
504 tm.tm_year = year + 100;
505 } else {
506 return -1;
507 }
508 return 0;
509 };
510 let mut check = *tm;
511 check.tm_mon = month - 1;
512 check.tm_mday = day;
513 if year == -1 {
514 check.tm_year = nt.tm_year;
515 } else if (1970..2100).contains(&year) {
516 check.tm_year = year - 1900;
517 } else if (70..100).contains(&year) {
518 check.tm_year = year;
519 } else if year < 38 {
520 check.tm_year = year + 100;
521 } else {
522 return -1;
523 }
524 let specified = tm_to_time_t(&check);
525 if specified >= 0 && now + 10 * 24 * 3600 < specified {
526 return -1;
527 }
528 tm.tm_mon = check.tm_mon;
529 tm.tm_mday = check.tm_mday;
530 if year != -1 {
531 tm.tm_year = check.tm_year;
532 }
533 0
534}
535
536fn set_time(hour: i64, minute: i64, second: i64, tm: &mut tm) -> i32 {
537 if (0..=24).contains(&hour) && (0..60).contains(&minute) && (0..=60).contains(&second) {
538 tm.tm_hour = hour as i32;
539 tm.tm_min = minute as i32;
540 tm.tm_sec = second as i32;
541 0
542 } else {
543 -1
544 }
545}
546
547pub(crate) fn match_multi_number(
549 num: u64,
550 date: &[u8],
551 sep_i: usize,
552 tm: &mut tm,
553 now_in: i64,
554) -> usize {
555 let Some(&c) = date.get(sep_i) else {
556 return 0;
557 };
558 if !matches!(c, b':' | b'-' | b'/' | b'.') {
559 return 0;
560 }
561
562 let (num2, n2) = parse_long_prefix(&date[sep_i + 1..]);
563 if n2 == 0 {
564 return 0;
565 }
566 let mut pos = sep_i + 1 + n2;
567 let mut num3: i64 = -1;
568 if pos < date.len() && date[pos] == c && pos + 1 < date.len() && date[pos + 1].is_ascii_digit()
569 {
570 let (n3, rel) = parse_long_prefix(&date[pos + 1..]);
571 num3 = n3;
572 pos += 1 + rel;
573 }
574
575 match c {
576 b':' => {
577 let mut n3 = num3;
578 if n3 < 0 {
579 n3 = 0;
580 }
581 if set_time(num as i64, num2, n3, tm) == 0 {
582 if pos < date.len()
583 && date[pos] == b'.'
584 && pos + 1 < date.len()
585 && date[pos + 1].is_ascii_digit()
586 && is_date_known(tm)
587 {
588 let (_, rel) = parse_long_prefix(&date[pos + 1..]);
589 pos += 1 + rel;
590 }
591 } else {
592 return 0;
593 }
594 }
595 b'-' | b'/' | b'.' => {
596 let now = if now_in == 0 {
597 process_wall_clock_sec()
598 } else {
599 now_in
600 };
601 let mut now_tm = empty_tm();
602 let refuse_future: Option<&tm> = if compat::gmtime(now as time_t, &mut now_tm) {
603 Some(&now_tm)
604 } else {
605 None
606 };
607
608 let y = num as i32;
609 let m = num2 as i32;
610 let d = if num3 < 0 { 0 } else { num3 as i32 };
611
612 if num > 70 {
613 if set_date(y, m, d, None, now, tm) == 0 {
614 return pos;
615 }
616 if set_date(y, d, m, None, now, tm) == 0 {
617 return pos;
618 }
619 }
620 if c != b'.' && set_date(d, y, m, refuse_future, now, tm) == 0 {
621 return pos;
622 }
623 if set_date(d, m, y, refuse_future, now, tm) == 0 {
624 return pos;
625 }
626 if c == b'.' && set_date(d, y, m, refuse_future, now, tm) == 0 {
627 return pos;
628 }
629 return 0;
630 }
631 _ => return 0,
632 }
633 pos
634}
635
636fn match_alpha(date: &[u8], tm: &mut tm, offset: &mut i32) -> usize {
637 for (i, name) in MONTH_NAMES.iter().enumerate() {
638 let m = match_string(date, name);
639 if m >= 3 {
640 tm.tm_mon = i as i32;
641 return m;
642 }
643 }
644
645 for (i, name) in WEEKDAY_NAMES.iter().enumerate() {
646 let m = match_string(date, name);
647 if m >= 3 {
648 tm.tm_wday = i as i32;
649 return m;
650 }
651 }
652
653 for tz in TIMEZONE_NAMES {
654 let m = match_string(date, tz.name);
655 if m >= 3 || m == tz.name.len() {
656 let off = tz.offset_hours + tz.dst;
657 if *offset == -1 {
658 *offset = 60 * off;
659 }
660 return m;
661 }
662 }
663
664 if match_string(date, "PM") == 2 {
665 tm.tm_hour = (tm.tm_hour % 12) + 12;
666 return 2;
667 }
668
669 if match_string(date, "AM") == 2 {
670 tm.tm_hour %= 12;
671 return 2;
672 }
673
674 if date.first() == Some(&b'T')
675 && date.get(1).is_some_and(|b| b.is_ascii_digit())
676 && tm.tm_hour == -1
677 {
678 tm.tm_min = 0;
679 tm.tm_sec = 0;
680 return 1;
681 }
682
683 skip_alpha(date)
684}
685
686fn match_digit(
687 date: &[u8],
688 tm: &mut tm,
689 offset: &mut i32,
690 tm_gmt: &mut i32,
691 now_sec: i64,
692) -> usize {
693 let (num, n) = parse_timestamp_prefix(date);
694 if n == 0 {
695 return 0;
696 }
697 let end = n;
698
699 if num >= 100_000_000 && nodate(tm) && compat::gmtime(num as time_t, tm) {
700 *tm_gmt = 1;
701 return end;
702 }
703
704 if let Some(&sep) = date.get(end) {
705 if matches!(sep, b':' | b'.' | b'/' | b'-')
706 && date.get(end + 1).is_some_and(|b| b.is_ascii_digit())
707 {
708 let m = match_multi_number(num, date, end, tm, now_sec);
709 if m != 0 {
710 return m;
711 }
712 }
713 }
714
715 let mut n_digits = 0usize;
716 loop {
717 n_digits += 1;
718 if n_digits >= date.len() || !date[n_digits].is_ascii_digit() {
719 break;
720 }
721 }
722
723 if n_digits == 8 || n_digits == 6 {
724 let num1 = (num / 10000) as i32;
725 let num2 = ((num % 10000) / 100) as i32;
726 let num3 = (num % 100) as i32;
727 if n_digits == 8 {
728 let _ = set_date(num1, num2, num3, None, now_sec, tm);
729 } else if set_time(num1 as i64, num2 as i64, num3 as i64, tm) == 0
730 && date.get(end) == Some(&b'.')
731 && date.get(end + 1).is_some_and(|b| b.is_ascii_digit())
732 {
733 let (_, rel) = parse_uint_suffix(&date[end + 1..]);
734 return end + 1 + rel;
735 }
736 return end;
737 }
738
739 if maybeiso8601(tm) {
740 let mut num1 = num as u32;
741 let mut num2: u32 = 0;
742 if n_digits == 4 {
743 num1 = (num / 100) as u32;
744 num2 = (num % 100) as u32;
745 }
746 if (n_digits == 4 || n_digits == 2)
747 && !nodate(tm)
748 && set_time(num1 as i64, num2 as i64, 0, tm) == 0
749 {
750 return n_digits;
751 }
752 tm.tm_min = -1;
753 tm.tm_sec = -1;
754 }
755
756 if n_digits == 4 {
757 if num <= 1400 && *offset == -1 {
758 let minutes = (num % 100) as u32;
759 let hours = (num / 100) as u32;
760 *offset = (hours * 60 + minutes) as i32;
761 } else if num > 1900 && num < 2100 {
762 tm.tm_year = (num as i32) - 1900;
763 }
764 return n_digits;
765 }
766
767 if n_digits > 2 {
768 return n_digits;
769 }
770
771 if num > 0 && num < 32 && tm.tm_mday < 0 {
772 tm.tm_mday = num as i32;
773 return n_digits;
774 }
775
776 if n_digits == 2 && tm.tm_year < 0 {
777 if num < 10 && tm.tm_mday >= 0 {
778 tm.tm_year = (num as i32) + 100;
779 return n_digits;
780 }
781 if num >= 70 {
782 tm.tm_year = num as i32;
783 return n_digits;
784 }
785 }
786
787 if num > 0 && num < 13 && tm.tm_mon < 0 {
788 tm.tm_mon = (num as i32) - 1;
789 }
790
791 n_digits
792}