diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index c890863d8..6d8fcfc79 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -1212,28 +1212,28 @@ fn test_datetime_parse_from_str() { assert_eq!(parse("Aug 09 2013 23:54:35 -0900", "%b %d %Y %H:%M:%S %::z"), Ok(dt)); assert_eq!(parse("Aug 09 2013 23:54:35 -09:00", "%b %d %Y %H:%M:%S %::z"), Ok(dt)); assert_eq!(parse("Aug 09 2013 23:54:35 -09 : 00", "%b %d %Y %H:%M:%S %::z"), Ok(dt)); + assert_eq!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z"), Ok(dt)); // mismatching colon expectations - assert!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z").is_err()); assert_eq!(parse("Aug 09 2013 23:54:35 -09::00", "%b %d %Y %H:%M:%S %::z"), Ok(dt)); assert_eq!(parse("Aug 09 2013 23:54:35 -09::00", "%b %d %Y %H:%M:%S %:z"), Ok(dt)); // wrong timezone data assert!(parse("Aug 09 2013 23:54:35 -09", "%b %d %Y %H:%M:%S %::z").is_err()); - assert_eq!(parse("Aug 09 2013 23:54:35 -09001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt)); - assert_eq!(parse("Aug 09 2013 23:54:35 -09:001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt)); + assert_eq!(parse("Aug 09 2013 23:54:35 -0900001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt)); + assert_eq!(parse("Aug 09 2013 23:54:35 -09:00001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt)); assert_eq!(parse("Aug 09 2013 23:54:35 -0900 ", "%b %d %Y %H:%M:%S %::z "), Ok(dt)); assert_eq!(parse("Aug 09 2013 23:54:35 -0900\t\n", "%b %d %Y %H:%M:%S %::z\t\n"), Ok(dt)); - assert_eq!(parse("Aug 09 2013 23:54:35 -0900:", "%b %d %Y %H:%M:%S %::z:"), Ok(dt)); - assert_eq!(parse("Aug 09 2013 23:54:35 :-0900:0", "%b %d %Y %H:%M:%S :%::z:0"), Ok(dt)); + assert!(parse("Aug 09 2013 23:54:35 -0900:", "%b %d %Y %H:%M:%S %::z:").is_err()); + assert!(parse("Aug 09 2013 23:54:35 :-0900:0", "%b %d %Y %H:%M:%S :%::z:0").is_err()); // mismatching colons and spaces assert!(parse("Aug 09 2013 23:54:35 :-0900: ", "%b %d %Y %H:%M:%S :%::z::").is_err()); // mismatching colons expectations - assert!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z").is_err()); - assert_eq!(parse("Aug 09 2013 -0900: 23:54:35", "%b %d %Y %::z: %H:%M:%S"), Ok(dt)); - assert_eq!(parse("Aug 09 2013 :-0900:0 23:54:35", "%b %d %Y :%::z:0 %H:%M:%S"), Ok(dt)); + assert_eq!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z"), Ok(dt)); + assert!(parse("Aug 09 2013 -0900: 23:54:35", "%b %d %Y %::z: %H:%M:%S").is_err()); + assert!(parse("Aug 09 2013 :-0900:0 23:54:35", "%b %d %Y :%::z:0 %H:%M:%S").is_err()); // mismatching colons expectations mid-string assert!(parse("Aug 09 2013 :-0900: 23:54:35", "%b %d %Y :%::z %H:%M:%S").is_err()); // mismatching colons expectations, before end - assert!(parse("Aug 09 2013 23:54:35 -09:00:00 ", "%b %d %Y %H:%M:%S %::z ").is_err()); + assert_eq!(parse("Aug 09 2013 23:54:35 -09:00:00 ", "%b %d %Y %H:%M:%S %::z "), Ok(dt)); // // %:::z diff --git a/src/format/parse.rs b/src/format/parse.rs index 5a3a70273..ecb92a1bf 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -211,7 +211,8 @@ pub(crate) fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseRes parsed.set_nanosecond(nanosecond)?; } - let offset = try_consume!(scan::timezone_offset(s, |s| scan::char(s, b':'), true, false, true)); + let offset = + try_consume!(scan::timezone_offset(s, |s| scan::char(s, b':'), true, false, true, false)); // This range check is similar to the one in `FixedOffset::east_opt`, so it would be redundant. // 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. @@ -461,6 +462,7 @@ where false, false, true, + matches!(spec, &TimezoneOffsetDoubleColon), )); parsed.set_offset(i64::from(offset))?; } @@ -472,6 +474,7 @@ where true, false, true, + false, )); parsed.set_offset(i64::from(offset))?; } @@ -484,6 +487,7 @@ where true, true, true, + false, )); parsed.set_offset(i64::from(offset))?; } @@ -576,7 +580,7 @@ fn parse_rfc3339_relaxed<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult let (s, offset) = if s.len() >= 3 && "UTC".as_bytes().eq_ignore_ascii_case(&s.as_bytes()[..3]) { (&s[3..], 0) } else { - scan::timezone_offset(s, scan::colon_or_space, true, false, true)? + scan::timezone_offset(s, scan::colon_or_space, true, false, true, false)? }; parsed.set_offset(i64::from(offset))?; Ok((s, ())) @@ -1833,6 +1837,11 @@ mod tests { } } + #[test] + fn issue_1629() { + DateTime::parse_from_str("2023-01-02T23:24:25+01:30:01", "%Y-%m-%dT%H:%M:%S%::z").unwrap(); + } + #[test] fn test_issue_1010() { let dt = crate::NaiveDateTime::parse_from_str("\u{c}SUN\u{e}\u{3000}\0m@J\u{3000}\0\u{3000}\0m\u{c}!\u{c}\u{b}\u{c}\u{c}\u{c}\u{c}%A\u{c}\u{b}\0SU\u{c}\u{c}", diff --git a/src/format/scan.rs b/src/format/scan.rs index 1ab87b9dd..0330bf501 100644 --- a/src/format/scan.rs +++ b/src/format/scan.rs @@ -205,6 +205,7 @@ pub(crate) fn timezone_offset( allow_zulu: bool, allow_missing_minutes: bool, allow_tz_minus_sign: bool, + allow_seconds: bool, ) -> ParseResult<(&str, i32)> where F: FnMut(&str) -> ParseResult<&str>, @@ -272,13 +273,32 @@ where } else { return Err(TOO_SHORT); }; + + s = match s.len() { + len if len >= 2 => &s[2..], + 0 => s, + _ => return Err(TOO_SHORT), + }; + + let mut seconds = hours * 3600 + minutes * 60; + match s.chars().next() { + Some(':' | '0'..='9') if allow_seconds => {} + _ => return Ok((s, if negative { -seconds } else { seconds })), + } + + s = consume_colon(s)?; + seconds += match digits(s) { + Ok((s1 @ b'0'..=b'5', s2 @ b'0'..=b'9')) => i32::from((s1 - b'0') * 10 + (s2 - b'0')), + Ok((b'6'..=b'9', b'0'..=b'9')) => return Err(OUT_OF_RANGE), + _ => return Err(INVALID), + }; + s = match s.len() { len if len >= 2 => &s[2..], 0 => s, _ => return Err(TOO_SHORT), }; - let seconds = hours * 3600 + minutes * 60; Ok((s, if negative { -seconds } else { seconds })) } @@ -319,7 +339,7 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, i32)> { } Err(INVALID) } else { - timezone_offset(s, |s| Ok(s), false, false, false) + timezone_offset(s, |s| Ok(s), false, false, false, false) } } diff --git a/src/offset/fixed.rs b/src/offset/fixed.rs index e7382bed1..b370160a4 100644 --- a/src/offset/fixed.rs +++ b/src/offset/fixed.rs @@ -119,7 +119,8 @@ impl FixedOffset { impl FromStr for FixedOffset { type Err = ParseError; fn from_str(s: &str) -> Result { - let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?; + let (_, offset) = + scan::timezone_offset(s, scan::colon_or_space, false, false, true, false)?; Self::east_opt(offset).ok_or(OUT_OF_RANGE) } }