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: {}",
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    /// Formats a `DateTime` into the Smithy epoch seconds date-time format.
89    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    /// Parses the Smithy epoch seconds date-time format into a `DateTime`.
100    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    // This code is taken from https://github.com/pyfisch/httpdate and modified under an
141    // Apache 2.0 License. Modifications:
142    // - Removed use of unsafe
143    // - Add deserialization of subsecond nanos
144    //
145    /// Format a `DateTime` in the HTTP date format (imf-fixdate)
146    ///
147    /// Example: "Mon, 16 Dec 2019 23:48:18 GMT"
148    ///
149    /// Some notes:
150    /// - HTTP date does not support years before `0001`—this will cause a panic.
151    /// - Subsecond nanos are not emitted
152    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        // HTTP date does not support years before 0001
208        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        // Extract the individual digits from year
215        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        // Extract the individual digits from hour
225        push_digit(&mut out, hour / 10);
226        push_digit(&mut out, hour % 10);
227
228        out.push(':');
229
230        // Extract the individual digits from minute
231        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    /// Parse an IMF-fixdate formatted date into a DateTime
246    ///
247    /// This function has a few caveats:
248    /// 1. It DOES NOT support the "deprecated" formats supported by HTTP date
249    /// 2. It supports up to 3 digits of subsecond precision
250    ///
251    /// Ok: "Mon, 16 Dec 2019 23:48:18 GMT"
252    /// Ok: "Mon, 16 Dec 2019 23:48:18.123 GMT"
253    /// Ok: "Mon, 16 Dec 2019 23:48:18.12 GMT"
254    /// Not Ok: "Mon, 16 Dec 2019 23:48:18.1234 GMT"
255    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            // split_at is correct because we asserted that this date is only valid ASCII so the byte index is
269            // the same as the char index
270            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        // Example: `Sun, 06 Nov 1994 08:49:37 GMT`
287        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                // The date must end with " GMT", so read from the character after the `.`
299                // to 4 from the end
300                let fraction_slice = &s[26..s.len() - 4];
301                if fraction_slice.len() > 3 {
302                    // Only thousandths are supported
303                    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                // We need to convert the fractional second to nanoseconds, so we need to scale
310                // according the the number of decimals provided
311                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    // OK: 1985-04-12T23:20:50.52Z
395    // OK: 1985-04-12T23:20:50Z
396    //
397    // Timezones not supported:
398    // Not OK: 1985-04-12T23:20:50-02:00
399    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    /// Read 1 RFC-3339 date from &str and return the remaining str
424    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    /// Format a [DateTime] in the RFC-3339 date format
434    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        // This is stated in the assumptions for RFC-3339. ISO-8601 allows for years
462        // between -99,999 and 99,999 inclusive, but RFC-3339 is bound between 0 and 9,999.
463        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    /// Formats sub-second fraction for RFC-3339 (including the '.').
480    /// Expects to be called with a number of `micros` between 0 and 999_999 inclusive.
481    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            // This test suite can be regenerated by the following Kotlin class:
539            // `codegen/src/test/kotlin/software/amazon/smithy/rust/tool/TimeTestSuiteGenerator.kt`
540            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}