package library import ( "encoding/binary" "errors" "io" "strings" ) // MP4 has the same gap as Vorbis comments (#2500). A tag writer stores several // genres as several "data" atoms inside one ©gen atom (mutagen and Picard do), // or occasionally as repeated ©gen atoms. dhowden/tag keeps atoms in a map keyed // by name (mp4.go: m.data[name] = data), so one value survives either way. // // Path to the values: moov > udta > meta > ilst > ©gen > data. A file that // carries only the numeric "gnre" atom has no ©gen; this reader returns // (nil, nil) for it, and extractGenres falls back to dhowden/tag, which resolves // gnre to a name. // maxMP4GenreAtomSize caps how much of one ©gen atom is buffered. Genre text is // bytes; the cap only stops a corrupt size from making the scanner allocate. const maxMP4GenreAtomSize = 1 << 20 var errMalformedAtom = errors.New("library: malformed MP4 atom") type mp4Atom struct { kind string body, limit int64 // body start and end offsets in the file } // readMP4Atom reads the atom header at pos, which must lie within [pos, limit). func readMP4Atom(rs io.ReadSeeker, pos, limit int64) (mp4Atom, error) { if _, err := rs.Seek(pos, io.SeekStart); err != nil { return mp4Atom{}, err } var hdr [8]byte if _, err := io.ReadFull(rs, hdr[:]); err != nil { return mp4Atom{}, errMalformedAtom } size := int64(binary.BigEndian.Uint32(hdr[0:4])) hdrLen := int64(8) switch size { case 0: // runs to the end of the enclosing atom size = limit - pos case 1: // 64-bit size follows the type var large [8]byte if _, err := io.ReadFull(rs, large[:]); err != nil { return mp4Atom{}, errMalformedAtom } size = int64(binary.BigEndian.Uint64(large[:])) hdrLen = 16 } if size < hdrLen || size > limit-pos { return mp4Atom{}, errMalformedAtom } return mp4Atom{kind: string(hdr[4:8]), body: pos + hdrLen, limit: pos + size}, nil } // findMP4Child returns the first child of the given kind in [start, end). func findMP4Child(rs io.ReadSeeker, start, end int64, kind string) (mp4Atom, bool, error) { for pos := start; pos+8 <= end; { a, err := readMP4Atom(rs, pos, end) if err != nil { return mp4Atom{}, false, err } if a.kind == kind { return a, true, nil } pos = a.limit } return mp4Atom{}, false, nil } // readMP4GenreValues returns every ©gen value, in file order. rs is seeked // as needed, so it is safe to call after dhowden/tag has consumed the reader. func readMP4GenreValues(rs io.ReadSeeker) ([]string, error) { end, err := rs.Seek(0, io.SeekEnd) if err != nil { return nil, err } // moov may sit after mdat; walking headers and seeking past bodies finds // it either way without reading the audio. cur := mp4Atom{body: 0, limit: end} for _, kind := range []string{"moov", "udta", "meta"} { next, found, err := findMP4Child(rs, cur.body, cur.limit, kind) if err != nil { return nil, errNoGenreFrame } if !found { return nil, nil // no iTunes metadata at all } cur = next } // meta is a "full box" with four bytes of version and flags before its // children in the ISO layout. QuickTime-style files omit them, so the // first child (hdlr) starts straight away. Look before skipping. if _, err := rs.Seek(cur.body, io.SeekStart); err != nil { return nil, errNoGenreFrame } var peek [8]byte if _, err := io.ReadFull(rs, peek[:]); err != nil { return nil, errNoGenreFrame } if string(peek[4:8]) != "hdlr" { cur.body += 4 } ilst, found, err := findMP4Child(rs, cur.body, cur.limit, "ilst") if err != nil { return nil, errNoGenreFrame } if !found { return nil, nil } var out []string for pos := ilst.body; pos+8 <= ilst.limit; { a, err := readMP4Atom(rs, pos, ilst.limit) if err != nil { return nil, errNoGenreFrame } pos = a.limit if a.kind != "\xa9gen" { continue } n := a.limit - a.body if n > maxMP4GenreAtomSize { return nil, errNoGenreFrame } if _, err := rs.Seek(a.body, io.SeekStart); err != nil { return nil, errNoGenreFrame } body := make([]byte, n) if _, err := io.ReadFull(rs, body); err != nil { return nil, errNoGenreFrame } values, ok := mp4DataValues(body) if !ok { return nil, errNoGenreFrame } out = append(out, values...) } return out, nil } // mp4DataValues reads the "data" atoms in a ©gen body. Each is: size, "data", // a 4-byte type indicator (1 = UTF-8 text), a 4-byte locale, then the value. // Atoms of other types carry no text and are skipped. func mp4DataValues(b []byte) ([]string, bool) { var out []string for len(b) >= 8 { size := binary.BigEndian.Uint32(b[0:4]) if size < 8 || uint64(size) > uint64(len(b)) { return nil, false } atom := b[:size] b = b[size:] if string(atom[4:8]) != "data" || len(atom) < 16 { continue } if binary.BigEndian.Uint32(atom[8:12])&0x00FFFFFF != 1 { continue } if v := strings.TrimSpace(string(atom[16:])); v != "" { out = append(out, v) } } return out, true }