389 lines
12 KiB
Go
389 lines
12 KiB
Go
package library
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
// Why this file exists at all: github.com/dhowden/tag reads every other field
|
|
// we need correctly, but its text-frame reader destroys multi-value frames.
|
|
// readTFrame does
|
|
//
|
|
// strings.Join(strings.Split(txt, string(singleZero)), "")
|
|
//
|
|
// — it splits on the ID3v2 null separator and rejoins with the EMPTY string, so
|
|
// a file tagged "Alternative Rock" + "Rock" comes back as the single token
|
|
// "Alternative RockRock" (#2499). We stored that verbatim, which corrupted the
|
|
// genre browse axis and polluted the taste profile's tag vocabulary.
|
|
//
|
|
// ffprobe is not an escape hatch either: ffmpeg's read_ttag calls decode_str
|
|
// exactly once with no loop, so it keeps only the FIRST value and silently
|
|
// discards the rest. Truncating multi-genre tags would blunt genre similarity,
|
|
// which is the main thing genre feeds.
|
|
//
|
|
// So the TCON frame is parsed here directly. Only the genre frame — everything
|
|
// else still comes from dhowden/tag, which handles it fine.
|
|
|
|
// maxID3TagSize caps how much of a file we'll buffer looking for TCON. Real
|
|
// tags are kilobytes; embedded cover art pushes them to a few megabytes. The
|
|
// cap exists so a corrupt or hostile size field can't make the scanner
|
|
// allocate wildly on a file it was only asked to index.
|
|
const maxID3TagSize = 16 << 20
|
|
|
|
// errNoGenreFrame means the file carries no readable genre frame. It is an
|
|
// expected outcome (plenty of files are untagged), not a failure.
|
|
var errNoGenreFrame = errors.New("library: no ID3v2 genre frame")
|
|
|
|
// readID3v2GenreValues returns the raw, still-unnormalised values of the ID3v2
|
|
// genre frame — one entry per value the tag actually declares. Numeric ID3v1
|
|
// references are left alone here; normaliseGenreValue resolves them.
|
|
//
|
|
// rs is seeked to the start, so it is safe to call after dhowden/tag has
|
|
// already consumed the reader.
|
|
func readID3v2GenreValues(rs io.ReadSeeker) ([]string, error) {
|
|
if _, err := rs.Seek(0, io.SeekStart); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var hdr [10]byte
|
|
if _, err := io.ReadFull(rs, hdr[:]); err != nil {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
if string(hdr[0:3]) != "ID3" {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
major := hdr[3]
|
|
// 2.2, 2.3 and 2.4 are the versions in the wild. A future 2.5 would very
|
|
// likely move the frame layout, so refuse rather than misparse it.
|
|
if major < 2 || major > 4 {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
tagFlags := hdr[5]
|
|
size := syncsafeInt(hdr[6:10])
|
|
if size <= 0 || size > maxID3TagSize {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
|
|
body := make([]byte, size)
|
|
if _, err := io.ReadFull(rs, body); err != nil {
|
|
// A truncated tag is still worth parsing as far as it goes — frame
|
|
// walking stops cleanly at the end of what we managed to read.
|
|
return nil, errNoGenreFrame
|
|
}
|
|
|
|
// 2.2 used flag 0x40 for whole-tag compression with a scheme that was
|
|
// never actually specified. Nothing can read those.
|
|
if major == 2 && tagFlags&0x40 != 0 {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
if tagFlags&0x80 != 0 {
|
|
// Whole-tag unsynchronisation (2.2/2.3). 2.4 moved this per-frame, but
|
|
// some writers still set it at tag level, and undoing it twice is
|
|
// harmless: after the first pass no 0xFF 0x00 pairs remain.
|
|
body = undoUnsynchronisation(body)
|
|
}
|
|
if major >= 3 && tagFlags&0x40 != 0 {
|
|
var ok bool
|
|
if body, ok = skipExtendedHeader(body, major); !ok {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
}
|
|
|
|
return findGenreFrame(body, major)
|
|
}
|
|
|
|
// findGenreFrame walks the frame list and decodes the genre frame's values.
|
|
func findGenreFrame(body []byte, major byte) ([]string, error) {
|
|
// 2.2 frames: 3-byte id + 3-byte size, no flags. 2.3/2.4: 4-byte id +
|
|
// 4-byte size + 2-byte flags. The size field is the other difference that
|
|
// matters — see frameSize.
|
|
idLen, sizeLen, flagLen := 4, 4, 2
|
|
wantID := "TCON"
|
|
if major == 2 {
|
|
idLen, sizeLen, flagLen = 3, 3, 0
|
|
wantID = "TCO"
|
|
}
|
|
hdrLen := idLen + sizeLen + flagLen
|
|
|
|
for off := 0; off+hdrLen <= len(body); {
|
|
id := string(body[off : off+idLen])
|
|
// A zero byte where a frame id belongs means we've reached the padding
|
|
// that fills out the tag. Everything after it is zeros.
|
|
if body[off] == 0 {
|
|
break
|
|
}
|
|
size := frameSize(body[off+idLen:off+idLen+sizeLen], major)
|
|
if size <= 0 || off+hdrLen+size > len(body) {
|
|
// Bogus length — we can't trust any offset past this point.
|
|
break
|
|
}
|
|
if id == wantID {
|
|
var flags uint16
|
|
if flagLen == 2 {
|
|
flags = binary.BigEndian.Uint16(body[off+idLen+sizeLen : off+hdrLen])
|
|
}
|
|
data, ok := frameData(body[off+hdrLen:off+hdrLen+size], major, flags)
|
|
if !ok {
|
|
return nil, errNoGenreFrame
|
|
}
|
|
return decodeTextValues(data), nil
|
|
}
|
|
off += hdrLen + size
|
|
}
|
|
return nil, errNoGenreFrame
|
|
}
|
|
|
|
// frameSize decodes a frame's length field. 2.4 made it syncsafe (7 bits per
|
|
// byte); 2.2 and 2.3 are plain big-endian. Reading a 2.3 size as syncsafe (or
|
|
// the reverse) yields a plausible-looking wrong offset rather than an obvious
|
|
// error, which is exactly how frame-walking bugs go unnoticed.
|
|
func frameSize(b []byte, major byte) int {
|
|
switch major {
|
|
case 2:
|
|
return int(b[0])<<16 | int(b[1])<<8 | int(b[2])
|
|
case 3:
|
|
n := binary.BigEndian.Uint32(b)
|
|
if n > maxID3TagSize {
|
|
return -1
|
|
}
|
|
return int(n)
|
|
default:
|
|
return syncsafeInt(b)
|
|
}
|
|
}
|
|
|
|
// frameData strips per-frame wrappers and reports whether the payload is
|
|
// readable at all. Compressed and encrypted frames are not (we have no
|
|
// zlib-in-frame or key handling, and neither is meaningful for a genre tag).
|
|
func frameData(data []byte, major byte, flags uint16) ([]byte, bool) {
|
|
if major == 3 {
|
|
// 2.3 flags: %abc00000 %ijk00000 — i compression, j encryption,
|
|
// k grouping.
|
|
if flags&0x0080 != 0 || flags&0x0040 != 0 {
|
|
return nil, false
|
|
}
|
|
if flags&0x0020 != 0 {
|
|
if len(data) < 1 {
|
|
return nil, false
|
|
}
|
|
data = data[1:] // group identifier
|
|
}
|
|
return data, true
|
|
}
|
|
if major == 4 {
|
|
// 2.4 flags: %0abc0000 %0h00kmnp — h grouping, k compression,
|
|
// m encryption, n unsynchronisation, p data-length indicator.
|
|
if flags&0x0008 != 0 || flags&0x0004 != 0 {
|
|
return nil, false
|
|
}
|
|
if flags&0x0040 != 0 {
|
|
if len(data) < 1 {
|
|
return nil, false
|
|
}
|
|
data = data[1:]
|
|
}
|
|
if flags&0x0001 != 0 {
|
|
if len(data) < 4 {
|
|
return nil, false
|
|
}
|
|
data = data[4:] // syncsafe expanded size; we don't need it
|
|
}
|
|
if flags&0x0002 != 0 {
|
|
data = undoUnsynchronisation(data)
|
|
}
|
|
return data, true
|
|
}
|
|
return data, true // 2.2 has no frame flags
|
|
}
|
|
|
|
// decodeTextValues splits a text frame's payload into its individual values and
|
|
// decodes each according to the frame's encoding byte.
|
|
//
|
|
// This is the whole point of the file: ID3v2 separates multiple values in one
|
|
// text frame with a null, and that separator is two bytes wide for the UTF-16
|
|
// encodings. Splitting a UTF-16 payload on single nulls would cut every ASCII
|
|
// character in half.
|
|
func decodeTextValues(data []byte) []string {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
encoding := data[0]
|
|
payload := data[1:]
|
|
|
|
switch encoding {
|
|
case 0: // ISO-8859-1
|
|
return mapChunks(splitOnNul(payload, 1), decodeLatin1)
|
|
case 3: // UTF-8
|
|
return mapChunks(splitOnNul(payload, 1), func(b []byte) string { return string(b) })
|
|
case 1, 2: // UTF-16 with BOM / UTF-16BE without
|
|
chunks := splitOnNul(payload, 2)
|
|
// Encoding 2 is big-endian by definition. Encoding 1 carries a byte
|
|
// order mark, which the spec says must appear on EVERY value in a
|
|
// multi-value frame — but writers that emit one only on the first value
|
|
// are common. Take the first BOM found as the default for values that
|
|
// lack their own, otherwise everything after the first value decodes
|
|
// byte-swapped into CJK gibberish.
|
|
defaultBE := true
|
|
if encoding == 1 {
|
|
for _, c := range chunks {
|
|
if be, ok := bomOrder(c); ok {
|
|
defaultBE = be
|
|
break
|
|
}
|
|
}
|
|
}
|
|
out := make([]string, 0, len(chunks))
|
|
for _, c := range chunks {
|
|
be := defaultBE
|
|
if encoding == 1 {
|
|
if o, ok := bomOrder(c); ok {
|
|
be, c = o, c[2:]
|
|
}
|
|
}
|
|
if s := strings.TrimSpace(decodeUTF16(c, be)); s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
default:
|
|
// Unknown encoding byte. Treating it as Latin-1 recovers ASCII text,
|
|
// which is better than dropping the frame.
|
|
return mapChunks(splitOnNul(payload, 1), decodeLatin1)
|
|
}
|
|
}
|
|
|
|
// splitOnNul splits on a null of the given width, honouring alignment so a
|
|
// 2-byte-wide separator can't match across a character boundary.
|
|
func splitOnNul(b []byte, width int) [][]byte {
|
|
var out [][]byte
|
|
start := 0
|
|
for i := 0; i+width <= len(b); i += width {
|
|
if !isNul(b[i : i+width]) {
|
|
continue
|
|
}
|
|
out = append(out, b[start:i])
|
|
start = i + width
|
|
}
|
|
if start < len(b) {
|
|
out = append(out, b[start:])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isNul(b []byte) bool {
|
|
for _, c := range b {
|
|
if c != 0 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func mapChunks(chunks [][]byte, decode func([]byte) string) []string {
|
|
out := make([]string, 0, len(chunks))
|
|
for _, c := range chunks {
|
|
if s := strings.TrimSpace(decode(c)); s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// decodeLatin1 widens ISO-8859-1 bytes to runes. A plain string() conversion
|
|
// would treat the bytes as UTF-8 and mangle every accented character.
|
|
func decodeLatin1(b []byte) string {
|
|
runes := make([]rune, len(b))
|
|
for i, c := range b {
|
|
runes[i] = rune(c)
|
|
}
|
|
return string(runes)
|
|
}
|
|
|
|
// bomOrder reports the byte order a UTF-16 byte-order mark declares, and
|
|
// whether one is present at all.
|
|
func bomOrder(b []byte) (bigEndian, ok bool) {
|
|
if len(b) < 2 {
|
|
return false, false
|
|
}
|
|
switch {
|
|
case b[0] == 0xFE && b[1] == 0xFF:
|
|
return true, true
|
|
case b[0] == 0xFF && b[1] == 0xFE:
|
|
return false, true
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// decodeUTF16 decodes UTF-16 code units in the given byte order. Any BOM has
|
|
// already been consumed by the caller.
|
|
func decodeUTF16(b []byte, bigEndian bool) string {
|
|
if len(b) < 2 {
|
|
return ""
|
|
}
|
|
units := make([]uint16, 0, len(b)/2)
|
|
for i := 0; i+1 < len(b); i += 2 {
|
|
if bigEndian {
|
|
units = append(units, uint16(b[i])<<8|uint16(b[i+1]))
|
|
} else {
|
|
units = append(units, uint16(b[i+1])<<8|uint16(b[i]))
|
|
}
|
|
}
|
|
return string(utf16.Decode(units))
|
|
}
|
|
|
|
// skipExtendedHeader advances past the optional extended header. The two
|
|
// versions disagree about whether the size field counts itself, which is worth
|
|
// spelling out because getting it wrong offsets the entire frame list by four
|
|
// bytes and makes every frame id look like padding.
|
|
func skipExtendedHeader(body []byte, major byte) ([]byte, bool) {
|
|
if len(body) < 4 {
|
|
return nil, false
|
|
}
|
|
if major == 3 {
|
|
// 2.3: size EXCLUDES the four size bytes themselves.
|
|
size := int(binary.BigEndian.Uint32(body[0:4]))
|
|
if size < 0 || 4+size > len(body) {
|
|
return nil, false
|
|
}
|
|
return body[4+size:], true
|
|
}
|
|
// 2.4: syncsafe size INCLUDING the size bytes.
|
|
size := syncsafeInt(body[0:4])
|
|
if size < 4 || size > len(body) {
|
|
return nil, false
|
|
}
|
|
return body[size:], true
|
|
}
|
|
|
|
// syncsafeInt decodes a 4-byte synchsafe integer (7 significant bits per byte).
|
|
func syncsafeInt(b []byte) int {
|
|
if len(b) < 4 {
|
|
return -1
|
|
}
|
|
// A set high bit means this isn't a valid synchsafe integer. Some writers
|
|
// emit a plain big-endian size here; refusing is safer than silently
|
|
// dropping bits and walking to a wrong offset.
|
|
for _, c := range b[:4] {
|
|
if c&0x80 != 0 {
|
|
return -1
|
|
}
|
|
}
|
|
return int(b[0])<<21 | int(b[1])<<14 | int(b[2])<<7 | int(b[3])
|
|
}
|
|
|
|
// undoUnsynchronisation collapses the 0xFF 0x00 pairs that unsynchronisation
|
|
// inserts to stop a tag from looking like an MPEG frame sync.
|
|
func undoUnsynchronisation(b []byte) []byte {
|
|
out := make([]byte, 0, len(b))
|
|
for i := 0; i < len(b); i++ {
|
|
out = append(out, b[i])
|
|
if b[i] == 0xFF && i+1 < len(b) && b[i+1] == 0x00 {
|
|
i++
|
|
}
|
|
}
|
|
return out
|
|
}
|