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