Skip to content

Commit 9c28f33

Browse files
committed
fix(gix-date): add parse_raw()
When parsing arbitrary date inputs from the user with gix_date::parse(), the non-strict gix_date::parse_header() would accept a variety of inputs that do not match any of the known/accepted formats, but would nonetheless be accepted as being "close enough" to a raw date as found in git commit headers. The new parse_raw() function takes a stricter approach to parsing raw dates as found in commit headers, only accepting inputs that unambiguously match a valid raw date. The gix_date::parse() function is updated to use the new parse_raw() function instead of the less-strict parse_header(). This has the effect of making gix_date::parse() avoid mis-parsing various inputs that would otherwise have been accepted by gix_date::parse_header(). For example "28 Jan 2005".
1 parent f6e0ca4 commit 9c28f33

File tree

4 files changed

+91
-7
lines changed

4 files changed

+91
-7
lines changed

gix-date/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub mod time;
1313

1414
///
1515
pub mod parse;
16-
pub use parse::function::{parse, parse_header};
16+
pub use parse::function::{parse, parse_header, parse_raw};
1717

1818
/// A timestamp with timezone.
1919
#[derive(Default, PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]

gix-date/src/parse.rs

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ pub(crate) mod function {
165165
Time::new(val, 0)
166166
} else if let Some(val) = relative::parse(input, now).transpose()? {
167167
Time::new(val.timestamp().as_second(), val.offset().seconds())
168-
} else if let Some(val) = parse_header(input) {
168+
} else if let Some(val) = parse_raw(input) {
169169
// Format::Raw
170170
val
171171
} else {
@@ -236,6 +236,52 @@ pub(crate) mod function {
236236
Some(time)
237237
}
238238

239+
/// Strictly parse the raw commit header format like `1745582210 +0200`.
240+
///
241+
/// Some strict rules include:
242+
///
243+
/// - The timezone offset must be present.
244+
/// - The timezone offset must have a sign; either `+` or `-`.
245+
/// - The timezone offset hours must be less than or equal to 14.
246+
/// - The timezone offset minutes must be exactly 0, 15, 30, or 45.
247+
/// - The timezone offset seconds may be present, but 0 is the only valid value.
248+
/// - Only whitespace may suffix the timezone offset.
249+
///
250+
/// But this function isn't perfectly strict insofar as it allows arbitrary
251+
/// whitespace before and after the seconds and offset components.
252+
///
253+
/// The goal is to only accept inputs that _unambiguously_ look like
254+
/// git's raw date format.
255+
pub fn parse_raw(input: &str) -> Option<Time> {
256+
let mut split = input.split_whitespace();
257+
let seconds = split.next()?.parse::<SecondsSinceUnixEpoch>().ok()?;
258+
let offset_str = split.next()?;
259+
if split.next().is_some() {
260+
return None;
261+
}
262+
let offset_len = offset_str.len();
263+
if offset_len != 5 && offset_len != 7 {
264+
return None;
265+
}
266+
let sign: i32 = match offset_str.get(..1)? {
267+
"-" => Some(-1),
268+
"+" => Some(1),
269+
_ => None,
270+
}?;
271+
let hours: u8 = offset_str.get(1..3)?.parse().ok()?;
272+
let minutes: u8 = offset_str.get(3..5)?.parse().ok()?;
273+
let offset_seconds: u8 = if offset_len == 7 {
274+
offset_str.get(5..7)?.parse().ok()?
275+
} else {
276+
0
277+
};
278+
if hours > 14 || (minutes != 0 && minutes != 15 && minutes != 30 && minutes != 45) || offset_seconds != 0 {
279+
return None;
280+
}
281+
let offset: i32 = sign * ((hours as i32) * 3600 + (minutes as i32) * 60);
282+
Some(Time { seconds, offset })
283+
}
284+
239285
/// This is just like `Zoned::strptime`, but it allows parsing datetimes
240286
/// whose weekdays are inconsistent with the date. While the day-of-week
241287
/// still must be parsed, it is otherwise ignored. This seems to be

gix-date/tests/time/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ mod write_to {
101101
assert_eq!(output.as_bstr(), expected);
102102
assert_eq!(time.size(), output.len());
103103

104-
let actual = gix_date::parse(&output.as_bstr().to_string(), None).expect("round-trippable");
104+
let actual = output.as_bstr().to_string().parse::<Time>().expect("round-trippable");
105105
assert_eq!(time, actual);
106106
}
107107
Ok(())

gix-date/tests/time/parse.rs

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,22 +83,27 @@ fn raw() -> gix_testtools::Result {
8383
);
8484

8585
assert_eq!(
86-
gix_date::parse("1313584730 +051800", None)?,
86+
gix_date::parse("1313584730 +051500", None)?,
8787
Time {
8888
seconds: 1313584730,
89-
offset: 19080,
89+
offset: 18900,
9090
},
9191
"seconds for time-offsets work as well"
9292
);
9393

9494
assert_eq!(
95-
gix_date::parse("1313584730 +051842", None)?,
95+
gix_date::parse("1313584730 -0230", None)?,
9696
Time {
9797
seconds: 1313584730,
98-
offset: 19122,
98+
offset: -150 * 60,
9999
},
100100
);
101101

102+
assert!(gix_date::parse("1313584730 +1500", None).is_err());
103+
assert!(gix_date::parse("1313584730 +000001", None).is_err());
104+
assert!(gix_date::parse("1313584730 +0001", None).is_err());
105+
assert!(gix_date::parse("1313584730 +000100", None).is_err());
106+
102107
let expected = Time {
103108
seconds: 1660874655,
104109
offset: -28800,
@@ -117,6 +122,39 @@ fn raw() -> gix_testtools::Result {
117122
Ok(())
118123
}
119124

125+
#[test]
126+
fn bad_raw_strict() {
127+
for bad_date_str in [
128+
"123456 !0600",
129+
"123456 0600",
130+
"123456 +060",
131+
"123456 -060",
132+
"123456 +06000",
133+
"123456 --060",
134+
"123456 -+060",
135+
"123456 --0600",
136+
"123456 +-06000",
137+
"123456 +-0600",
138+
"123456 +-060",
139+
"123456 +10030",
140+
"123456 06000",
141+
"123456 0600",
142+
"123456 +0600 extra",
143+
"123456 +0600 2005",
144+
"123456+0600",
145+
"123456 + 600",
146+
"123456 -1500",
147+
"123456 +1500",
148+
"123456 +6600",
149+
"123456 +0660",
150+
"123456 +060010",
151+
"123456 -060010",
152+
"123456 +0075",
153+
] {
154+
assert!(gix_date::parse_raw(bad_date_str).is_none());
155+
}
156+
}
157+
120158
#[test]
121159
fn bad_raw() {
122160
for bad_date_str in [

0 commit comments

Comments
 (0)