aws_smithy_types/date_time/
format.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use 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    /// The given date-time string was invalid.
15    Invalid(Cow<'static, str>),
16    /// Failed to parse an integer inside the given date-time string.
17    IntParseError,
18}
19
20/// Error returned when date-time parsing fails.
21#[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    /// The given date-time cannot be represented in the requested date format.
47    OutOfRange(Cow<'static, str>),
48}
49
50/// Error returned when date-time formatting fails.
51#[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    /// Formats a `DateTime` into the Smithy epoch seconds date-time format.
88    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    /// Parses the Smithy epoch seconds date-time format into a `DateTime`.
99    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    // This code is taken from https://github.com/pyfisch/httpdate and modified under an
140    // Apache 2.0 License. Modifications:
141    // - Removed use of unsafe
142    // - Add deserialization of subsecond nanos
143    //
144    /// Format a `DateTime` in the HTTP date format (imf-fixdate)
145    ///
146    /// Example: "Mon, 16 Dec 2019 23:48:18 GMT"
147    ///
148    /// Some notes:
149    /// - HTTP date does not support years before `0001`—this will cause a panic.
150    /// - Subsecond nanos are not emitted
151    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        // HTTP date does not support years before 0001
206        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        // Extract the individual digits from year
213        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        // Extract the individual digits from hour
223        push_digit(&mut out, hour / 10);
224        push_digit(&mut out, hour % 10);
225
226        out.push(':');
227
228        // Extract the individual digits from minute
229        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    /// Parse an IMF-fixdate formatted date into a DateTime
244    ///
245    /// This function has a few caveats:
246    /// 1. It DOES NOT support the "deprecated" formats supported by HTTP date
247    /// 2. It supports up to 3 digits of subsecond precision
248    ///
249    /// Ok: "Mon, 16 Dec 2019 23:48:18 GMT"
250    /// Ok: "Mon, 16 Dec 2019 23:48:18.123 GMT"
251    /// Ok: "Mon, 16 Dec 2019 23:48:18.12 GMT"
252    /// Not Ok: "Mon, 16 Dec 2019 23:48:18.1234 GMT"
253    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            // split_at is correct because we asserted that this date is only valid ASCII so the byte index is
267            // the same as the char index
268            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        // Example: `Sun, 06 Nov 1994 08:49:37 GMT`
285        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                // The date must end with " GMT", so read from the character after the `.`
297                // to 4 from the end
298                let fraction_slice = &s[26..s.len() - 4];
299                if fraction_slice.len() > 3 {
300                    // Only thousandths are supported
301                    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                // We need to convert the fractional second to nanoseconds, so we need to scale
308                // according the the number of decimals provided
309                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    // OK: 1985-04-12T23:20:50.52Z
393    // OK: 1985-04-12T23:20:50Z
394    //
395    // Timezones not supported:
396    // Not OK: 1985-04-12T23:20:50-02:00
397    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    /// Read 1 RFC-3339 date from &str and return the remaining str
422    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    /// Format a [DateTime] in the RFC-3339 date format
432    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        // This is stated in the assumptions for RFC-3339. ISO-8601 allows for years
459        // between -99,999 and 99,999 inclusive, but RFC-3339 is bound between 0 and 9,999.
460        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    /// Formats sub-second fraction for RFC-3339 (including the '.').
476    /// Expects to be called with a number of `micros` between 0 and 999_999 inclusive.
477    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            // This test suite can be regenerated by the following Kotlin class:
535            // `codegen/src/test/kotlin/software/amazon/smithy/rust/tool/TimeTestSuiteGenerator.kt`
536            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}