tag/sum.go

221 lines
4.8 KiB
Go
Raw Normal View History

2015-03-19 13:21:53 +01:00
package tag
import (
"crypto/sha1"
"encoding/binary"
2015-09-20 12:09:21 +02:00
"errors"
2015-03-19 13:21:53 +01:00
"fmt"
"hash"
2015-03-19 13:21:53 +01:00
"io"
"os"
)
// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
// (ID3, MP4) invariant.
func Sum(r io.ReadSeeker) (string, error) {
2015-03-19 13:21:53 +01:00
b, err := readBytes(r, 11)
if err != nil {
return "", err
}
_, err = r.Seek(-11, os.SEEK_CUR)
if err != nil {
return "", fmt.Errorf("could not seek back to original position: %v", err)
}
switch {
case string(b[0:4]) == "fLaC":
2015-09-20 12:09:21 +02:00
return SumFLAC(r)
case string(b[4:11]) == "ftypM4A":
return SumAtoms(r)
2015-03-19 13:21:53 +01:00
case string(b[0:3]) == "ID3":
return SumID3v2(r)
2015-03-19 13:21:53 +01:00
}
h, err := SumID3v1(r)
2015-03-19 13:21:53 +01:00
if err != nil {
if err == ErrNotID3v1 {
return SumAll(r)
2015-03-19 13:21:53 +01:00
}
return "", err
}
return h, nil
}
// SumAll returns a checksum of the content from the reader (until EOF).
func SumAll(r io.ReadSeeker) (string, error) {
h := sha1.New()
_, err := io.Copy(h, r)
2015-03-19 13:21:53 +01:00
if err != nil {
return "", nil
}
return hashSum(h), nil
2015-03-19 13:21:53 +01:00
}
// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is
// metadata invariant.
func SumAtoms(r io.ReadSeeker) (string, error) {
2015-03-19 13:21:53 +01:00
for {
var size uint32
err := binary.Read(r, binary.BigEndian, &size)
if err != nil {
if err == io.EOF {
return "", fmt.Errorf("reached EOF before audio data")
}
return "", err
}
name, err := readString(r, 4)
if err != nil {
return "", err
}
switch name {
case "meta":
// next_item_id (int32)
_, err := r.Seek(4, os.SEEK_CUR)
2015-03-19 13:21:53 +01:00
if err != nil {
return "", err
}
fallthrough
case "moov", "udta", "ilst":
2015-06-30 15:05:59 +02:00
continue
2015-03-19 13:21:53 +01:00
case "mdat": // stop when we get to the data
h := sha1.New()
_, err := io.CopyN(h, r, int64(size-8))
2015-03-19 13:21:53 +01:00
if err != nil {
return "", fmt.Errorf("error reading audio data: %v", err)
}
return hashSum(h), nil
2015-03-19 13:21:53 +01:00
}
_, err = r.Seek(int64(size-8), os.SEEK_CUR)
if err != nil {
return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
}
}
}
func sizeToEndOffset(r io.ReadSeeker, offset int64) (int64, error) {
n, err := r.Seek(-128, os.SEEK_END)
if err != nil {
return 0, fmt.Errorf("error seeking end offset (%d bytes): %v", offset, err)
}
_, err = r.Seek(-n, os.SEEK_CUR)
if err != nil {
return 0, fmt.Errorf("error seeking back to original position: %v", err)
}
return n, nil
}
// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided
// by the io.ReadSeeker which is metadata invariant.
func SumID3v1(r io.ReadSeeker) (string, error) {
n, err := sizeToEndOffset(r, 128)
2015-03-19 13:21:53 +01:00
if err != nil {
return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
}
// TODO: improve this check???
if n <= 0 {
return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3")
2015-03-19 13:21:53 +01:00
}
h := sha1.New()
_, err = io.CopyN(h, r, n)
if err != nil {
return "", fmt.Errorf("error reading %v bytes: %v", n, err)
2015-03-19 13:21:53 +01:00
}
return hashSum(h), nil
2015-03-19 13:21:53 +01:00
}
2015-04-16 00:36:35 +02:00
// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
// io.ReadSeeker which is metadata invariant.
func SumID3v2(r io.ReadSeeker) (string, error) {
header, err := readID3v2Header(r)
2015-03-19 13:21:53 +01:00
if err != nil {
return "", fmt.Errorf("error reading ID3v2 header: %v", err)
}
_, err = r.Seek(int64(header.Size), os.SEEK_CUR)
2015-03-19 13:21:53 +01:00
if err != nil {
return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
}
n, err := sizeToEndOffset(r, 128)
2015-03-19 13:21:53 +01:00
if err != nil {
return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
2015-03-19 13:21:53 +01:00
}
// TODO: remove this check?????
if n < 0 {
return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n)
}
h := sha1.New()
_, err = io.CopyN(h, r, n)
if err != nil {
return "", fmt.Errorf("error reading %v bytes: %v", n, err)
2015-03-19 13:21:53 +01:00
}
return hashSum(h), nil
2015-03-19 13:21:53 +01:00
}
2015-09-20 12:09:21 +02:00
// SumFLAC costructs a checksum of the FLAC audio file data provided by the io.ReadSeeker (ignores
// metadata fields).
func SumFLAC(r io.ReadSeeker) (string, error) {
flac, err := readString(r, 4)
if err != nil {
return "", err
}
if flac != "fLaC" {
return "", errors.New("expected 'fLaC'")
}
for {
last, err := skipFLACMetadataBlock(r)
if err != nil {
return "", err
}
if last {
break
}
}
h := sha1.New()
_, err = io.Copy(h, r)
if err != nil {
return "", fmt.Errorf("error reading data bytes from FLAC: %v", err)
}
return hashSum(h), nil
}
func skipFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
blockHeader, err := readBytes(r, 1)
if err != nil {
return
}
if getBit(blockHeader[0], 7) {
blockHeader[0] ^= (1 << 7)
last = true
}
blockLen, err := readInt(r, 3)
if err != nil {
return
}
_, err = r.Seek(int64(blockLen), os.SEEK_CUR)
return
}
func hashSum(h hash.Hash) string {
return fmt.Sprintf("%x", h.Sum([]byte{}))
2015-03-19 13:21:53 +01:00
}