mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-13 13:29:09 +02:00
Parse tags and artwork with one bounded frame walker and use a combined MP3 scan on cold cover caches. Skip artwork for tag-only reads and skip further pictures after finding a usable cover. Preserve earlier fields when trailing frames are malformed. Cover ID3 versions, extended headers, unsynchronization, malformed data, and large artwork with byte-read assertions.
200 lines
5.5 KiB
Go
200 lines
5.5 KiB
Go
package gobackend
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// A malformed frame must not allocate an entire declared tag (up to 256 MiB).
|
|
// Artwork is optional; tag-only callers seek past it without allocating it.
|
|
const maxID3FrameBytes = 32 << 20
|
|
|
|
func readID3v2WithCover(file io.ReadSeeker, includeCover bool) (*AudioMetadata, []byte, string, error) {
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
var header [10]byte
|
|
if _, err := io.ReadFull(file, header[:]); err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
if string(header[:3]) != "ID3" {
|
|
return nil, nil, "", fmt.Errorf("no ID3v2 header")
|
|
}
|
|
version, flags := header[3], header[5]
|
|
if version < 2 || version > 4 || header[6]|header[7]|header[8]|header[9] >= 128 {
|
|
return nil, nil, "", fmt.Errorf("invalid ID3 version or tag size")
|
|
}
|
|
size := int64(syncsafeToInt(header[6:10]))
|
|
end, err := file.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
if size > end-10 {
|
|
return nil, nil, "", io.ErrUnexpectedEOF
|
|
}
|
|
if _, err := file.Seek(10, io.SeekStart); err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
var reader io.Reader = file
|
|
// ID3v2.2/2.3 unsynchronization covers the entire tag, including headers;
|
|
// decode it as a bounded stream so large pictures still need no allocation.
|
|
if flags&0x80 != 0 && version < 4 {
|
|
reader = &id3UnsyncReader{source: bufio.NewReader(io.LimitReader(file, size))}
|
|
}
|
|
if flags&0x40 != 0 {
|
|
if version == 2 {
|
|
return nil, nil, "", fmt.Errorf("compressed ID3v2.2 tag unsupported")
|
|
}
|
|
var extended [4]byte
|
|
if _, err := io.ReadFull(reader, extended[:]); err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
length := int64(binary.BigEndian.Uint32(extended[:]))
|
|
if version == 4 {
|
|
length = int64(syncsafeToInt(extended[:])) - 4
|
|
}
|
|
if length < 0 || length > size-4 {
|
|
return nil, nil, "", fmt.Errorf("invalid ID3 extended header")
|
|
}
|
|
if err := skipID3Bytes(reader, length); err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
size -= length + 4
|
|
}
|
|
metadata := &AudioMetadata{}
|
|
var cover []byte
|
|
var mime string
|
|
err = walkID3Frames(reader, size, version, version == 4 && flags&0x80 != 0, func() bool { return includeCover && len(cover) == 0 },
|
|
func(id string, data []byte) {
|
|
if id == "APIC" || id == "PIC" {
|
|
if len(cover) == 0 {
|
|
cover, mime = parseAPICFrame(data, version)
|
|
}
|
|
} else {
|
|
applyID3Frame(metadata, version, id, data)
|
|
}
|
|
})
|
|
return metadata, cover, mime, err
|
|
}
|
|
|
|
// walkID3Frames is shared by metadata, cover extraction and combined scanning.
|
|
// Only selected frame payloads are read; the enclosing tag bounds every seek.
|
|
func walkID3Frames(reader io.Reader, remaining int64, version byte, tagUnsync bool, wantCover func() bool, visit func(string, []byte)) error {
|
|
headerLength, idLength := 10, 4
|
|
if version == 2 {
|
|
headerLength, idLength = 6, 3
|
|
}
|
|
var header [10]byte
|
|
for remaining >= int64(headerLength) {
|
|
if count, err := io.ReadFull(reader, header[:headerLength]); err != nil {
|
|
// A globally unsynchronized tag can have fewer decoded bytes than its
|
|
// raw size; exhaustion between frames is normal.
|
|
if err == io.EOF {
|
|
return nil
|
|
}
|
|
if err == io.ErrUnexpectedEOF {
|
|
padding := true
|
|
for _, value := range header[:count] {
|
|
if value != 0 {
|
|
padding = false
|
|
break
|
|
}
|
|
}
|
|
if padding {
|
|
return nil
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
remaining -= int64(headerLength)
|
|
if header[0] == 0 || string(header[:3]) == "3DI" {
|
|
return nil
|
|
}
|
|
id := string(header[:idLength])
|
|
var size int64
|
|
switch version {
|
|
case 2:
|
|
size = int64(header[3])<<16 | int64(header[4])<<8 | int64(header[5])
|
|
case 4:
|
|
if header[4]|header[5]|header[6]|header[7] >= 128 {
|
|
return fmt.Errorf("invalid ID3 frame size")
|
|
}
|
|
size = int64(syncsafeToInt(header[4:8]))
|
|
default:
|
|
size = int64(binary.BigEndian.Uint32(header[4:8]))
|
|
}
|
|
if size <= 0 || size > remaining {
|
|
return fmt.Errorf("invalid ID3 frame bounds")
|
|
}
|
|
remaining -= size
|
|
picture := id == "APIC" || id == "PIC"
|
|
wanted := (picture && wantCover != nil && wantCover()) || (!picture && (id[0] == 'T' || id == "COMM" || id == "USLT" || id == "ULT"))
|
|
flags := byte(0)
|
|
if version != 2 {
|
|
flags = header[9]
|
|
}
|
|
unsupported := (version == 3 && flags&0xc0 != 0) || (version == 4 && flags&0x0c != 0)
|
|
if !wanted || unsupported || size > maxID3FrameBytes {
|
|
if err := skipID3Bytes(reader, size); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
data := make([]byte, int(size))
|
|
if _, err := io.ReadFull(reader, data); err != nil {
|
|
return err
|
|
}
|
|
if version == 3 && flags&0x20 != 0 || version == 4 && flags&0x40 != 0 {
|
|
if len(data) < 1 {
|
|
continue
|
|
}
|
|
data = data[1:]
|
|
}
|
|
if version == 4 && flags&0x01 != 0 {
|
|
if len(data) < 4 {
|
|
continue
|
|
}
|
|
data = data[4:]
|
|
}
|
|
if tagUnsync || version == 4 && flags&0x02 != 0 {
|
|
data = removeUnsync(data)
|
|
}
|
|
visit(id, data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func skipID3Bytes(reader io.Reader, size int64) error {
|
|
if seeker, ok := reader.(io.Seeker); ok {
|
|
_, err := seeker.Seek(size, io.SeekCurrent)
|
|
return err
|
|
}
|
|
_, err := io.CopyN(io.Discard, reader, size)
|
|
return err
|
|
}
|
|
|
|
type id3UnsyncReader struct {
|
|
source *bufio.Reader
|
|
afterFF bool
|
|
}
|
|
|
|
func (reader *id3UnsyncReader) Read(output []byte) (int, error) {
|
|
count := 0
|
|
for count < len(output) {
|
|
value, err := reader.source.ReadByte()
|
|
if err != nil {
|
|
return count, err
|
|
}
|
|
if reader.afterFF && value == 0 {
|
|
reader.afterFF = false
|
|
continue
|
|
}
|
|
reader.afterFF = value == 0xff
|
|
output[count] = value
|
|
count++
|
|
}
|
|
return count, nil
|
|
}
|