@@ -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+ 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+ Time { seconds, offset } . into ( )
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
@@ -252,6 +298,75 @@ pub(crate) mod function {
252298 static P : rfc2822:: DateTimeParser = rfc2822:: DateTimeParser :: new ( ) . relaxed_weekday ( true ) ;
253299 P . parse_zoned ( input)
254300 }
301+
302+ #[ cfg( test) ]
303+ mod tests {
304+ use super :: * ;
305+
306+ #[ test]
307+ fn parse_raw_valid ( ) {
308+ // These examples show how it's more loose than it has to be,
309+ // merely as a side effect of the implementation.
310+ for ( valid, expected_seconds, expected_offset) in [
311+ ( "12345 +0000" , 12345 , 0 ) ,
312+ ( "-1234567 +0000" , -1234567 , 0 ) ,
313+ ( "+1234567 -000000" , 1234567 , 0 ) ,
314+ ( " +0 -000000 " , 0 , 0 ) ,
315+ ( "\t -0\t -0000\t " , 0 , 0 ) ,
316+ ( "\n -0\r \n -0000\n " , 0 , 0 ) ,
317+ ] {
318+ assert_eq ! (
319+ parse_raw( valid) ,
320+ Some ( Time {
321+ seconds: expected_seconds,
322+ offset: expected_offset
323+ } ) ,
324+ "should succeed: '{valid}'"
325+ ) ;
326+ }
327+ }
328+
329+ #[ test]
330+ fn parse_raw_invalid ( ) {
331+ for ( bad_date_str, message) in [
332+ ( "123456 !0600" , "invalid sign - must be + or -" ) ,
333+ ( "123456 0600" , "missing offset sign" ) ,
334+ ( "123456 +060" , "positive offset too short" ) ,
335+ ( "123456 -060" , "negative offset too short" ) ,
336+ ( "123456 +06000" , "not enough offset seconds" ) ,
337+ ( "123456 --060" , "duplicate offset sign with correct offset length" ) ,
338+ ( "123456 -+060" , "multiple offset signs with correct offset length" ) ,
339+ ( "123456 --0600" , "multiple offset signs, but incorrect offset length" ) ,
340+ ( "123456 +-06000" , "multiple offset signs with correct offset length" ) ,
341+ ( "123456 +-0600" , "multiple offset signs with incorrect offset length" ) ,
342+ ( "123456 +-060" , "multiple offset signs with correct offset length" ) ,
343+ ( "123456 +10030" , "invalid offset length with one 'second' field" ) ,
344+ ( "123456 06000" , "invalid offset length, missing sign" ) ,
345+ ( "123456 +0600 extra" , "extra field past offset" ) ,
346+ ( "123456 +0600 2005" , "extra field past offset that looks like year" ) ,
347+ ( "123456+0600" , "missing space between unix timestamp and offset" ) ,
348+ (
349+ "123456 + 600" ,
350+ "extra spaces between sign and offset (which also is too short)" ,
351+ ) ,
352+ ( "123456 -1500" , "negative offset hours out of bounds" ) ,
353+ ( "123456 +1500" , "positive offset hours out of bounds" ) ,
354+ ( "123456 +6600" , "positive offset hours out of bounds" ) ,
355+ ( "123456 +0660" , "invalid offset minutes" ) ,
356+ ( "123456 +060010" , "positive offset seconds is allowed but only if zero" ) ,
357+ ( "123456 -060010" , "negative offset seconds is allowed but only if zero" ) ,
358+ ( "123456 +0075" , "positive offset minutes invalid" ) ,
359+ ( "++123456 +0000" , "duplicate timestamp sign" ) ,
360+ ( "--123456 +0000" , "duplicate timestamp sign" ) ,
361+ ( "1234567 -+1+1+0" , "unsigned offset parsing rejects '+'" ) ,
362+ ] {
363+ assert ! (
364+ parse_raw( bad_date_str) . is_none( ) ,
365+ "should fail: '{bad_date_str}': {message}"
366+ ) ;
367+ }
368+ }
369+ }
255370}
256371
257372mod relative {
0 commit comments