|
|
@ -8,24 +8,177 @@ import ( |
|
|
|
"github.com/Unbewohnte/id3ed/util" |
|
|
|
"github.com/Unbewohnte/id3ed/util" |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2") |
|
|
|
// Main header`s flags
|
|
|
|
|
|
|
|
|
|
|
|
type HeaderFlags struct { |
|
|
|
type HeaderFlags struct { |
|
|
|
Unsynchronisated bool |
|
|
|
Unsynchronisated bool |
|
|
|
|
|
|
|
Compressed bool |
|
|
|
HasExtendedHeader bool |
|
|
|
HasExtendedHeader bool |
|
|
|
Experimental bool |
|
|
|
Experimental bool |
|
|
|
FooterPresent bool |
|
|
|
FooterPresent bool |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// ID3v2.x header structure
|
|
|
|
// ID3v2.x`s main header structure
|
|
|
|
type Header struct { |
|
|
|
type Header struct { |
|
|
|
Identifier string |
|
|
|
Identifier string |
|
|
|
Flags HeaderFlags |
|
|
|
Flags HeaderFlags |
|
|
|
Version string |
|
|
|
Version string |
|
|
|
Size uint32 |
|
|
|
Size uint32 |
|
|
|
|
|
|
|
ExtendedHeader ExtendedHeader |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// extended header`s flags
|
|
|
|
|
|
|
|
type ExtendedHeaderFlags struct { |
|
|
|
|
|
|
|
UpdateTag bool |
|
|
|
|
|
|
|
CRCpresent bool |
|
|
|
|
|
|
|
HasRestrictions bool |
|
|
|
|
|
|
|
Restrictions byte // a `lazy` approach :), just for now, maybe...
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type ExtendedHeader struct { |
|
|
|
|
|
|
|
Size uint32 |
|
|
|
|
|
|
|
Flags ExtendedHeaderFlags |
|
|
|
|
|
|
|
PaddingSize uint32 |
|
|
|
|
|
|
|
CRCdata []byte |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Reads and structuralises extended header. Must
|
|
|
|
|
|
|
|
// be called AFTER the main header has beeen read (does not seek).
|
|
|
|
|
|
|
|
// ALSO ISN`T TESTED !!!
|
|
|
|
|
|
|
|
func (h *Header) readExtendedHeader(r io.Reader) error { |
|
|
|
|
|
|
|
h.ExtendedHeader = ExtendedHeader{} |
|
|
|
|
|
|
|
if !h.Flags.HasExtendedHeader { |
|
|
|
|
|
|
|
return nil |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var extended ExtendedHeader |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// extended size
|
|
|
|
|
|
|
|
extendedSize, err := util.Read(r, 4) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch h.Version { |
|
|
|
|
|
|
|
case V2_3: |
|
|
|
|
|
|
|
extended.Size = util.BytesToInt(extendedSize) |
|
|
|
|
|
|
|
case V2_4: |
|
|
|
|
|
|
|
extended.Size = util.BytesToIntSynchsafe(extendedSize) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// extended flags
|
|
|
|
|
|
|
|
switch h.Version { |
|
|
|
|
|
|
|
case V2_3: |
|
|
|
|
|
|
|
extendedFlag, err := util.Read(r, 2) // reading flag byte and a null-byte after
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
flagbits := fmt.Sprintf("%08b", extendedFlag[0]) |
|
|
|
|
|
|
|
if flagbits[0] == 1 { |
|
|
|
|
|
|
|
extended.Flags.CRCpresent = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
extended.Flags.CRCpresent = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case V2_4: |
|
|
|
|
|
|
|
// skipping `Number of flag bytes` because it`s always `1`
|
|
|
|
|
|
|
|
_, err := util.Read(r, 1) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
flagByte, err := util.Read(r, 1) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
flagBits := fmt.Sprintf("%08b", flagByte[0]) |
|
|
|
|
|
|
|
if flagBits[1] == 1 { |
|
|
|
|
|
|
|
extended.Flags.UpdateTag = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
extended.Flags.UpdateTag = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if flagBits[2] == 1 { |
|
|
|
|
|
|
|
extended.Flags.CRCpresent = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
extended.Flags.CRCpresent = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if flagBits[3] == 1 { |
|
|
|
|
|
|
|
extended.Flags.HasRestrictions = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
extended.Flags.HasRestrictions = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// extracting data given by flags
|
|
|
|
|
|
|
|
switch h.Version { |
|
|
|
|
|
|
|
case V2_3: |
|
|
|
|
|
|
|
if extended.Flags.CRCpresent { |
|
|
|
|
|
|
|
crcData, err := util.Read(r, 4) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
extended.CRCdata = crcData |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
case V2_4: |
|
|
|
|
|
|
|
// `Each flag that is set in the extended header has data attached`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if extended.Flags.UpdateTag { |
|
|
|
|
|
|
|
// skipping null-byte length of `UpdateTag`
|
|
|
|
|
|
|
|
_, err := util.Read(r, 1) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if extended.Flags.CRCpresent { |
|
|
|
|
|
|
|
crclen, err := util.Read(r, 1) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
crcData, err := util.Read(r, uint64(crclen[0])) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
extended.CRCdata = crcData |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if extended.Flags.HasRestrictions { |
|
|
|
|
|
|
|
// skipping one-byte length of `Restrictions`, because it`s always `1`
|
|
|
|
|
|
|
|
_, err := util.Read(r, 1) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
restrictionsByte, err := util.Read(r, 1) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// a `lazy` approach :), just for now
|
|
|
|
|
|
|
|
extended.Flags.Restrictions = restrictionsByte[0] |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// extracting other version-dependent header data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// padding if V2_3
|
|
|
|
|
|
|
|
if h.Version == V2_3 { |
|
|
|
|
|
|
|
paddingSizeBytes, err := util.Read(r, 4) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return fmt.Errorf("could not read from reader: %s", err) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
paddingSize := util.BytesToInt(paddingSizeBytes) |
|
|
|
|
|
|
|
extended.PaddingSize = paddingSize |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// finally `attaching` parsed extended header to the main *Header
|
|
|
|
|
|
|
|
h.ExtendedHeader = extended |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Reads and structuralises ID3v2 header from given bytes.
|
|
|
|
// Reads and structuralises ID3v2 header and if present - extended header.
|
|
|
|
// Returns a blank header struct if encountered an error
|
|
|
|
// Returns a blank header struct if encountered an error
|
|
|
|
func readHeader(rs io.ReadSeeker) (Header, error) { |
|
|
|
func readHeader(rs io.ReadSeeker) (Header, error) { |
|
|
|
_, err := rs.Seek(0, io.SeekStart) |
|
|
|
_, err := rs.Seek(0, io.SeekStart) |
|
|
@ -49,14 +202,8 @@ func readHeader(rs io.ReadSeeker) (Header, error) { |
|
|
|
header.Identifier = string(identifier) |
|
|
|
header.Identifier = string(identifier) |
|
|
|
|
|
|
|
|
|
|
|
// version
|
|
|
|
// version
|
|
|
|
majorVersion, err := util.ByteToInt(hBytes[3]) |
|
|
|
majorVersion := int(hBytes[3]) |
|
|
|
if err != nil { |
|
|
|
revisionNumber := int(hBytes[4]) |
|
|
|
return Header{}, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
revisionNumber, err := util.ByteToInt(hBytes[4]) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return Header{}, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch majorVersion { |
|
|
|
switch majorVersion { |
|
|
|
case 2: |
|
|
|
case 2: |
|
|
@ -76,6 +223,17 @@ func readHeader(rs io.ReadSeeker) (Header, error) { |
|
|
|
|
|
|
|
|
|
|
|
// v3.0 and v4.0 have different amount of flags
|
|
|
|
// v3.0 and v4.0 have different amount of flags
|
|
|
|
switch header.Version { |
|
|
|
switch header.Version { |
|
|
|
|
|
|
|
case V2_2: |
|
|
|
|
|
|
|
if flagBits[0] == 1 { |
|
|
|
|
|
|
|
header.Flags.Unsynchronisated = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
header.Flags.Unsynchronisated = false |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if flagBits[1] == 1 { |
|
|
|
|
|
|
|
header.Flags.Compressed = true |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
header.Flags.Compressed = false |
|
|
|
|
|
|
|
} |
|
|
|
case V2_3: |
|
|
|
case V2_3: |
|
|
|
if flagBits[0] == 1 { |
|
|
|
if flagBits[0] == 1 { |
|
|
|
header.Flags.Unsynchronisated = true |
|
|
|
header.Flags.Unsynchronisated = true |
|
|
@ -121,9 +279,16 @@ func readHeader(rs io.ReadSeeker) (Header, error) { |
|
|
|
// size
|
|
|
|
// size
|
|
|
|
sizeBytes := hBytes[6:] |
|
|
|
sizeBytes := hBytes[6:] |
|
|
|
|
|
|
|
|
|
|
|
size := util.BytesToIntIgnoreFirstBit(sizeBytes) |
|
|
|
size := util.BytesToIntSynchsafe(sizeBytes) |
|
|
|
|
|
|
|
|
|
|
|
header.Size = size |
|
|
|
header.Size = size |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if header.Flags.HasExtendedHeader { |
|
|
|
|
|
|
|
err = header.readExtendedHeader(rs) |
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
|
|
|
return header, err |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return header, nil |
|
|
|
return header, nil |
|
|
|
} |
|
|
|
} |
|
|
|