1use std::borrow::Cow;
7use std::error::Error;
8use std::fmt;
9
10const NANOS_PER_SECOND: u32 = 1_000_000_000;
11
12#[derive(Debug)]
13pub(super) enum DateTimeParseErrorKind {
14 Invalid(Cow<'static, str>),
16 IntParseError,
18}
19
20#[derive(Debug)]
22pub struct DateTimeParseError {
23 kind: DateTimeParseErrorKind,
24}
25
26impl Error for DateTimeParseError {}
27
28impl fmt::Display for DateTimeParseError {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 use DateTimeParseErrorKind::*;
31 match &self.kind {
32 Invalid(msg) => write!(f, "invalid date-time: {msg}"),
33 IntParseError => write!(f, "failed to parse int"),
34 }
35 }
36}
37
38impl From<DateTimeParseErrorKind> for DateTimeParseError {
39 fn from(kind: DateTimeParseErrorKind) -> Self {
40 Self { kind }
41 }
42}
43
44#[derive(Debug)]
45enum DateTimeFormatErrorKind {
46 OutOfRange(Cow<'static, str>),
48}
49
50#[derive(Debug)]
52pub struct DateTimeFormatError {
53 kind: DateTimeFormatErrorKind,
54}
55
56impl Error for DateTimeFormatError {}
57
58impl fmt::Display for DateTimeFormatError {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 match &self.kind {
61 DateTimeFormatErrorKind::OutOfRange(msg) => write!(
62 f,
63 "date-time cannot be formatted since it is out of range: {msg}"
64 ),
65 }
66 }
67}
68
69impl From<DateTimeFormatErrorKind> for DateTimeFormatError {
70 fn from(kind: DateTimeFormatErrorKind) -> Self {
71 DateTimeFormatError { kind }
72 }
73}
74
75fn remove_trailing_zeros(string: &mut String) {
76 while let Some(b'0') = string.as_bytes().last() {
77 string.pop();
78 }
79}
80
81pub(crate) mod epoch_seconds {
82 use super::remove_trailing_zeros;
83 use super::{DateTimeParseError, DateTimeParseErrorKind};
84 use crate::DateTime;
85 use std::str::FromStr;
86
87 pub(crate) fn format(date_time: &DateTime) -> String {
89 if date_time.subsecond_nanos == 0 {
90 format!("{}", date_time.seconds)
91 } else {
92 let mut result = format!("{}.{:0>9}", date_time.seconds, date_time.subsecond_nanos);
93 remove_trailing_zeros(&mut result);
94 result
95 }
96 }
97
98 pub(crate) fn parse(value: &str) -> Result<DateTime, DateTimeParseError> {
100 let mut parts = value.splitn(2, '.');
101 let (mut whole, mut decimal) = (0i64, 0u32);
102 if let Some(whole_str) = parts.next() {
103 whole =
104 <i64>::from_str(whole_str).map_err(|_| DateTimeParseErrorKind::IntParseError)?;
105 }
106 if let Some(decimal_str) = parts.next() {
107 if decimal_str.starts_with('+') || decimal_str.starts_with('-') {
108 return Err(DateTimeParseErrorKind::Invalid(
109 "invalid epoch-seconds timestamp".into(),
110 )
111 .into());
112 }
113 if decimal_str.len() > 9 {
114 return Err(DateTimeParseErrorKind::Invalid(
115 "decimal is longer than 9 digits".into(),
116 )
117 .into());
118 }
119 let missing_places = 9 - decimal_str.len() as isize;
120 decimal =
121 <u32>::from_str(decimal_str).map_err(|_| DateTimeParseErrorKind::IntParseError)?;
122 for _ in 0..missing_places {
123 decimal *= 10;
124 }
125 }
126 Ok(DateTime::from_secs_and_nanos(whole, decimal))
127 }
128}
129
130pub(crate) mod http_date {
131 use crate::date_time::format::{
132 DateTimeFormatError, DateTimeFormatErrorKind, DateTimeParseError, DateTimeParseErrorKind,
133 NANOS_PER_SECOND,
134 };
135 use crate::DateTime;
136 use std::str::FromStr;
137 use time::{Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};
138
139 pub(crate) fn format(date_time: &DateTime) -> Result<String, DateTimeFormatError> {
152 fn out_of_range<E: std::fmt::Display>(cause: E) -> DateTimeFormatError {
153 DateTimeFormatErrorKind::OutOfRange(
154 format!(
155 "HTTP dates support dates between Mon, 01 Jan 0001 00:00:00 GMT \
156 and Fri, 31 Dec 9999 23:59:59.999 GMT. {cause}"
157 )
158 .into(),
159 )
160 .into()
161 }
162 let structured = OffsetDateTime::from_unix_timestamp_nanos(date_time.as_nanos())
163 .map_err(out_of_range)?;
164 let weekday = match structured.weekday() {
165 Weekday::Monday => "Mon",
166 Weekday::Tuesday => "Tue",
167 Weekday::Wednesday => "Wed",
168 Weekday::Thursday => "Thu",
169 Weekday::Friday => "Fri",
170 Weekday::Saturday => "Sat",
171 Weekday::Sunday => "Sun",
172 };
173 let month = match structured.month() {
174 Month::January => "Jan",
175 Month::February => "Feb",
176 Month::March => "Mar",
177 Month::April => "Apr",
178 Month::May => "May",
179 Month::June => "Jun",
180 Month::July => "Jul",
181 Month::August => "Aug",
182 Month::September => "Sep",
183 Month::October => "Oct",
184 Month::November => "Nov",
185 Month::December => "Dec",
186 };
187 let mut out = String::with_capacity(32);
188 fn push_digit(out: &mut String, digit: u8) {
189 debug_assert!(digit < 10);
190 out.push((b'0' + digit) as char);
191 }
192
193 out.push_str(weekday);
194 out.push_str(", ");
195 let day = structured.day();
196 push_digit(&mut out, day / 10);
197 push_digit(&mut out, day % 10);
198
199 out.push(' ');
200 out.push_str(month);
201
202 out.push(' ');
203
204 let year = structured.year();
205 let year = if year < 1 {
207 return Err(out_of_range("HTTP dates cannot be before the year 0001"));
208 } else {
209 year as u32
210 };
211
212 push_digit(&mut out, (year / 1000) as u8);
214 push_digit(&mut out, (year / 100 % 10) as u8);
215 push_digit(&mut out, (year / 10 % 10) as u8);
216 push_digit(&mut out, (year % 10) as u8);
217
218 out.push(' ');
219
220 let hour = structured.hour();
221
222 push_digit(&mut out, hour / 10);
224 push_digit(&mut out, hour % 10);
225
226 out.push(':');
227
228 let minute = structured.minute();
230 push_digit(&mut out, minute / 10);
231 push_digit(&mut out, minute % 10);
232
233 out.push(':');
234
235 let second = structured.second();
236 push_digit(&mut out, second / 10);
237 push_digit(&mut out, second % 10);
238
239 out.push_str(" GMT");
240 Ok(out)
241 }
242
243 pub(crate) fn parse(s: &str) -> Result<DateTime, DateTimeParseError> {
254 if !s.is_ascii() {
255 return Err(DateTimeParseErrorKind::Invalid("date-time must be ASCII".into()).into());
256 }
257 let x = s.trim().as_bytes();
258 parse_imf_fixdate(x)
259 }
260
261 pub(crate) fn read(s: &str) -> Result<(DateTime, &str), DateTimeParseError> {
262 if !s.is_ascii() {
263 return Err(DateTimeParseErrorKind::Invalid("date-time must be ASCII".into()).into());
264 }
265 let (first_date, rest) = match find_subsequence(s.as_bytes(), b" GMT") {
266 Some(idx) => s.split_at(idx),
269 None => {
270 return Err(DateTimeParseErrorKind::Invalid("date-time is not GMT".into()).into())
271 }
272 };
273 Ok((parse(first_date)?, rest))
274 }
275
276 fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
277 haystack
278 .windows(needle.len())
279 .position(|window| window == needle)
280 .map(|idx| idx + needle.len())
281 }
282
283 fn parse_imf_fixdate(s: &[u8]) -> Result<DateTime, DateTimeParseError> {
284 if s.len() < 29
286 || s.len() > 33
287 || !s.ends_with(b" GMT")
288 || s[16] != b' '
289 || s[19] != b':'
290 || s[22] != b':'
291 {
292 return Err(DateTimeParseErrorKind::Invalid("incorrectly shaped string".into()).into());
293 }
294 let nanos: u32 = match &s[25] {
295 b'.' => {
296 let fraction_slice = &s[26..s.len() - 4];
299 if fraction_slice.len() > 3 {
300 return Err(DateTimeParseErrorKind::Invalid(
302 "Smithy http-date only supports millisecond precision".into(),
303 )
304 .into());
305 }
306 let fraction: u32 = parse_slice(fraction_slice)?;
307 let multiplier = [10, 100, 1000];
310 fraction * (NANOS_PER_SECOND / multiplier[fraction_slice.len() - 1])
311 }
312 b' ' => 0,
313 _ => {
314 return Err(
315 DateTimeParseErrorKind::Invalid("incorrectly shaped string".into()).into(),
316 )
317 }
318 };
319
320 let hours = parse_slice(&s[17..19])?;
321 let minutes = parse_slice(&s[20..22])?;
322 let seconds = parse_slice(&s[23..25])?;
323 let time = Time::from_hms_nano(hours, minutes, seconds, nanos).map_err(|err| {
324 DateTimeParseErrorKind::Invalid(
325 format!("time components are out of range: {err}").into(),
326 )
327 })?;
328
329 let month = match &s[7..12] {
330 b" Jan " => Month::January,
331 b" Feb " => Month::February,
332 b" Mar " => Month::March,
333 b" Apr " => Month::April,
334 b" May " => Month::May,
335 b" Jun " => Month::June,
336 b" Jul " => Month::July,
337 b" Aug " => Month::August,
338 b" Sep " => Month::September,
339 b" Oct " => Month::October,
340 b" Nov " => Month::November,
341 b" Dec " => Month::December,
342 month => {
343 return Err(DateTimeParseErrorKind::Invalid(
344 format!(
345 "invalid month: {}",
346 std::str::from_utf8(month).unwrap_or_default()
347 )
348 .into(),
349 )
350 .into())
351 }
352 };
353 let year = parse_slice(&s[12..16])?;
354 let day = parse_slice(&s[5..7])?;
355 let date = Date::from_calendar_date(year, month, day).map_err(|err| {
356 DateTimeParseErrorKind::Invalid(
357 format!("date components are out of range: {err}").into(),
358 )
359 })?;
360 let date_time = PrimitiveDateTime::new(date, time).assume_offset(UtcOffset::UTC);
361
362 Ok(DateTime::from_nanos(date_time.unix_timestamp_nanos())
363 .expect("this date format cannot produce out of range date-times"))
364 }
365
366 fn parse_slice<T>(ascii_slice: &[u8]) -> Result<T, DateTimeParseError>
367 where
368 T: FromStr,
369 {
370 let as_str =
371 std::str::from_utf8(ascii_slice).expect("should only be called on ascii strings");
372 Ok(as_str
373 .parse::<T>()
374 .map_err(|_| DateTimeParseErrorKind::IntParseError)?)
375 }
376}
377
378pub(crate) mod rfc3339 {
379 use crate::date_time::format::{
380 DateTimeFormatError, DateTimeFormatErrorKind, DateTimeParseError, DateTimeParseErrorKind,
381 };
382 use crate::DateTime;
383 use time::format_description::well_known::Rfc3339;
384 use time::OffsetDateTime;
385
386 #[derive(Debug, PartialEq)]
387 pub(crate) enum AllowOffsets {
388 OffsetsAllowed,
389 OffsetsForbidden,
390 }
391
392 pub(crate) fn parse(
398 s: &str,
399 allow_offsets: AllowOffsets,
400 ) -> Result<DateTime, DateTimeParseError> {
401 if allow_offsets == AllowOffsets::OffsetsForbidden && !matches!(s.chars().last(), Some('Z'))
402 {
403 return Err(DateTimeParseErrorKind::Invalid(
404 "Smithy does not support timezone offsets in RFC-3339 date times".into(),
405 )
406 .into());
407 }
408 if s.len() > 10 && !matches!(s.as_bytes()[10], b'T' | b't') {
409 return Err(DateTimeParseErrorKind::Invalid(
410 "RFC-3339 only allows `T` as a separator for date-time values".into(),
411 )
412 .into());
413 }
414 let date_time = OffsetDateTime::parse(s, &Rfc3339).map_err(|err| {
415 DateTimeParseErrorKind::Invalid(format!("invalid RFC-3339 date-time: {err}").into())
416 })?;
417 Ok(DateTime::from_nanos(date_time.unix_timestamp_nanos())
418 .expect("this date format cannot produce out of range date-times"))
419 }
420
421 pub(crate) fn read(
423 s: &str,
424 allow_offests: AllowOffsets,
425 ) -> Result<(DateTime, &str), DateTimeParseError> {
426 let delim = s.find('Z').map(|idx| idx + 1).unwrap_or_else(|| s.len());
427 let (head, rest) = s.split_at(delim);
428 Ok((parse(head, allow_offests)?, rest))
429 }
430
431 pub(crate) fn format(date_time: &DateTime) -> Result<String, DateTimeFormatError> {
433 use std::fmt::Write;
434 fn out_of_range<E: std::fmt::Display>(cause: E) -> DateTimeFormatError {
435 DateTimeFormatErrorKind::OutOfRange(
436 format!(
437 "RFC-3339 timestamps support dates between 0001-01-01T00:00:00.000Z \
438 and 9999-12-31T23:59:59.999Z. {cause}"
439 )
440 .into(),
441 )
442 .into()
443 }
444 let (year, month, day, hour, minute, second, micros) = {
445 let s = OffsetDateTime::from_unix_timestamp_nanos(date_time.as_nanos())
446 .map_err(out_of_range)?;
447 (
448 s.year(),
449 u8::from(s.month()),
450 s.day(),
451 s.hour(),
452 s.minute(),
453 s.second(),
454 s.microsecond(),
455 )
456 };
457
458 if !(1..=9_999).contains(&year) {
461 return Err(out_of_range(""));
462 }
463
464 let mut out = String::with_capacity(33);
465 write!(
466 out,
467 "{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}"
468 )
469 .unwrap();
470 format_subsecond_fraction(&mut out, micros);
471 out.push('Z');
472 Ok(out)
473 }
474
475 fn format_subsecond_fraction(into: &mut String, micros: u32) {
478 debug_assert!(micros < 1_000_000);
479 if micros > 0 {
480 into.push('.');
481 let (mut remaining, mut place) = (micros, 100_000);
482 while remaining > 0 {
483 let digit = (remaining / place) % 10;
484 into.push(char::from(b'0' + (digit as u8)));
485 remaining -= digit * place;
486 place /= 10;
487 }
488 }
489 }
490}
491
492#[cfg(test)]
493mod tests {
494 use super::*;
495 use crate::date_time::format::rfc3339::AllowOffsets;
496 use crate::DateTime;
497 use lazy_static::lazy_static;
498 use proptest::prelude::*;
499 use std::fs::File;
500 use std::io::Read;
501 use std::str::FromStr;
502
503 #[derive(Debug, serde::Deserialize)]
504 struct TestCase {
505 canonical_seconds: String,
506 canonical_nanos: u32,
507 #[allow(dead_code)]
508 iso8601: String,
509 #[allow(dead_code)]
510 error: bool,
511 smithy_format_value: Option<String>,
512 }
513 impl TestCase {
514 fn time(&self) -> DateTime {
515 DateTime::from_secs_and_nanos(
516 <i64>::from_str(&self.canonical_seconds).unwrap(),
517 self.canonical_nanos,
518 )
519 }
520 }
521
522 #[derive(serde::Deserialize)]
523 struct TestCases {
524 format_date_time: Vec<TestCase>,
525 format_http_date: Vec<TestCase>,
526 format_epoch_seconds: Vec<TestCase>,
527 parse_date_time: Vec<TestCase>,
528 parse_http_date: Vec<TestCase>,
529 parse_epoch_seconds: Vec<TestCase>,
530 }
531
532 lazy_static! {
533 static ref TEST_CASES: TestCases = {
534 let mut json = Vec::new();
537 let mut file = File::open("test_data/date_time_format_test_suite.json").expect("open test data file");
538 file.read_to_end(&mut json).expect("read test data");
539 serde_json::from_slice(&json).expect("valid test data")
540 };
541 }
542
543 fn format_test<F>(test_cases: &[TestCase], format: F)
544 where
545 F: Fn(&DateTime) -> Result<String, DateTimeFormatError>,
546 {
547 for test_case in test_cases {
548 if let Some(expected) = test_case.smithy_format_value.as_ref() {
549 let actual = format(&test_case.time()).expect("failed to format");
550 assert_eq!(expected, &actual, "Additional context:\n{test_case:#?}");
551 } else {
552 format(&test_case.time()).expect_err("date should fail to format");
553 }
554 }
555 }
556
557 fn parse_test<F>(test_cases: &[TestCase], parse: F)
558 where
559 F: Fn(&str) -> Result<DateTime, DateTimeParseError>,
560 {
561 for test_case in test_cases {
562 let expected = test_case.time();
563 let to_parse = test_case
564 .smithy_format_value
565 .as_ref()
566 .expect("parse test cases should always have a formatted value");
567 let actual = parse(to_parse);
568
569 assert!(
570 actual.is_ok(),
571 "Failed to parse `{}`: {}\nAdditional context:\n{:#?}",
572 to_parse,
573 actual.err().unwrap(),
574 test_case
575 );
576 assert_eq!(
577 expected,
578 actual.unwrap(),
579 "Additional context:\n{test_case:#?}"
580 );
581 }
582 }
583
584 #[test]
585 fn format_epoch_seconds() {
586 format_test(&TEST_CASES.format_epoch_seconds, |dt| {
587 Ok(epoch_seconds::format(dt))
588 });
589 }
590
591 #[test]
592 fn parse_epoch_seconds() {
593 parse_test(&TEST_CASES.parse_epoch_seconds, epoch_seconds::parse);
594 }
595
596 #[test]
597 fn format_http_date() {
598 format_test(&TEST_CASES.format_http_date, http_date::format);
599 }
600
601 #[test]
602 fn parse_http_date() {
603 parse_test(&TEST_CASES.parse_http_date, http_date::parse);
604 }
605
606 #[test]
607 fn date_time_out_of_range() {
608 assert_eq!(
609 "0001-01-01T00:00:00Z",
610 rfc3339::format(&DateTime::from_secs(-62_135_596_800)).unwrap()
611 );
612 assert_eq!(
613 "9999-12-31T23:59:59.999999Z",
614 rfc3339::format(&DateTime::from_secs_and_nanos(253402300799, 999_999_999)).unwrap()
615 );
616
617 assert!(matches!(
618 rfc3339::format(&DateTime::from_secs(-62_135_596_800 - 1)),
619 Err(DateTimeFormatError {
620 kind: DateTimeFormatErrorKind::OutOfRange(_)
621 })
622 ));
623 assert!(matches!(
624 rfc3339::format(&DateTime::from_secs(253402300799 + 1)),
625 Err(DateTimeFormatError {
626 kind: DateTimeFormatErrorKind::OutOfRange(_)
627 })
628 ));
629 }
630
631 #[test]
632 fn format_date_time() {
633 format_test(&TEST_CASES.format_date_time, rfc3339::format);
634 }
635
636 #[test]
637 fn parse_date_time() {
638 parse_test(&TEST_CASES.parse_date_time, |date| {
639 rfc3339::parse(date, AllowOffsets::OffsetsForbidden)
640 });
641 }
642
643 #[test]
644 fn epoch_seconds_invalid_cases() {
645 assert!(epoch_seconds::parse("").is_err());
646 assert!(epoch_seconds::parse("123.+456").is_err());
647 assert!(epoch_seconds::parse("123.-456").is_err());
648 assert!(epoch_seconds::parse("123.456.789").is_err());
649 assert!(epoch_seconds::parse("123 . 456").is_err());
650 assert!(epoch_seconds::parse("123.456 ").is_err());
651 assert!(epoch_seconds::parse(" 123.456").is_err());
652 assert!(epoch_seconds::parse("a.456").is_err());
653 assert!(epoch_seconds::parse("123.a").is_err());
654 assert!(epoch_seconds::parse("123..").is_err());
655 assert!(epoch_seconds::parse(".123").is_err());
656 }
657
658 #[test]
659 fn read_rfc3339_date_comma_split() {
660 let date = "1985-04-12T23:20:50Z,1985-04-12T23:20:51Z";
661 let (e1, date) =
662 rfc3339::read(date, AllowOffsets::OffsetsForbidden).expect("should succeed");
663 let (e2, date2) =
664 rfc3339::read(&date[1..], AllowOffsets::OffsetsForbidden).expect("should succeed");
665 assert_eq!(date2, "");
666 assert_eq!(date, ",1985-04-12T23:20:51Z");
667 let expected = DateTime::from_secs_and_nanos(482196050, 0);
668 assert_eq!(e1, expected);
669 let expected = DateTime::from_secs_and_nanos(482196051, 0);
670 assert_eq!(e2, expected);
671 }
672
673 #[test]
674 fn parse_rfc3339_with_timezone() {
675 let dt = rfc3339::parse("1985-04-12T21:20:51-02:00", AllowOffsets::OffsetsAllowed);
676 assert_eq!(dt.unwrap(), DateTime::from_secs_and_nanos(482196051, 0));
677 }
678
679 #[test]
680 fn parse_rfc3339_timezone_forbidden() {
681 let dt = rfc3339::parse("1985-04-12T23:20:50-02:00", AllowOffsets::OffsetsForbidden);
682 assert!(matches!(
683 dt.unwrap_err(),
684 DateTimeParseError {
685 kind: DateTimeParseErrorKind::Invalid(_)
686 }
687 ));
688 }
689
690 #[test]
691 fn http_date_out_of_range() {
692 assert_eq!(
693 "Mon, 01 Jan 0001 00:00:00 GMT",
694 http_date::format(&DateTime::from_secs(-62_135_596_800)).unwrap()
695 );
696 assert_eq!(
697 "Fri, 31 Dec 9999 23:59:59 GMT",
698 http_date::format(&DateTime::from_secs_and_nanos(253402300799, 999_999_999)).unwrap()
699 );
700
701 assert!(matches!(
702 http_date::format(&DateTime::from_secs(-62_135_596_800 - 1)),
703 Err(DateTimeFormatError {
704 kind: DateTimeFormatErrorKind::OutOfRange(_)
705 })
706 ));
707 assert!(matches!(
708 http_date::format(&DateTime::from_secs(253402300799 + 1)),
709 Err(DateTimeFormatError {
710 kind: DateTimeFormatErrorKind::OutOfRange(_)
711 })
712 ));
713 }
714
715 #[test]
716 fn http_date_too_much_fraction() {
717 let fractional = "Mon, 16 Dec 2019 23:48:18.1212 GMT";
718 assert!(matches!(
719 http_date::parse(fractional),
720 Err(DateTimeParseError {
721 kind: DateTimeParseErrorKind::Invalid(_)
722 })
723 ));
724 }
725
726 #[test]
727 fn http_date_bad_fraction() {
728 let fractional = "Mon, 16 Dec 2019 23:48:18. GMT";
729 assert!(matches!(
730 http_date::parse(fractional),
731 Err(DateTimeParseError {
732 kind: DateTimeParseErrorKind::IntParseError
733 })
734 ));
735 }
736
737 #[test]
738 fn http_date_read_date() {
739 let fractional = "Mon, 16 Dec 2019 23:48:18.123 GMT,some more stuff";
740 let ts = 1576540098;
741 let expected = DateTime::from_fractional_secs(ts, 0.123);
742 let (actual, rest) = http_date::read(fractional).expect("valid");
743 assert_eq!(rest, ",some more stuff");
744 assert_eq!(expected, actual);
745 http_date::read(rest).expect_err("invalid date");
746 }
747
748 #[track_caller]
749 fn http_date_check_roundtrip(epoch_secs: i64, subsecond_nanos: u32) {
750 let date_time = DateTime::from_secs_and_nanos(epoch_secs, subsecond_nanos);
751 let formatted = http_date::format(&date_time).unwrap();
752 let parsed = http_date::parse(&formatted);
753 let read = http_date::read(&formatted);
754 match parsed {
755 Err(failure) => panic!("Date failed to parse {failure:?}"),
756 Ok(date) => {
757 assert!(read.is_ok());
758 if date.subsecond_nanos != subsecond_nanos {
759 assert_eq!(http_date::format(&date_time).unwrap(), formatted);
760 } else {
761 assert_eq!(date, date_time)
762 }
763 }
764 }
765 }
766
767 #[test]
768 fn http_date_roundtrip() {
769 for epoch_secs in -1000..1000 {
770 http_date_check_roundtrip(epoch_secs, 1);
771 }
772
773 http_date_check_roundtrip(1576540098, 0);
774 http_date_check_roundtrip(9999999999, 0);
775 }
776
777 #[test]
778 fn parse_rfc3339_invalid_separator() {
779 let test_cases = [
780 ("1985-04-12 23:20:50Z", AllowOffsets::OffsetsForbidden),
781 ("1985-04-12x23:20:50Z", AllowOffsets::OffsetsForbidden),
782 ("1985-04-12 23:20:50-02:00", AllowOffsets::OffsetsAllowed),
783 ("1985-04-12a23:20:50-02:00", AllowOffsets::OffsetsAllowed),
784 ];
785 for (date, offset) in test_cases.into_iter() {
786 let dt = rfc3339::parse(date, offset);
787 assert!(matches!(
788 dt.unwrap_err(),
789 DateTimeParseError {
790 kind: DateTimeParseErrorKind::Invalid(_)
791 }
792 ));
793 }
794 }
795 #[test]
796 fn parse_rfc3339_t_separator() {
797 let test_cases = [
798 ("1985-04-12t23:20:50Z", AllowOffsets::OffsetsForbidden),
799 ("1985-04-12T23:20:50Z", AllowOffsets::OffsetsForbidden),
800 ("1985-04-12t23:20:50-02:00", AllowOffsets::OffsetsAllowed),
801 ("1985-04-12T23:20:50-02:00", AllowOffsets::OffsetsAllowed),
802 ];
803 for (date, offset) in test_cases.into_iter() {
804 let dt = rfc3339::parse(date, offset);
805 assert!(
806 dt.is_ok(),
807 "failed to parse date: '{}' with error: {:?}",
808 date,
809 dt.err().unwrap()
810 );
811 }
812 }
813
814 proptest! {
815 #![proptest_config(ProptestConfig::with_cases(10000))]
816
817 #[test]
818 fn round_trip(secs in -10000000..9999999999i64, nanos in 0..1_000_000_000u32) {
819 http_date_check_roundtrip(secs, nanos);
820 }
821 }
822}