The previous Set() always did a full tag+audio rewrite, even for changing
a single byte. That triggered the FILE_SHARE_DELETE rename dance and
required streaming kilobytes through a temp file just to flip a rating.
New strategy (tryPatchPOPM in popm.go):
- Existing POPM frame: seek + 1 byte write. Done.
- No POPM, padding available: write a fresh ~33-byte POPM frame into
the existing zero-padding. Tag size unchanged, audio offset unchanged,
no rename, works while Winamp holds the file open.
- Anything weird (no ID3 tag, header flags set, unknown version,
padding too small, malformed frame, POPM with frame flags): refuse
with errPatchRefused; Set() falls back to the existing rewrite path.
Safety invariants enforced before every write:
- Strict bounds: write region must lie inside [10, 10+tagSize).
- Padding region must be verified all-zero.
- Only ID3v2.3 and v2.4 with zero header flags accepted.
- POPM frame must have zero frame flags (rejects compression /
encryption / data-length indicator that would shift the rating
offset).
- All frame sizes are validated against tag bounds before use.
Tests (popm_test.go): patch existing POPM for both v2.3 and v2.4,
insert into padding, no-op when stars=0 and no frame, walk past TIT2
to find POPM, plus seven refusal cases (no ID3, unsync flag, unknown
version, small padding, POPM with flags, invalid frame ID, oversized
frame size). Also: integration test that Set() preserves file size
when padding is available, proving the patch path is taken.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>