From 4b577493b1ff53c8bb980f263d40d4cf700e562b Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 4 Aug 2026 06:42:40 -0400 Subject: [PATCH] feat(13-04): tag extraction across mp3/flac/m4a/ogg with media-root confinement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes Task 3 on top of the recovered wip checkpoint (be8f24b4): - fix the programmatically-generated MP3 fixture's frame length: lofty's Header::read computes samples*bitrate*125/sample_rate with truncating integer division BEFORE adding the padding byte, so the FF FB 52 C4 frame is 209 bytes, not 210 — the off-by-one made cmp_header miss the second frame sync and reject the whole file as containing an invalid frame (mp3_id3v24_yields_full_record now passes; fixture-only fix, production code untouched) - all 7 music::tags tests green; no binary audio fixtures committed (fixtures are built byte-by-byte into tempdirs at test run time) - extract_tags canonicalizes and confines to caller-supplied media_roots before opening any file (T-13-20); non-audio is a distinct NotAudio error vs the Ok/has_tags=false untagged fallback (T-13-21) - entity types in music/mod.rs implement 13-MUSIC-MODEL.md exactly: hybrid-identity TrackId, derived albums/artists, MUSIC_SCHEMA_VERSION=1 Co-Authored-By: Claude Fable 5 --- core/archipelago/src/music/tags.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/core/archipelago/src/music/tags.rs b/core/archipelago/src/music/tags.rs index 5997da21..ff415635 100644 --- a/core/archipelago/src/music/tags.rs +++ b/core/archipelago/src/music/tags.rs @@ -165,10 +165,16 @@ mod tests { /// 64kbps, 44100Hz, mono, padded — the exact byte pattern lofty's own /// test suite uses as a known-good frame header /// (`mpeg/header.rs::tests::rev_search_for_frame_header`). Its computed - /// frame length (samples * bitrate * 125 / sample_rate + padding) is - /// 210 bytes. + /// frame length matches lofty's `Header::read` exactly: + /// `samples * bitrate * 125 / sample_rate` truncates *before* the + /// padding byte is added — `1152 * 64 * 125 / 44100 = 208`, `+ 1` + /// padding = **209** bytes. The frames written below must be exactly + /// this long, because lofty confirms a frame sync by comparing the + /// candidate header against the bytes found exactly `len` later + /// (`find_next_frame`/`cmp_header`); an off-by-one frame length makes + /// the whole file "contain an invalid frame". const MP3_FRAME_HEADER: [u8; 4] = [0xFF, 0xFB, 0x52, 0xC4]; - const MP3_FRAME_LEN: usize = 210; + const MP3_FRAME_LEN: usize = 209; fn mp3_frame() -> Vec { let mut frame = MP3_FRAME_HEADER.to_vec();