diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index c6be142d08..f7b53afa52 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -405,7 +405,7 @@ fn test_datetime_rfc2822_and_rfc3339() { ); assert_eq!( DateTime::parse_from_rfc2822("Wed, 18 Feb 2015 23:16:09 -0000"), - Ok(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap()) + Ok(FixedOffset::OFFSET_UNKNOWN.with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap()) ); assert_eq!( DateTime::parse_from_rfc3339("2015-02-18T23:16:09Z"), diff --git a/src/format/parse.rs b/src/format/parse.rs index c3f50067c3..d98f08ce02 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -14,6 +14,7 @@ use super::scan; use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad, Parsed}; use super::{ParseError, ParseErrorKind, ParseResult}; use super::{BAD_FORMAT, INVALID, NOT_ENOUGH, OUT_OF_RANGE, TOO_LONG, TOO_SHORT}; +use crate::format::parsed::NO_OFFSET_INFO; use crate::{DateTime, FixedOffset, Weekday}; fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<()> { @@ -144,10 +145,8 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st } s = scan::space(s)?; // mandatory - if let Some(offset) = try_consume!(scan::timezone_offset_2822(s)) { - // only set the offset when it is definitely known (i.e. not `-0000`) - parsed.set_offset(i64::from(offset))?; - } + let offset = try_consume!(scan::timezone_offset_2822(s)); + parsed.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))?; // optional comments while let Ok((s_out, ())) = scan::comment_2822(s) { @@ -220,12 +219,12 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st // But it is possible to read the offset directly from `Parsed`. We want to only successfully // populate `Parsed` if the input is fully valid RFC 3339. const MAX_OFFSET: i32 = 23 * 3600 + 59 * 60; - if offset < -MAX_OFFSET || offset > MAX_OFFSET { - return Err(OUT_OF_RANGE); + if offset >= Some(-MAX_OFFSET) || offset <= Some(MAX_OFFSET) { + parsed.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))?; + Ok((s, ())) + } else { + Err(OUT_OF_RANGE) } - parsed.set_offset(i64::from(offset))?; - - Ok((s, ())) } /// Tries to parse given string into `parsed` with given formatting items. @@ -462,7 +461,9 @@ where s.trim_left(), scan::colon_or_space )); - parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; + parsed + .set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO))) + .map_err(|e| (s, e))?; } &TimezoneOffsetColonZ | &TimezoneOffsetZ => { @@ -470,7 +471,9 @@ where s.trim_left(), scan::colon_or_space )); - parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; + parsed + .set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO))) + .map_err(|e| (s, e))?; } &Internal(InternalFixed { val: InternalInternal::TimezoneOffsetPermissive, @@ -479,7 +482,9 @@ where s.trim_left(), scan::colon_or_space )); - parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?; + parsed + .set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO))) + .map_err(|e| (s, e))?; } &RFC2822 => try_consume!(parse_rfc2822(parsed, s)), @@ -776,7 +781,7 @@ fn test_parse() { // fixed: timezone offsets check!("+00:00", [fix!(TimezoneOffset)]; offset: 0); - check!("-00:00", [fix!(TimezoneOffset)]; offset: 0); + check!("-00:00", [fix!(TimezoneOffset)]; offset: NO_OFFSET_INFO); check!("+00:01", [fix!(TimezoneOffset)]; offset: 60); check!("-00:01", [fix!(TimezoneOffset)]; offset: -60); check!("+00:30", [fix!(TimezoneOffset)]; offset: 30 * 60); @@ -858,7 +863,6 @@ fn test_parse() { #[cfg(test)] #[test] fn test_rfc2822() { - use super::NOT_ENOUGH; use super::*; use crate::offset::FixedOffset; use crate::DateTime; @@ -882,6 +886,7 @@ fn test_rfc2822() { ("20 Jan 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // no day of week ("20 JAN 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // upper case month ("Tue, 20 Jan 2015 17:35 -0800", Ok("Tue, 20 Jan 2015 17:35:00 -0800")), // no second + ("20 Jan 2015 17:35:20 -0000", Ok("Tue, 20 Jan 2015 17:35:20 -0000")), // -0000 offset ("11 Sep 2001 09:45:00 EST", Ok("Tue, 11 Sep 2001 09:45:00 -0500")), ("30 Feb 2015 17:35:20 -0800", Err(OUT_OF_RANGE)), // bad day of month ("Tue, 20 Jan 2015", Err(TOO_SHORT)), // omitted fields @@ -892,7 +897,10 @@ fn test_rfc2822() { ("Tue, 20 Jan 2015 17:35:90 -0800", Err(OUT_OF_RANGE)), // bad second ("Tue, 20 Jan 2015 17:35:20 -0890", Err(OUT_OF_RANGE)), // bad offset ("6 Jun 1944 04:00:00Z", Err(INVALID)), // bad offset (zulu not allowed) - ("Tue, 20 Jan 2015 17:35:20 HAS", Err(NOT_ENOUGH)), // bad named time zone + ("Tue, 20 Jan 2015 17:35:20 HAS", Err(INVALID)), // bad named time zone + ("20 Jan 2015 17:35:20 +0000", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), + ("20 Jan 2015 17:35:20 -0001", Ok("Tue, 20 Jan 2015 17:35:20 -0001")), + ("Tue, 20 Jan 2015 17:35:20 -9900", Err(OUT_OF_RANGE)), // bad offset // named timezones that have specific timezone offsets // see https://www.rfc-editor.org/rfc/rfc2822#section-4.3 ("Tue, 20 Jan 2015 17:35:20 GMT", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), @@ -914,7 +922,8 @@ fn test_rfc2822() { ("Tue, 20 Jan 2015 17:35:20 K", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), ("Tue, 20 Jan 2015 17:35:20 k", Ok("Tue, 20 Jan 2015 17:35:20 +0000")), // named single-letter timezone "J" is specifically not valid - ("Tue, 20 Jan 2015 17:35:20 J", Err(NOT_ENOUGH)), + ("Tue, 20 Jan 2015 17:35:20 J", Err(INVALID)), + ("Tue, 20 Jan 2015😈17:35:20 -0800", Err(INVALID)), // bad character! ]; fn rfc2822_to_datetime(date: &str) -> ParseResult> { @@ -997,6 +1006,7 @@ fn test_rfc3339() { let testdates = [ ("2015-01-20T17:35:20-08:00", Ok("2015-01-20T17:35:20-08:00")), // normal case ("1944-06-06T04:04:00Z", Ok("1944-06-06T04:04:00+00:00")), // D-day + ("2015-01-20T17:35:20-00:00", Ok("2015-01-20T17:35:20-00:00")), // offset -00:00 ("2001-09-11T09:45:00-08:00", Ok("2001-09-11T09:45:00-08:00")), ("2015-01-20T17:35:20.001-08:00", Ok("2015-01-20T17:35:20.001-08:00")), ("2015-01-20T17:35:20.000031-08:00", Ok("2015-01-20T17:35:20.000031-08:00")), diff --git a/src/format/scan.rs b/src/format/scan.rs index 2962ef162b..b76bfd9e58 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -207,7 +207,9 @@ pub(super) fn colon_or_space(s: &str) -> ParseResult<&str> { /// /// The additional `colon` may be used to parse a mandatory or optional `:` /// between hours and minutes, and should return either a new suffix or `Err` when parsing fails. -pub(super) fn timezone_offset(s: &str, consume_colon: F) -> ParseResult<(&str, i32)> +/// +/// May return `None` which indicates no offset data is available (i.e. `-0000`). +pub(super) fn timezone_offset(s: &str, consume_colon: F) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { @@ -218,7 +220,7 @@ fn timezone_offset_internal( mut s: &str, mut consume_colon: F, allow_missing_minutes: bool, -) -> ParseResult<(&str, i32)> +) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { @@ -268,22 +270,27 @@ where }; let seconds = hours * 3600 + minutes * 60; - Ok((s, if negative { -seconds } else { seconds })) + + if seconds == 0 && negative { + return Ok((s, None)); + } + Ok((s, Some(if negative { -seconds } else { seconds }))) } /// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as `+00:00`. -pub(super) fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, i32)> +/// May return `None` which indicates no offset data is available (i.e. `-0000`). +pub(super) fn timezone_offset_zulu(s: &str, colon: F) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { let bytes = s.as_bytes(); match bytes.first() { - Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), + Some(&b'z') | Some(&b'Z') => Ok((&s[1..], Some(0))), Some(&b'u') | Some(&b'U') => { if bytes.len() >= 3 { let (b, c) = (bytes[1], bytes[2]); match (b | 32, c | 32) { - (b't', b'c') => Ok((&s[3..], 0)), + (b't', b'c') => Ok((&s[3..], Some(0))), _ => Err(INVALID), } } else { @@ -296,18 +303,18 @@ where /// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as /// `+00:00`, and allows missing minutes entirely. -pub(super) fn timezone_offset_permissive(s: &str, colon: F) -> ParseResult<(&str, i32)> +pub(super) fn timezone_offset_permissive(s: &str, colon: F) -> ParseResult<(&str, Option)> where F: FnMut(&str) -> ParseResult<&str>, { match s.as_bytes().first() { - Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)), + Some(&b'z') | Some(&b'Z') => Ok((&s[1..], Some(0))), _ => timezone_offset_internal(s, colon, true), } } /// Same as `timezone_offset` but also allows for RFC 2822 legacy timezones. -/// May return `None` which indicates an insufficient offset data (i.e. `-0000`). +/// May return `None` which indicates no offset data is available (i.e. `-0000`). /// See [RFC 2822 Section 4.3]. /// /// [RFC 2822 Section 4.3]: https://tools.ietf.org/html/rfc2822#section-4.3 @@ -334,14 +341,14 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option)> match name[0] { // recommended by RFC 2822: consume but treat it as -0000 b'a'..=b'i' | b'k'..=b'z' | b'A'..=b'I' | b'K'..=b'Z' => offset_hours(0), - _ => Ok((s, None)), + _ => Err(INVALID), } } else { - Ok((s, None)) + Err(INVALID) } } else { let (s_, offset) = timezone_offset(s, |s| Ok(s))?; - Ok((s_, Some(offset))) + Ok((s_, offset)) } }