@@ -79,6 +79,8 @@ impl Timestamp {
7979 /// The maximum length of a timestamp in bytes
8080 pub const MAX_LENGTH : usize = 19 ;
8181
82+ const SEPARATORS : [ u8 ; 3 ] = [ b'-' , b'T' , b':' ] ;
83+
8284 /// Read a [`Timestamp`]
8385 ///
8486 /// NOTE: This will take [`Self::MAX_LENGTH`] bytes from the reader. Ensure that it only contains the timestamp
@@ -94,10 +96,8 @@ impl Timestamp {
9496 macro_rules! read_segment {
9597 ( $expr: expr) => {
9698 match $expr {
99+ Ok ( ( _, 0 ) ) => break ,
97100 Ok ( ( val, _) ) => Some ( val as u8 ) ,
98- Err ( LoftyError {
99- kind: ErrorKind :: Io ( io) ,
100- } ) if matches!( io. kind( ) , std:: io:: ErrorKind :: UnexpectedEof ) => break ,
101101 Err ( e) => return Err ( e. into( ) ) ,
102102 }
103103 } ;
@@ -118,6 +118,12 @@ impl Timestamp {
118118 return Ok ( None ) ;
119119 }
120120
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+
121127 let reader = & mut & content[ ..] ;
122128
123129 // 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 {
129135 }
130136
131137 timestamp. year = year;
138+ if reader. is_empty ( ) {
139+ return Ok ( Some ( timestamp) ) ;
140+ }
132141
133142 #[ allow( clippy:: never_loop) ]
134143 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+ ) ) ;
137154 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+ ) ) ;
140165 break ;
141166 }
142167
@@ -148,7 +173,9 @@ impl Timestamp {
148173 sep : Option < u8 > ,
149174 parse_mode : ParsingMode ,
150175 ) -> Result < ( u16 , usize ) > {
151- const SEPARATORS : [ u8 ; 3 ] = [ b'-' , b'T' , b':' ] ;
176+ if content. is_empty ( ) {
177+ return Ok ( ( 0 , 0 ) ) ;
178+ }
152179
153180 if let Some ( sep) = sep {
154181 let byte = content. read_u8 ( ) ?;
@@ -181,7 +208,10 @@ impl Timestamp {
181208 //
182209 // The easiest way to check for a missing digit is to see if we're just eating into
183210 // 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+ {
185215 break ;
186216 }
187217
@@ -370,4 +400,70 @@ mod tests {
370400 let empty_timestamp_strict = Timestamp :: parse ( & mut "" . as_bytes ( ) , ParsingMode :: Strict ) ;
371401 assert ! ( empty_timestamp_strict. is_err( ) ) ;
372402 }
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+ }
373469}
0 commit comments