Skip to content

Commit e787f81

Browse files
committed
Timestamp: Support parsing without separators
1 parent a029858 commit e787f81

File tree

2 files changed

+106
-9
lines changed

2 files changed

+106
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Fixed
1010
- **MusePack**: Fix potential panic when the beginning silence makes up the entire sample count ([PR](https://github.com/Serial-ATA/lofty-rs/pull/449))
11+
- **Timestamp**: Support timestamps without separators (ex. "20240906" vs "2024-09-06") ([issue](https://github.com/Serial-ATA/lofty-rs/issues/452)) ([PR](https://github.com/Serial-ATA/lofty-rs/issues/453))
1112

1213
## [0.21.1] - 2024-08-28
1314

lofty/src/tag/items/timestamp.rs

Lines changed: 105 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)