package library import ( "bufio" "bytes" "encoding/binary" "errors" "io" "strings" ) // Why this file exists: Vorbis comments (FLAC, Ogg Vorbis, Opus) say a field // has several values by repeating it: // // GENRE=Boom Bap // GENRE=Downtempo // // dhowden/tag stores comments in a map keyed by field name // (vorbis.go: m.c[strings.ToLower(k)] = v), so each repeat overwrites the last // and only the final value survives. On the operator's library that was 3,658 // of 4,092 FLAC files: Kupla's Life Forms declares eight genres and was stored // as "Instrumental Hip Hop" alone (#2500). Nothing looked corrupt; the other // seven were simply never seen by browse or the taste profile. // // The comment block is read here directly, as the ID3v2 genre frame is in // id3v2genre.go. Every other field still comes from dhowden/tag. // maxVorbisCommentSize caps a comment block or packet. Comments are kilobytes; // an embedded METADATA_BLOCK_PICTURE can take them to a few megabytes. The cap // only stops a corrupt length from making the scanner allocate wildly. const maxVorbisCommentSize = 16 << 20 var errMalformedComment = errors.New("library: malformed Vorbis comment") // readFLACGenreValues returns every GENRE value in a FLAC file's comment block. // (nil, nil) means the file is readable and declares no genre. rs is seeked to // the start, so it is safe to call after dhowden/tag has consumed the reader. func readFLACGenreValues(rs io.ReadSeeker) ([]string, error) { if _, err := rs.Seek(0, io.SeekStart); err != nil { return nil, err } var magic [4]byte if _, err := io.ReadFull(rs, magic[:]); err != nil { return nil, errNoGenreFrame } // Some taggers prepend an ID3v2 tag to a FLAC. The FLAC stream starts after it. if string(magic[:3]) == "ID3" { var rest [6]byte if _, err := io.ReadFull(rs, rest[:]); err != nil { return nil, errNoGenreFrame } size := syncsafeInt(rest[2:6]) if size < 0 { return nil, errNoGenreFrame } skip := int64(10 + size) if rest[1]&0x10 != 0 { skip += 10 // 2.4 footer } if _, err := rs.Seek(skip, io.SeekStart); err != nil { return nil, errNoGenreFrame } if _, err := io.ReadFull(rs, magic[:]); err != nil { return nil, errNoGenreFrame } } if string(magic[:]) != "fLaC" { return nil, errNoGenreFrame } for { var hdr [4]byte if _, err := io.ReadFull(rs, hdr[:]); err != nil { return nil, errNoGenreFrame } last := hdr[0]&0x80 != 0 blockType := hdr[0] & 0x7F size := int64(hdr[1])<<16 | int64(hdr[2])<<8 | int64(hdr[3]) switch blockType { case 4: // VORBIS_COMMENT body := make([]byte, size) if _, err := io.ReadFull(rs, body); err != nil { return nil, errNoGenreFrame } return vorbisCommentGenres(body) case 127: // invalid by spec; nothing after it can be trusted return nil, errNoGenreFrame } if last { return nil, nil // no comment block at all } if _, err := rs.Seek(size, io.SeekCurrent); err != nil { return nil, errNoGenreFrame } } } // readOggGenreValues returns every GENRE value in an Ogg Vorbis or Opus file. // The comments are the stream's second packet, which can span several pages // when it carries cover art, so pages are reassembled until it is complete. // Only the first logical stream is read; pages of any other are skipped. func readOggGenreValues(rs io.ReadSeeker) ([]string, error) { if _, err := rs.Seek(0, io.SeekStart); err != nil { return nil, err } br := bufio.NewReader(rs) var ( serial uint32 haveFirst bool packet []byte packetIdx int ) for { var hdr [27]byte if _, err := io.ReadFull(br, hdr[:]); err != nil { return nil, errNoGenreFrame } if string(hdr[0:4]) != "OggS" { return nil, errNoGenreFrame } pageSerial := binary.LittleEndian.Uint32(hdr[14:18]) segments := make([]byte, hdr[26]) if _, err := io.ReadFull(br, segments); err != nil { return nil, errNoGenreFrame } if !haveFirst { serial, haveFirst = pageSerial, true } if pageSerial != serial { var n int64 for _, l := range segments { n += int64(l) } if _, err := io.CopyN(io.Discard, br, n); err != nil { return nil, errNoGenreFrame } continue } for _, l := range segments { if len(packet)+int(l) > maxVorbisCommentSize { return nil, errNoGenreFrame } seg := make([]byte, l) if _, err := io.ReadFull(br, seg); err != nil { return nil, errNoGenreFrame } packet = append(packet, seg...) if l == 255 { continue // the packet carries on in the next segment } if packetIdx == 1 { return oggCommentGenres(packet) } packetIdx++ packet = packet[:0] } } } // oggCommentGenres strips the codec's comment-packet prefix. Ogg FLAC and Speex // are rare enough to leave to dhowden/tag. func oggCommentGenres(packet []byte) ([]string, error) { switch { case bytes.HasPrefix(packet, []byte("\x03vorbis")): return vorbisCommentGenres(packet[7:]) case bytes.HasPrefix(packet, []byte("OpusTags")): return vorbisCommentGenres(packet[8:]) } return nil, errNoGenreFrame } // vorbisCommentGenres reads a comment block (vendor string, then a count of // length-prefixed "KEY=value" entries, little-endian lengths throughout) and // returns the GENRE values in file order. Field names are case-insensitive by // spec. Anything after the last entry, such as Vorbis's framing bit, is ignored. func vorbisCommentGenres(b []byte) ([]string, error) { next := func() ([]byte, bool) { if len(b) < 4 { return nil, false } n := binary.LittleEndian.Uint32(b) if uint64(n) > uint64(len(b)-4) { return nil, false } field := b[4 : 4+n] b = b[4+n:] return field, true } if _, ok := next(); !ok { // vendor string return nil, errMalformedComment } if len(b) < 4 { return nil, errMalformedComment } count := binary.LittleEndian.Uint32(b) b = b[4:] var out []string // Each entry costs at least four bytes, so a corrupt count runs out of // input long before it runs up a loop. for i := uint32(0); i < count; i++ { field, ok := next() if !ok { return nil, errMalformedComment } key, value, found := strings.Cut(string(field), "=") if !found || !strings.EqualFold(key, "GENRE") { continue } if value = strings.TrimSpace(value); value != "" { out = append(out, value) } } return out, nil }