Skip to content

Commit 71b2047

Browse files
committed
Fix parsing for zone offset seconds
1 parent 8b86349 commit 71b2047

File tree

4 files changed

+43
-14
lines changed

4 files changed

+43
-14
lines changed

src/datetime/tests.rs

+9-9
Original file line numberDiff line numberDiff line change
@@ -1212,28 +1212,28 @@ fn test_datetime_parse_from_str() {
12121212
assert_eq!(parse("Aug 09 2013 23:54:35 -0900", "%b %d %Y %H:%M:%S %::z"), Ok(dt));
12131213
assert_eq!(parse("Aug 09 2013 23:54:35 -09:00", "%b %d %Y %H:%M:%S %::z"), Ok(dt));
12141214
assert_eq!(parse("Aug 09 2013 23:54:35 -09 : 00", "%b %d %Y %H:%M:%S %::z"), Ok(dt));
1215+
assert_eq!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z"), Ok(dt));
12151216
// mismatching colon expectations
1216-
assert!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z").is_err());
12171217
assert_eq!(parse("Aug 09 2013 23:54:35 -09::00", "%b %d %Y %H:%M:%S %::z"), Ok(dt));
12181218
assert_eq!(parse("Aug 09 2013 23:54:35 -09::00", "%b %d %Y %H:%M:%S %:z"), Ok(dt));
12191219
// wrong timezone data
12201220
assert!(parse("Aug 09 2013 23:54:35 -09", "%b %d %Y %H:%M:%S %::z").is_err());
1221-
assert_eq!(parse("Aug 09 2013 23:54:35 -09001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt));
1222-
assert_eq!(parse("Aug 09 2013 23:54:35 -09:001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt));
1221+
assert_eq!(parse("Aug 09 2013 23:54:35 -0900001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt));
1222+
assert_eq!(parse("Aug 09 2013 23:54:35 -09:00001234", "%b %d %Y %H:%M:%S %::z1234"), Ok(dt));
12231223
assert_eq!(parse("Aug 09 2013 23:54:35 -0900 ", "%b %d %Y %H:%M:%S %::z "), Ok(dt));
12241224
assert_eq!(parse("Aug 09 2013 23:54:35 -0900\t\n", "%b %d %Y %H:%M:%S %::z\t\n"), Ok(dt));
1225-
assert_eq!(parse("Aug 09 2013 23:54:35 -0900:", "%b %d %Y %H:%M:%S %::z:"), Ok(dt));
1226-
assert_eq!(parse("Aug 09 2013 23:54:35 :-0900:0", "%b %d %Y %H:%M:%S :%::z:0"), Ok(dt));
1225+
assert!(parse("Aug 09 2013 23:54:35 -0900:", "%b %d %Y %H:%M:%S %::z:").is_err());
1226+
assert!(parse("Aug 09 2013 23:54:35 :-0900:0", "%b %d %Y %H:%M:%S :%::z:0").is_err());
12271227
// mismatching colons and spaces
12281228
assert!(parse("Aug 09 2013 23:54:35 :-0900: ", "%b %d %Y %H:%M:%S :%::z::").is_err());
12291229
// mismatching colons expectations
1230-
assert!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z").is_err());
1231-
assert_eq!(parse("Aug 09 2013 -0900: 23:54:35", "%b %d %Y %::z: %H:%M:%S"), Ok(dt));
1232-
assert_eq!(parse("Aug 09 2013 :-0900:0 23:54:35", "%b %d %Y :%::z:0 %H:%M:%S"), Ok(dt));
1230+
assert_eq!(parse("Aug 09 2013 23:54:35 -09:00:00", "%b %d %Y %H:%M:%S %::z"), Ok(dt));
1231+
assert!(parse("Aug 09 2013 -0900: 23:54:35", "%b %d %Y %::z: %H:%M:%S").is_err());
1232+
assert!(parse("Aug 09 2013 :-0900:0 23:54:35", "%b %d %Y :%::z:0 %H:%M:%S").is_err());
12331233
// mismatching colons expectations mid-string
12341234
assert!(parse("Aug 09 2013 :-0900: 23:54:35", "%b %d %Y :%::z %H:%M:%S").is_err());
12351235
// mismatching colons expectations, before end
1236-
assert!(parse("Aug 09 2013 23:54:35 -09:00:00 ", "%b %d %Y %H:%M:%S %::z ").is_err());
1236+
assert_eq!(parse("Aug 09 2013 23:54:35 -09:00:00 ", "%b %d %Y %H:%M:%S %::z "), Ok(dt));
12371237

12381238
//
12391239
// %:::z

src/format/parse.rs

+10-2
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ pub(crate) fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseRes
211211
parsed.set_nanosecond(nanosecond)?;
212212
}
213213

214-
let offset = try_consume!(scan::timezone_offset(s, |s| scan::char(s, b':'), true, false, true));
214+
let offset = try_consume!(scan::timezone_offset(s, |s| scan::char(s, b':'), true, false, true, false));
215215
// This range check is similar to the one in `FixedOffset::east_opt`, so it would be redundant.
216216
// But it is possible to read the offset directly from `Parsed`. We want to only successfully
217217
// populate `Parsed` if the input is fully valid RFC 3339.
@@ -461,6 +461,7 @@ where
461461
false,
462462
false,
463463
true,
464+
matches!(spec, &TimezoneOffsetDoubleColon),
464465
));
465466
parsed.set_offset(i64::from(offset))?;
466467
}
@@ -472,6 +473,7 @@ where
472473
true,
473474
false,
474475
true,
476+
false,
475477
));
476478
parsed.set_offset(i64::from(offset))?;
477479
}
@@ -484,6 +486,7 @@ where
484486
true,
485487
true,
486488
true,
489+
false,
487490
));
488491
parsed.set_offset(i64::from(offset))?;
489492
}
@@ -576,7 +579,7 @@ fn parse_rfc3339_relaxed<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult
576579
let (s, offset) = if s.len() >= 3 && "UTC".as_bytes().eq_ignore_ascii_case(&s.as_bytes()[..3]) {
577580
(&s[3..], 0)
578581
} else {
579-
scan::timezone_offset(s, scan::colon_or_space, true, false, true)?
582+
scan::timezone_offset(s, scan::colon_or_space, true, false, true, false)?
580583
};
581584
parsed.set_offset(i64::from(offset))?;
582585
Ok((s, ()))
@@ -1833,6 +1836,11 @@ mod tests {
18331836
}
18341837
}
18351838

1839+
#[test]
1840+
fn issue_1629() {
1841+
DateTime::parse_from_str("2023-01-02T23:24:25+01:30:01", "%Y-%m-%dT%H:%M:%S%::z").unwrap();
1842+
}
1843+
18361844
#[test]
18371845
fn test_issue_1010() {
18381846
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}",

src/format/scan.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ pub(crate) fn timezone_offset<F>(
205205
allow_zulu: bool,
206206
allow_missing_minutes: bool,
207207
allow_tz_minus_sign: bool,
208+
allow_seconds: bool,
208209
) -> ParseResult<(&str, i32)>
209210
where
210211
F: FnMut(&str) -> ParseResult<&str>,
@@ -272,13 +273,32 @@ where
272273
} else {
273274
return Err(TOO_SHORT);
274275
};
276+
277+
s = match dbg!(s).len() {
278+
len if len >= 2 => &s[2..],
279+
0 => s,
280+
_ => return Err(TOO_SHORT),
281+
};
282+
283+
let mut seconds = hours * 3600 + minutes * 60;
284+
match s.chars().next() {
285+
Some(':' | '0'..='9') if allow_seconds => {}
286+
_ => return Ok((s, if negative { -seconds } else { seconds })),
287+
}
288+
289+
s = consume_colon(s)?;
290+
seconds += match digits(s) {
291+
Ok((s1 @ b'0'..=b'5', s2 @ b'0'..=b'9')) => i32::from((s1 - b'0') * 10 + (s2 - b'0')),
292+
Ok((b'6'..=b'9', b'0'..=b'9')) => return Err(OUT_OF_RANGE),
293+
_ => return Err(INVALID),
294+
};
295+
275296
s = match s.len() {
276297
len if len >= 2 => &s[2..],
277298
0 => s,
278299
_ => return Err(TOO_SHORT),
279300
};
280301

281-
let seconds = hours * 3600 + minutes * 60;
282302
Ok((s, if negative { -seconds } else { seconds }))
283303
}
284304

@@ -319,7 +339,7 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, i32)> {
319339
}
320340
Err(INVALID)
321341
} else {
322-
timezone_offset(s, |s| Ok(s), false, false, false)
342+
timezone_offset(s, |s| Ok(s), false, false, false, false)
323343
}
324344
}
325345

src/offset/fixed.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ impl FixedOffset {
119119
impl FromStr for FixedOffset {
120120
type Err = ParseError;
121121
fn from_str(s: &str) -> Result<Self, Self::Err> {
122-
let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?;
122+
let (_, offset) =
123+
scan::timezone_offset(s, scan::colon_or_space, false, false, true, false)?;
123124
Self::east_opt(offset).ok_or(OUT_OF_RANGE)
124125
}
125126
}

0 commit comments

Comments
 (0)