From db26a173f8e915edc858d5dfe6f704144a030a6d Mon Sep 17 00:00:00 2001 From: Unbewohnte Date: Thu, 15 Jul 2021 15:10:30 +0300 Subject: [PATCH] =?UTF-8?q?=E2=97=BE=20Procrastinated=20really=20hard=20on?= =?UTF-8?q?=20ID3v2=20writing=20support...=20=E2=97=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- util/conversion.go | 25 +++--- util/read.go | 8 +- v1/constants.go | 5 +- v1/errors.go | 7 ++ v1/read.go | 118 ++++++++++++++++++++++++--- v1/tag.go | 29 +++++-- v2/errors.go | 7 ++ v2/frame.go | 4 +- v2/header.go | 199 +++++++++++++++++++++++++++++++++++++++++---- 9 files changed, 347 insertions(+), 55 deletions(-) create mode 100644 v1/errors.go create mode 100644 v2/errors.go diff --git a/util/conversion.go b/util/conversion.go index 927616e..df45918 100644 --- a/util/conversion.go +++ b/util/conversion.go @@ -1,8 +1,6 @@ package util import ( - "fmt" - "strconv" "strings" euni "golang.org/x/text/encoding/unicode" @@ -12,18 +10,19 @@ import ( const first7BitsMask = uint32(254) << 24 // shifting 11111110 to the end of uint32 -// Decodes given byte into integer -func ByteToInt(gByte byte) (int, error) { - integer, err := strconv.Atoi(fmt.Sprintf("%d", gByte)) - if err != nil { - return 0, err +// Converts given bytes into integer +func BytesToInt(gBytes []byte) uint32 { + var integer uint32 = 0 + for _, b := range gBytes { + integer = integer << 8 + integer = integer | uint32(b) } - return integer, nil + return integer } // Decodes given integer bytes into integer, ignores the first bit // of every given byte in binary form -func BytesToIntIgnoreFirstBit(gBytes []byte) uint32 { +func BytesToIntSynchsafe(gBytes []byte) uint32 { var integer uint32 = 0 for _, b := range gBytes { integer = integer << 7 @@ -34,14 +33,14 @@ func BytesToIntIgnoreFirstBit(gBytes []byte) uint32 { } // The exact opposite of what `BytesToIntIgnoreFirstBit` does -func IntToBytesFirstBitZeroed(gInt uint32) []byte { +func SynchsafeIntToBytes(gInt uint32) []byte { bytes := make([]byte, 32) // looping 4 times (32 bits / 8 bits (4 bytes in int32)) for i := 0; i < 32; i += 8 { - gIntCopy := gInt //ie: 11010100 11001011 00100000 10111111 - first7 := gIntCopy & first7BitsMask - shifted := first7 >> 25 // 00000000 00000000 00000000 01101010 + gIntCopy := gInt //11010101 11001011 00100000 10111111 + first7 := gIntCopy & first7BitsMask //11010100 00000000 00000000 00000000 + shifted := first7 >> 25 //00000000 00000000 00000000 01101010 bytes = append(bytes, byte(shifted)) } diff --git a/util/read.go b/util/read.go index 3a648c5..6969088 100644 --- a/util/read.go +++ b/util/read.go @@ -6,9 +6,9 @@ import ( ) // Shortcut function to read n bytes from reader. The general idea peeked from here: https://github.com/dhowden/tag/blob/master/util.go -func Read(rs io.Reader, n uint64) ([]byte, error) { +func Read(r io.Reader, n uint64) ([]byte, error) { read := make([]byte, n) - _, err := rs.Read(read) + _, err := r.Read(read) if err != nil { return nil, fmt.Errorf("could not read from reader: %s", err) } @@ -18,9 +18,9 @@ func Read(rs io.Reader, n uint64) ([]byte, error) { // Reads from rs and conversts read []byte into string, ignoring all non-printable or // invalid characters. -func ReadToString(rs io.Reader, n uint64) (string, error) { +func ReadToString(r io.Reader, n uint64) (string, error) { read := make([]byte, n) - _, err := rs.Read(read) + _, err := r.Read(read) if err != nil { return "", fmt.Errorf("could not read from reader: %s", err) } diff --git a/v1/constants.go b/v1/constants.go index 5281e2e..22d9612 100644 --- a/v1/constants.go +++ b/v1/constants.go @@ -1,8 +1,11 @@ package v1 const ID3v1IDENTIFIER string = "TAG" -const ID3v1SIZE int = 128 // bytes +const ID3v1SIZE int = 128 const ID3v1INVALIDGENRE int = 255 +const ID3v1ENHANCEDIDENTIFIER string = "TAG+" +const ENHANCEDSIZE int = 227 + const V1_0 string = "ID3v1.0" const V1_1 string = "ID3v1.1" diff --git a/v1/errors.go b/v1/errors.go new file mode 100644 index 0000000..2f08032 --- /dev/null +++ b/v1/errors.go @@ -0,0 +1,7 @@ +package v1 + +// Exported ID3v1-specific errors + +import "fmt" + +var ErrDoesNotUseID3v1 error = fmt.Errorf("does not use ID3v1") diff --git a/v1/read.go b/v1/read.go index ede85ba..61ac896 100644 --- a/v1/read.go +++ b/v1/read.go @@ -9,12 +9,114 @@ import ( "github.com/Unbewohnte/id3ed/util" ) -var ErrDoesNotUseID3v1 error = fmt.Errorf("does not use ID3v1") +var errDoesNotUseEnhancedID3v1 error = fmt.Errorf("does not use enhanced ID3v1 tag") -// Retrieves ID3v1 field values of provided io.ReadSeeker (usually a file) +// Checks if enhanced tag is used +func usesEnhancedTag(rs io.ReadSeeker) bool { + _, err := rs.Seek(-int64(ID3v1SIZE+ENHANCEDSIZE), io.SeekEnd) + if err != nil { + return false + } + identifier, err := util.Read(rs, 4) + if err != nil { + return false + } + if !bytes.Equal(identifier, []byte(ID3v1ENHANCEDIDENTIFIER)) { + return false + } + + return true +} + +// Tries to read enhanced ID3V1 tag from rs +func readEnhancedTag(rs io.ReadSeeker) (EnhancedID3v1Tag, error) { + + if !usesEnhancedTag(rs) { + return EnhancedID3v1Tag{}, errDoesNotUseEnhancedID3v1 + } + + var enhanced EnhancedID3v1Tag + + // set reader into the position + _, err := rs.Seek(-int64(ID3v1SIZE+ENHANCEDSIZE), io.SeekEnd) + if err != nil { + return enhanced, fmt.Errorf("could not seek: %s", err) + } + + // songname + songName, err := util.ReadToString(rs, 60) + if err != nil { + return EnhancedID3v1Tag{}, err + } + enhanced.SongName = songName + + artist, err := util.ReadToString(rs, 60) + if err != nil { + return enhanced, err + } + enhanced.Artist = artist + + // album + album, err := util.ReadToString(rs, 60) + if err != nil { + return enhanced, err + } + enhanced.Album = album + + // speed + speedByte, err := util.Read(rs, 1) + if err != nil { + return enhanced, err + } + + var speed string + switch speedByte[0] { + case 0: + speed = "Unset" + case 1: + speed = "Slow" + case 2: + speed = "Medium" + case 3: + speed = "Fast" + case 4: + speed = "Hardcore" + } + enhanced.Speed = speed + + // genre + genre, err := util.ReadToString(rs, 30) + if err != nil { + return enhanced, err + } + enhanced.Genre = genre + + // time + startTime, err := util.ReadToString(rs, 6) + if err != nil { + return enhanced, err + } + enhanced.StartTime = startTime + + endtime, err := util.ReadToString(rs, 6) + if err != nil { + return enhanced, err + } + enhanced.EndTime = endtime + + return enhanced, nil +} + +// Retrieves ID3v1 field values from rs. func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { var tag ID3v1Tag + // check if uses enhanced tag + if usesEnhancedTag(rs) { + enhanced, _ := readEnhancedTag(rs) + tag.EnhancedTag = enhanced + } + // set reader to the last 128 bytes _, err := rs.Seek(-int64(ID3v1SIZE), io.SeekEnd) if err != nil { @@ -76,10 +178,8 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { // check if 29th byte is null byte (v1.0 or v1.1) if comment[28] == 0 { // it is v1.1, track number exists - track, err = util.ByteToInt(comment[29]) - if err != nil { - return nil, fmt.Errorf("could not get int from byte: %s", err) - } + track = int(comment[29]) + tag.Track = uint8(track) comment = comment[0:28] @@ -91,10 +191,8 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { if err != nil { return nil, err } - genreInt, err := util.ByteToInt(genreByte[0]) - if err != nil { - return nil, fmt.Errorf("cannot convert bytes to int: %s", err) - } + genreInt := int(genreByte[0]) + genre, exists := id3v1genres[int(genreInt)] if !exists { genre = "" diff --git a/v1/tag.go b/v1/tag.go index a2c71da..e6179b0 100644 --- a/v1/tag.go +++ b/v1/tag.go @@ -3,12 +3,25 @@ package v1 // https://id3.org/ID3v1 - documentation type ID3v1Tag struct { - version string - SongName string - Artist string - Album string - Year int - Comment string - Track uint8 // basically a byte, but converted to int for convenience - Genre string + version string + SongName string + Artist string + Album string + Year int + Comment string + Track uint8 // basically a byte, but converted to int for convenience + Genre string + EnhancedTag EnhancedID3v1Tag +} + +// from https://en.wikipedia.org/wiki/ID3 + +type EnhancedID3v1Tag struct { + SongName string + Artist string + Album string + Speed string + Genre string + StartTime string + EndTime string } diff --git a/v2/errors.go b/v2/errors.go new file mode 100644 index 0000000..5488397 --- /dev/null +++ b/v2/errors.go @@ -0,0 +1,7 @@ +package v2 + +// Exported ID3v2-specific errors + +import "fmt" + +var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2") diff --git a/v2/frame.go b/v2/frame.go index dcfb4d8..d7738c9 100644 --- a/v2/frame.go +++ b/v2/frame.go @@ -63,7 +63,7 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { } header.ID = string(fHeaderbytes[0:3]) - framesizeBytes := util.BytesToIntIgnoreFirstBit(fHeaderbytes[3:6]) + framesizeBytes := util.BytesToIntSynchsafe(fHeaderbytes[3:6]) header.Size = framesizeBytes case V2_3: @@ -82,7 +82,7 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { // Size framesizeBytes := fHeaderbytes[4:8] - framesize := util.BytesToIntIgnoreFirstBit(framesizeBytes) + framesize := util.BytesToIntSynchsafe(framesizeBytes) header.Size = framesize diff --git a/v2/header.go b/v2/header.go index b468553..6ec1b51 100644 --- a/v2/header.go +++ b/v2/header.go @@ -8,24 +8,177 @@ import ( "github.com/Unbewohnte/id3ed/util" ) -var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2") - +// Main header`s flags type HeaderFlags struct { Unsynchronisated bool + Compressed bool HasExtendedHeader bool Experimental bool FooterPresent bool } -// ID3v2.x header structure +// ID3v2.x`s main header structure type Header struct { - Identifier string - Flags HeaderFlags - Version string - Size uint32 + Identifier string + Flags HeaderFlags + Version string + 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 func readHeader(rs io.ReadSeeker) (Header, error) { _, err := rs.Seek(0, io.SeekStart) @@ -49,14 +202,8 @@ func readHeader(rs io.ReadSeeker) (Header, error) { header.Identifier = string(identifier) // version - majorVersion, err := util.ByteToInt(hBytes[3]) - if err != nil { - return Header{}, err - } - revisionNumber, err := util.ByteToInt(hBytes[4]) - if err != nil { - return Header{}, err - } + majorVersion := int(hBytes[3]) + revisionNumber := int(hBytes[4]) switch majorVersion { case 2: @@ -76,6 +223,17 @@ func readHeader(rs io.ReadSeeker) (Header, error) { // v3.0 and v4.0 have different amount of flags 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: if flagBits[0] == 1 { header.Flags.Unsynchronisated = true @@ -121,9 +279,16 @@ func readHeader(rs io.ReadSeeker) (Header, error) { // size sizeBytes := hBytes[6:] - size := util.BytesToIntIgnoreFirstBit(sizeBytes) + size := util.BytesToIntSynchsafe(sizeBytes) header.Size = size + if header.Flags.HasExtendedHeader { + err = header.readExtendedHeader(rs) + if err != nil { + return header, err + } + } + return header, nil }