@@ -165,7 +165,7 @@ pub(crate) mod function {
165
165
Time :: new ( val, 0 )
166
166
} else if let Some ( val) = relative:: parse ( input, now) . transpose ( ) ? {
167
167
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) {
169
169
// Format::Raw
170
170
val
171
171
} else {
@@ -236,6 +236,52 @@ pub(crate) mod function {
236
236
Some ( time)
237
237
}
238
238
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
+
239
285
/// This is just like `Zoned::strptime`, but it allows parsing datetimes
240
286
/// whose weekdays are inconsistent with the date. While the day-of-week
241
287
/// still must be parsed, it is otherwise ignored. This seems to be
@@ -252,6 +298,75 @@ pub(crate) mod function {
252
298
static P : rfc2822:: DateTimeParser = rfc2822:: DateTimeParser :: new ( ) . relaxed_weekday ( true ) ;
253
299
P . parse_zoned ( input)
254
300
}
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
+ }
255
370
}
256
371
257
372
mod relative {
0 commit comments