#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TrackMetadata {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
}
pub fn parse(body: &[u8]) -> Option<TrackMetadata> {
let mut meta: Option<TrackMetadata> = None;
walk(body, &mut |tag, payload| {
if tag == *b"mlit" {
let m = meta.get_or_insert_with(TrackMetadata::default);
walk(payload, &mut |tag, payload| {
let field = match &tag {
b"minm" => &mut m.title,
b"asar" => &mut m.artist,
b"asal" => &mut m.album,
_ => return,
};
*field = Some(String::from_utf8_lossy(payload).into_owned());
});
}
});
meta
}
fn walk(mut data: &[u8], f: &mut impl FnMut([u8; 4], &[u8])) {
while data.len() >= 8 {
let tag: [u8; 4] = data[..4].try_into().unwrap();
let len = u32::from_be_bytes(data[4..8].try_into().unwrap()) as usize;
let rest = &data[8..];
if len > rest.len() {
return;
}
f(tag, &rest[..len]);
data = &rest[len..];
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(tag: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut e = Vec::with_capacity(8 + payload.len());
e.extend_from_slice(tag);
e.extend_from_slice(&(payload.len() as u32).to_be_bytes());
e.extend_from_slice(payload);
e
}
fn mlit(children: &[Vec<u8>]) -> Vec<u8> {
entry(b"mlit", &children.concat())
}
#[test]
fn extracts_title_artist_album_from_mlit() {
let body = mlit(&[
entry(b"minm", "Song Title".as_bytes()),
entry(b"asar", "The Artist".as_bytes()),
entry(b"asal", "The Album".as_bytes()),
]);
assert_eq!(
parse(&body),
Some(TrackMetadata {
title: Some("Song Title".into()),
artist: Some("The Artist".into()),
album: Some("The Album".into()),
})
);
}
#[test]
fn unknown_tags_are_skipped() {
let body = mlit(&[
entry(b"astm", &180_000u32.to_be_bytes()),
entry(b"minm", b"Song"),
entry(b"mper", &42u64.to_be_bytes()),
entry(b"asal", b"Album"),
]);
let meta = parse(&body).unwrap();
assert_eq!(meta.title.as_deref(), Some("Song"));
assert_eq!(meta.album.as_deref(), Some("Album"));
assert_eq!(meta.artist, None);
}
#[test]
fn missing_fields_stay_none() {
let meta = parse(&mlit(&[entry(b"minm", b"Just A Title")])).unwrap();
assert_eq!(meta.title.as_deref(), Some("Just A Title"));
assert_eq!(meta.artist, None);
assert_eq!(meta.album, None);
}
#[test]
fn no_mlit_container_yields_none() {
assert_eq!(parse(&entry(b"minm", b"loose")), None);
assert_eq!(parse(b""), None);
assert_eq!(parse(b"not dmap at all"), None);
}
#[test]
fn truncated_entries_end_the_walk_without_losing_earlier_fields() {
let mut body = mlit(&[entry(b"minm", b"Song")]);
body.extend_from_slice(b"asar");
body.extend_from_slice(&100u32.to_be_bytes()); assert_eq!(parse(&body).unwrap().title.as_deref(), Some("Song"));
let mut inner = entry(b"minm", b"Song");
inner.extend_from_slice(b"asal");
inner.extend_from_slice(&100u32.to_be_bytes());
let meta = parse(&entry(b"mlit", &inner)).unwrap();
assert_eq!(meta.title.as_deref(), Some("Song"));
assert_eq!(meta.album, None);
}
#[test]
fn invalid_utf8_is_converted_lossily() {
let meta = parse(&mlit(&[entry(b"minm", &[0xff, b'A'])])).unwrap();
assert_eq!(meta.title.as_deref(), Some("\u{fffd}A"));
}
}