package rating import ( "bytes" "encoding/binary" "os" "testing" ) // buildTaggedMP3 builds a synthetic MP3 with a real ID3v2 tag. // - ver: 3 or 4 (ID3v2 minor version) // - frames: pre-built frame bytes (full frame including 10-byte frame header) // - paddingLen: bytes of zero padding inside the tag // - audio: payload after the tag // // The returned bytes are: 10-byte ID3 header | frames | padding | audio. func buildTaggedMP3(ver byte, frames []byte, paddingLen int, audio []byte) []byte { tagSize := len(frames) + paddingLen var hdr [10]byte hdr[0], hdr[1], hdr[2] = 'I', 'D', '3' hdr[3] = ver hdr[4] = 0 hdr[5] = 0 // no flags s := uint32(tagSize) hdr[6] = byte((s >> 21) & 0x7F) hdr[7] = byte((s >> 14) & 0x7F) hdr[8] = byte((s >> 7) & 0x7F) hdr[9] = byte(s & 0x7F) out := make([]byte, 0, 10+tagSize+len(audio)) out = append(out, hdr[:]...) out = append(out, frames...) out = append(out, make([]byte, paddingLen)...) out = append(out, audio...) return out } // makePOPMFrame builds a POPM frame matching ver's size encoding. func makePOPMFrame(ver byte, email string, rating byte, counter []byte) []byte { dataLen := len(email) + 1 + 1 + len(counter) out := make([]byte, 10+dataLen) copy(out[0:4], "POPM") switch ver { case 3: binary.BigEndian.PutUint32(out[4:8], uint32(dataLen)) case 4: s := uint32(dataLen) out[4] = byte((s >> 21) & 0x7F) out[5] = byte((s >> 14) & 0x7F) out[6] = byte((s >> 7) & 0x7F) out[7] = byte(s & 0x7F) } copy(out[10:], email) out[10+len(email)] = 0 out[10+len(email)+1] = rating copy(out[10+len(email)+2:], counter) return out } func fakeAudio(n int) []byte { a := make([]byte, n) for i := range a { a[i] = byte((i * 7) & 0xFF) } return a } // --- TESTS: patch path with existing POPM (1-byte write) --------------------- func TestPatch_existingPOPM_v23(t *testing.T) { for _, stars := range []int{1, 2, 3, 4, 5, 0} { t.Run("", func(t *testing.T) { // Use a starting rating (200) that doesn't match any starsToPOPM value // so we always observe a 1-byte change regardless of target stars. frame := makePOPMFrame(3, "rating@winamp.com", 200, []byte{0, 0, 0, 0}) audio := fakeAudio(2048) data := buildTaggedMP3(3, frame, 512, audio) path := writeTempMP3(t, data) before, _ := os.ReadFile(path) if err := tryPatchPOPM(path, stars); err != nil { t.Fatalf("tryPatchPOPM: %v", err) } after, _ := os.ReadFile(path) if len(before) != len(after) { t.Fatalf("file size changed: %d → %d", len(before), len(after)) } // Exactly ONE byte should differ — the rating byte. diffCount := 0 diffOff := -1 for i := range before { if before[i] != after[i] { diffCount++ diffOff = i } } if diffCount != 1 { t.Fatalf("expected exactly 1 byte changed, got %d", diffCount) } // The rating byte sits at: 10 (header) + 10 (frame hdr) + len(email)+1 wantOff := 10 + 10 + len("rating@winamp.com") + 1 if diffOff != wantOff { t.Errorf("changed byte at %d, expected at %d", diffOff, wantOff) } if after[diffOff] != starsToPOPM(stars) { t.Errorf("rating byte = %d, want %d", after[diffOff], starsToPOPM(stars)) } // Audio region untouched. audioStart := 10 + 10 + 23 + 512 // hdr + frame + padding if !bytes.Equal(after[audioStart:], audio) { t.Error("audio data corrupted") } // Get() should reflect the new rating got, _ := Get(path) if got != stars { t.Errorf("Get() = %d, want %d", got, stars) } }) } } func TestPatch_existingPOPM_v24(t *testing.T) { frame := makePOPMFrame(4, "rating@winamp.com", 1, []byte{0, 0, 0, 0}) audio := fakeAudio(1024) data := buildTaggedMP3(4, frame, 256, audio) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 5); err != nil { t.Fatalf("tryPatchPOPM: %v", err) } got, _ := Get(path) if got != 5 { t.Errorf("Get() = %d, want 5", got) } } // --- TESTS: patch path inserts new POPM into padding ------------------------- func TestPatch_insertIntoPadding(t *testing.T) { audio := fakeAudio(1024) data := buildTaggedMP3(3, nil, 1024, audio) // empty tag body, all padding path := writeTempMP3(t, data) beforeSize := int64(len(data)) if err := tryPatchPOPM(path, 4); err != nil { t.Fatalf("tryPatchPOPM: %v", err) } st, _ := os.Stat(path) if st.Size() != beforeSize { t.Fatalf("file size changed: %d → %d", beforeSize, st.Size()) } got, err := Get(path) if err != nil { t.Fatalf("Get: %v", err) } if got != 4 { t.Errorf("Get() = %d, want 4", got) } // Audio untouched. after, _ := os.ReadFile(path) audioStart := 10 + 1024 if !bytes.Equal(after[audioStart:], audio) { t.Error("audio data corrupted") } } // --- TESTS: patch path refuses safely on unsupported inputs ------------------ func TestPatch_refusesNoID3(t *testing.T) { path := writeTempMP3(t, fakeAudio(2048)) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } func TestPatch_refusesUnsyncFlag(t *testing.T) { data := buildTaggedMP3(3, nil, 1024, fakeAudio(512)) data[5] = 0x80 // set unsync flag path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } func TestPatch_refusesUnknownVersion(t *testing.T) { data := buildTaggedMP3(2, nil, 1024, fakeAudio(512)) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } func TestPatch_refusesPaddingTooSmall(t *testing.T) { // No POPM, only 5 bytes padding — new frame needs ~33 bytes. data := buildTaggedMP3(3, nil, 5, fakeAudio(512)) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } func TestPatch_refusesPOPMWithFlags(t *testing.T) { frame := makePOPMFrame(3, "rating@winamp.com", 128, []byte{0, 0, 0, 0}) frame[9] = 0x01 // set a frame flag data := buildTaggedMP3(3, frame, 256, fakeAudio(512)) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } func TestPatch_refusesInvalidFrameID(t *testing.T) { // Garbage frame ID (lowercase invalid). bad := []byte{'a', 'b', 'c', 'd', 0, 0, 0, 4, 0, 0, 0, 0, 0, 0} data := buildTaggedMP3(3, bad, 256, fakeAudio(512)) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } func TestPatch_refusesPOPMOversizedSize(t *testing.T) { // POPM with size larger than what fits in the tag — must refuse, not panic. frame := makePOPMFrame(3, "rating@winamp.com", 128, []byte{0, 0, 0, 0}) binary.BigEndian.PutUint32(frame[4:8], 999999) // lie about size data := buildTaggedMP3(3, frame, 256, fakeAudio(512)) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 3); err != errPatchRefused { t.Errorf("got %v, want errPatchRefused", err) } } // --- TESTS: stars=0 semantics in patch path ---------------------------------- func TestPatch_starsZeroWithNoFrame_noWrite(t *testing.T) { data := buildTaggedMP3(3, nil, 1024, fakeAudio(512)) path := writeTempMP3(t, data) before, _ := os.ReadFile(path) if err := tryPatchPOPM(path, 0); err != nil { t.Fatalf("unexpected: %v", err) } after, _ := os.ReadFile(path) if !bytes.Equal(before, after) { t.Error("file changed even though stars=0 and no existing frame") } } // --- TESTS: walks past non-POPM frames correctly ---------------------------- func TestPatch_skipsOtherFrames(t *testing.T) { // TIT2 (title) frame first, then POPM. Encoding byte 0x00 = ISO-8859-1. tit2Data := append([]byte{0x00}, "Test Title"...) tit2 := make([]byte, 10+len(tit2Data)) copy(tit2[0:4], "TIT2") binary.BigEndian.PutUint32(tit2[4:8], uint32(len(tit2Data))) copy(tit2[10:], tit2Data) popm := makePOPMFrame(3, "rating@winamp.com", 1, []byte{0, 0, 0, 0}) frames := append(tit2, popm...) audio := fakeAudio(1024) data := buildTaggedMP3(3, frames, 512, audio) path := writeTempMP3(t, data) if err := tryPatchPOPM(path, 5); err != nil { t.Fatalf("tryPatchPOPM: %v", err) } got, _ := Get(path) if got != 5 { t.Errorf("Get() = %d, want 5", got) } // Audio untouched. after, _ := os.ReadFile(path) audioStart := 10 + len(frames) + 512 if !bytes.Equal(after[audioStart:], audio) { t.Error("audio data corrupted") } } // --- TESTS: Set() integration uses patch when it can ------------------------ func TestSet_usesPatchPathWhenPossible(t *testing.T) { // File has padding → patch should succeed → audio MUST remain byte-identical. audio := fakeAudio(4096) data := buildTaggedMP3(3, nil, 2048, audio) path := writeTempMP3(t, data) beforeSize := int64(len(data)) if err := Set(path, 3); err != nil { t.Fatalf("Set: %v", err) } st, _ := os.Stat(path) if st.Size() != beforeSize { t.Errorf("Set() with available padding changed file size: %d → %d (patch path should be used)", beforeSize, st.Size()) } got, _ := Get(path) if got != 3 { t.Errorf("Get() = %d, want 3", got) } }