@@ -79,6 +79,8 @@ impl Timestamp {
79
79
/// The maximum length of a timestamp in bytes
80
80
pub const MAX_LENGTH : usize = 19 ;
81
81
82
+ const SEPARATORS : [ u8 ; 3 ] = [ b'-' , b'T' , b':' ] ;
83
+
82
84
/// Read a [`Timestamp`]
83
85
///
84
86
/// NOTE: This will take [`Self::MAX_LENGTH`] bytes from the reader. Ensure that it only contains the timestamp
@@ -94,10 +96,8 @@ impl Timestamp {
94
96
macro_rules! read_segment {
95
97
( $expr: expr) => {
96
98
match $expr {
99
+ Ok ( ( _, 0 ) ) => break ,
97
100
Ok ( ( val, _) ) => Some ( val as u8 ) ,
98
- Err ( LoftyError {
99
- kind: ErrorKind :: Io ( io) ,
100
- } ) if matches!( io. kind( ) , std:: io:: ErrorKind :: UnexpectedEof ) => break ,
101
101
Err ( e) => return Err ( e. into( ) ) ,
102
102
}
103
103
} ;
@@ -118,6 +118,12 @@ impl Timestamp {
118
118
return Ok ( None ) ;
119
119
}
120
120
121
+ // It is valid for a timestamp to contain no separators, but this will lower our tolerance
122
+ // for common mistakes. We ignore the "T" separator here because it is **ALWAYS** required.
123
+ let timestamp_contains_separators = content
124
+ . iter ( )
125
+ . any ( |& b| b != b'T' && Self :: SEPARATORS . contains ( & b) ) ;
126
+
121
127
let reader = & mut & content[ ..] ;
122
128
123
129
// We need to very that the year is exactly 4 bytes long. This doesn't matter for other segments.
@@ -129,14 +135,33 @@ impl Timestamp {
129
135
}
130
136
131
137
timestamp. year = year;
138
+ if reader. is_empty ( ) {
139
+ return Ok ( Some ( timestamp) ) ;
140
+ }
132
141
133
142
#[ allow( clippy:: never_loop) ]
134
143
loop {
135
- timestamp. month = read_segment ! ( Self :: segment:: <2 >( reader, Some ( b'-' ) , parse_mode) ) ;
136
- timestamp. day = read_segment ! ( Self :: segment:: <2 >( reader, Some ( b'-' ) , parse_mode) ) ;
144
+ timestamp. month = read_segment ! ( Self :: segment:: <2 >(
145
+ reader,
146
+ timestamp_contains_separators. then_some( b'-' ) ,
147
+ parse_mode
148
+ ) ) ;
149
+ timestamp. day = read_segment ! ( Self :: segment:: <2 >(
150
+ reader,
151
+ timestamp_contains_separators. then_some( b'-' ) ,
152
+ parse_mode
153
+ ) ) ;
137
154
timestamp. hour = read_segment ! ( Self :: segment:: <2 >( reader, Some ( b'T' ) , parse_mode) ) ;
138
- timestamp. minute = read_segment ! ( Self :: segment:: <2 >( reader, Some ( b':' ) , parse_mode) ) ;
139
- timestamp. second = read_segment ! ( Self :: segment:: <2 >( reader, Some ( b':' ) , parse_mode) ) ;
155
+ timestamp. minute = read_segment ! ( Self :: segment:: <2 >(
156
+ reader,
157
+ timestamp_contains_separators. then_some( b':' ) ,
158
+ parse_mode
159
+ ) ) ;
160
+ timestamp. second = read_segment ! ( Self :: segment:: <2 >(
161
+ reader,
162
+ timestamp_contains_separators. then_some( b':' ) ,
163
+ parse_mode
164
+ ) ) ;
140
165
break ;
141
166
}
142
167
@@ -148,7 +173,9 @@ impl Timestamp {
148
173
sep : Option < u8 > ,
149
174
parse_mode : ParsingMode ,
150
175
) -> Result < ( u16 , usize ) > {
151
- const SEPARATORS : [ u8 ; 3 ] = [ b'-' , b'T' , b':' ] ;
176
+ if content. is_empty ( ) {
177
+ return Ok ( ( 0 , 0 ) ) ;
178
+ }
152
179
153
180
if let Some ( sep) = sep {
154
181
let byte = content. read_u8 ( ) ?;
@@ -181,7 +208,10 @@ impl Timestamp {
181
208
//
182
209
// The easiest way to check for a missing digit is to see if we're just eating into
183
210
// the next segment's separator.
184
- if sep. is_some ( ) && SEPARATORS . contains ( & i) && parse_mode != ParsingMode :: Strict {
211
+ if sep. is_some ( )
212
+ && Self :: SEPARATORS . contains ( & i)
213
+ && parse_mode != ParsingMode :: Strict
214
+ {
185
215
break ;
186
216
}
187
217
@@ -370,4 +400,70 @@ mod tests {
370
400
let empty_timestamp_strict = Timestamp :: parse ( & mut "" . as_bytes ( ) , ParsingMode :: Strict ) ;
371
401
assert ! ( empty_timestamp_strict. is_err( ) ) ;
372
402
}
403
+
404
+ #[ test_log:: test]
405
+ fn timestamp_no_separators ( ) {
406
+ let timestamp = "20240603T140849" ;
407
+ let parsed_timestamp =
408
+ Timestamp :: parse ( & mut timestamp. as_bytes ( ) , ParsingMode :: BestAttempt ) . unwrap ( ) ;
409
+ assert_eq ! ( parsed_timestamp, Some ( expected( ) ) ) ;
410
+ }
411
+
412
+ #[ test_log:: test]
413
+ fn timestamp_decode_partial_no_separators ( ) {
414
+ let partial_timestamps: [ ( & [ u8 ] , Timestamp ) ; 6 ] = [
415
+ (
416
+ b"2024" ,
417
+ Timestamp {
418
+ year : 2024 ,
419
+ ..Timestamp :: default ( )
420
+ } ,
421
+ ) ,
422
+ (
423
+ b"202406" ,
424
+ Timestamp {
425
+ year : 2024 ,
426
+ month : Some ( 6 ) ,
427
+ ..Timestamp :: default ( )
428
+ } ,
429
+ ) ,
430
+ (
431
+ b"20240603" ,
432
+ Timestamp {
433
+ year : 2024 ,
434
+ month : Some ( 6 ) ,
435
+ day : Some ( 3 ) ,
436
+ ..Timestamp :: default ( )
437
+ } ,
438
+ ) ,
439
+ (
440
+ b"20240603T14" ,
441
+ Timestamp {
442
+ year : 2024 ,
443
+ month : Some ( 6 ) ,
444
+ day : Some ( 3 ) ,
445
+ hour : Some ( 14 ) ,
446
+ ..Timestamp :: default ( )
447
+ } ,
448
+ ) ,
449
+ (
450
+ b"20240603T1408" ,
451
+ Timestamp {
452
+ year : 2024 ,
453
+ month : Some ( 6 ) ,
454
+ day : Some ( 3 ) ,
455
+ hour : Some ( 14 ) ,
456
+ minute : Some ( 8 ) ,
457
+ ..Timestamp :: default ( )
458
+ } ,
459
+ ) ,
460
+ ( b"20240603T140849" , expected ( ) ) ,
461
+ ] ;
462
+
463
+ for ( data, expected) in partial_timestamps {
464
+ let parsed_timestamp = Timestamp :: parse ( & mut & data[ ..] , ParsingMode :: Strict )
465
+ . unwrap_or_else ( |e| panic ! ( "{e}: {}" , std:: str :: from_utf8( data) . unwrap( ) ) ) ;
466
+ assert_eq ! ( parsed_timestamp, Some ( expected) ) ;
467
+ }
468
+ }
373
469
}
0 commit comments