diff --git a/id3ed.go b/id3ed.go index eb2e325..738317b 100644 --- a/id3ed.go +++ b/id3ed.go @@ -1,11 +1,76 @@ package id3ed -// type File struct { -// ContainsV1 bool -// ContainsV2 bool -// ? -// } +import ( + "fmt" + "os" + + v1 "github.com/Unbewohnte/id3ed/v1" + v2 "github.com/Unbewohnte/id3ed/v2" +) + +type File struct { + path string + ContainsID3v1 bool + ContainsID3v2 bool + ID3v1Tag *v1.ID3v1Tag + ID3v2Tag *v2.ID3v2Tag +} + +// Opens file specified by `path`, reads all ID3 tags that +// it can find and returns a *File +func Open(path string) (*File, error) { + fhandler, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("could not open: %s", err) + } + defer fhandler.Close() + + var file File + + file.path = path // saving path to the file for future writing + + v1tag, err := v1.Readv1Tag(fhandler) + switch err { + case nil: + file.ContainsID3v1 = true + case v1.ErrDoesNotUseID3v1: + file.ContainsID3v1 = false + default: + return nil, fmt.Errorf("could not read ID3v1 tag from file: %s", err) + } + file.ID3v1Tag = v1tag + + v2tag, err := v2.ReadV2Tag(fhandler) + switch err { + case nil: + file.ContainsID3v2 = true + case v2.ErrDoesNotUseID3v2: + file.ContainsID3v2 = false + default: + return nil, fmt.Errorf("could not read ID3v2 tag from file: %s", err) + } + file.ID3v2Tag = v2tag + + return &file, nil +} + +// Writes given ID3v1 tag to file +func (f *File) WriteID3v1(tag *v1.ID3v1Tag) error { + fhandler, err := os.OpenFile(f.path, os.O_RDWR, os.ModePerm) + if err != nil { + return fmt.Errorf("could not read a file: %s", err) + } + defer fhandler.Close() + + err = tag.WriteToFile(fhandler) + if err != nil { + return fmt.Errorf("could not write ID3v1 to file: %s", err) + } + + return nil +} -// func Open(path string) (*File, error) { -// return &File{}, nil +// still not implemented +// func (f *File) WriteID3v2(tag *v2.ID3v2Tag) error { +// return nil // } diff --git a/id3ed_test.go b/id3ed_test.go new file mode 100644 index 0000000..f97be9b --- /dev/null +++ b/id3ed_test.go @@ -0,0 +1,45 @@ +package id3ed + +import ( + "path/filepath" + "testing" + + v1 "github.com/Unbewohnte/id3ed/v1" +) + +var TESTDATAPATH string = "testData" + +func TestOpen(t *testing.T) { + file, err := Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) + if err != nil { + t.Errorf("Open failed: %s", err) + } + + if file.ContainsID3v1 { + t.Error("Open failed: expected testing file to not contain ID3v1") + } + + if !file.ContainsID3v2 { + t.Error("Open failed: expected testing file to contain ID3v2") + } +} + +func TestWriteID3v1(t *testing.T) { + file, err := Open(filepath.Join(TESTDATAPATH, "testwritev1.mp3")) + if err != nil { + t.Errorf("Open failed: %s", err) + } + v1tag := &v1.ID3v1Tag{ + SongName: "testsong", + Artist: "testartist", + Album: "testalbum", + Year: 727, + Comment: "testcomment", + Genre: "Blues", + } + + err = file.WriteID3v1(v1tag) + if err != nil { + t.Errorf("WriteID3v1 failed: %s", err) + } +} diff --git a/testData/testwritev1.mp3 b/testData/testwritev1.mp3 index 0f5e9ce..064e8da 100644 Binary files a/testData/testwritev1.mp3 and b/testData/testwritev1.mp3 differ diff --git a/v1/read.go b/v1/read.go index 10685a0..ede85ba 100644 --- a/v1/read.go +++ b/v1/read.go @@ -9,6 +9,8 @@ import ( "github.com/Unbewohnte/id3ed/util" ) +var ErrDoesNotUseID3v1 error = fmt.Errorf("does not use ID3v1") + // Retrieves ID3v1 field values of provided io.ReadSeeker (usually a file) func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { var tag ID3v1Tag @@ -27,7 +29,7 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { if !bytes.Equal(identifier, []byte(ID3v1IDENTIFIER)) { // no identifier, given file does not use ID3v1 - return nil, fmt.Errorf("does not use ID3v1: expected %s; got %s", ID3v1IDENTIFIER, identifier) + return nil, ErrDoesNotUseID3v1 } // Songname @@ -100,9 +102,9 @@ func Readv1Tag(rs io.ReadSeeker) (*ID3v1Tag, error) { tag.Genre = genre if track == 0 { - tag.Version = V1_0 + tag.version = V1_0 } else { - tag.Version = V1_1 + tag.version = V1_1 } return &tag, nil diff --git a/v1/tag.go b/v1/tag.go index df12b5f..a2c71da 100644 --- a/v1/tag.go +++ b/v1/tag.go @@ -3,7 +3,7 @@ package v1 // https://id3.org/ID3v1 - documentation type ID3v1Tag struct { - Version string + version string SongName string Artist string Album string diff --git a/v1/v1_test.go b/v1/v1_test.go index ae13047..c9317a1 100644 --- a/v1/v1_test.go +++ b/v1/v1_test.go @@ -27,8 +27,8 @@ func TestReadv1Tag(t *testing.T) { t.Errorf("GetID3v1Tag failed: %s", err) } - if tag.Version != V1_1 { - t.Errorf("GetID3v1Tag failed: expected version to be %s; got %s", V1_1, tag.Version) + if tag.version != V1_1 { + t.Errorf("GetID3v1Tag failed: expected version to be %s; got %s", V1_1, tag.version) } if tag.Comment != "Comment here " { @@ -44,35 +44,35 @@ func TestReadv1Tag(t *testing.T) { } } -func TestWritev1Tags(t *testing.T) { - f, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev1.mp3"), os.O_CREATE|os.O_RDWR, os.ModePerm) - if err != nil { - t.Errorf("%s", err) - } - defer f.Close() - - tag := TESTv1TAG - - // writing a tag - err = tag.write(f) - if err != nil { - t.Errorf("WriteID3v1Tag failed: %s", err) - } - - // reading a tag - readTag, err := Readv1Tag(f) - if err != nil { - t.Errorf("%s", err) - } - - if readTag.Album != "testalbum" { - t.Errorf("WriteID3v1Tag failed: expected %s; got %s", "testalbum", readTag.Album) - } - - if readTag.Year != 727 { - t.Errorf("WriteID3v1Tag failed: expected %d; got %d", 727, readTag.Year) - } -} +// func TestWritev1Tags(t *testing.T) { +// f, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev1.mp3"), os.O_CREATE|os.O_RDWR, os.ModePerm) +// if err != nil { +// t.Errorf("%s", err) +// } +// defer f.Close() + +// tag := TESTv1TAG + +// // writing a tag +// err = tag.write(f) +// if err != nil { +// t.Errorf("WriteID3v1Tag failed: %s", err) +// } + +// // reading a tag +// readTag, err := Readv1Tag(f) +// if err != nil { +// t.Errorf("%s", err) +// } + +// if readTag.Album != "testalbum" { +// t.Errorf("WriteID3v1Tag failed: expected %s; got %s", "testalbum", readTag.Album) +// } + +// if readTag.Year != 727 { +// t.Errorf("WriteID3v1Tag failed: expected %d; got %d", 727, readTag.Year) +// } +// } func TestWriteID3v1ToFile(t *testing.T) { f, err := os.OpenFile(filepath.Join(TESTDATAPATH, "testwritev1.mp3"), os.O_CREATE|os.O_RDWR, os.ModePerm) diff --git a/v1/write.go b/v1/write.go index 6c1fcc7..d04513f 100644 --- a/v1/write.go +++ b/v1/write.go @@ -14,10 +14,13 @@ import ( // NOTE: will not remove already existing ID3v1 tag if it`s present, // use ⁕WriteToFile⁕ method if you`re working with REAL mp3 files !!! func (tag *ID3v1Tag) write(dst io.WriteSeeker) error { - dst.Seek(0, io.SeekEnd) + _, err := dst.Seek(0, io.SeekEnd) + if err != nil { + return fmt.Errorf("could not seek: %s", err) + } // ID - _, err := dst.Write([]byte(ID3v1IDENTIFIER)) + _, err = dst.Write([]byte(ID3v1IDENTIFIER)) if err != nil { return fmt.Errorf("could not write to writer: %s", err) } diff --git a/v2/frame.go b/v2/frame.go index 9d1e4af..589ea4c 100644 --- a/v2/frame.go +++ b/v2/frame.go @@ -3,6 +3,7 @@ package v2 import ( "fmt" "io" + "strings" "github.com/Unbewohnte/id3ed/util" ) @@ -10,6 +11,7 @@ import ( var ErrGotPadding error = fmt.Errorf("got padding") var ErrBiggerThanSize error = fmt.Errorf("frame size is bigger than size of the whole tag") var ErrInvalidFHeaderSize error = fmt.Errorf("frame header must be 6 or 10 bytes long") +var ErrInvalidID error = fmt.Errorf("invalid identifier") type FrameFlags struct { TagAlterPreservation bool @@ -31,6 +33,20 @@ type Frame struct { Contents []byte } +// Checks if provided frame identifier is valid by +// its length and presence of invalid characters. +func isValidFrameID(frameID []byte) bool { + if len(frameID) != 3 && len(frameID) != 4 { + return false + } + str := strings.ToValidUTF8(string(frameID), "invalidChar") + if len(str) != 3 && len(str) != 4 { + return false + } + + return true +} + // Structuralises frame header from given bytes. For versions see: constants. func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { // validation check @@ -42,6 +58,9 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { switch version { case V2_2: + if !isValidFrameID(fHeaderbytes[0:3]) { + return FrameHeader{}, ErrInvalidID + } header.ID = string(fHeaderbytes[0:3]) framesizeBytes, err := util.BytesToIntIgnoreFirstBit(fHeaderbytes[3:6]) @@ -58,6 +77,9 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { default: // ID + if !isValidFrameID(fHeaderbytes[0:4]) { + return FrameHeader{}, ErrInvalidID + } header.ID = string(fHeaderbytes[0:4]) // Size @@ -120,7 +142,7 @@ func getFrameHeader(fHeaderbytes []byte, version string) (FrameHeader, error) { // Reads ID3v2.3.0 or ID3v2.4.0 frame from given frame bytes. // Returns a blank Frame struct if encountered an error, amount of // bytes read from io.Reader. -func ReadNextFrame(r io.Reader, h Header) (Frame, uint64, error) { +func readNextFrame(r io.Reader, h Header) (Frame, uint64, error) { var frame Frame var read uint64 = 0 @@ -133,7 +155,7 @@ func ReadNextFrame(r io.Reader, h Header) (Frame, uint64, error) { read += uint64(HEADERSIZE) frameHeader, err := getFrameHeader(headerBytes, h.Version) - if err == ErrGotPadding { + if err == ErrGotPadding || err == ErrInvalidID { return Frame{}, read, err } else if err != nil { return Frame{}, read, fmt.Errorf("could not get header of a frame: %s", err) diff --git a/v2/frame_test.go b/v2/frame_test.go index 3243b4c..47b4d70 100644 --- a/v2/frame_test.go +++ b/v2/frame_test.go @@ -14,12 +14,12 @@ func TestReadNextFrame(t *testing.T) { t.Errorf("%s", err) } - header, err := ReadHeader(f) + header, err := readHeader(f) if err != nil { t.Errorf("%s", err) } - firstFrame, _, err := ReadNextFrame(f, header) + firstFrame, _, err := readNextFrame(f, header) if err != nil { t.Errorf("ReadFrame failed: %s", err) } @@ -34,7 +34,7 @@ func TestReadNextFrame(t *testing.T) { false, firstFrame.Header.Flags.Encrypted) } - secondFrame, _, err := ReadNextFrame(f, header) + secondFrame, _, err := readNextFrame(f, header) if err != nil { t.Errorf("ReadFrame failed: %s", err) } @@ -49,24 +49,3 @@ func TestReadNextFrame(t *testing.T) { "2006", secondFrame.Contents) } } - -// func TestGetFrames(t *testing.T) { -// f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) -// if err != nil { -// t.Errorf("%s", err) -// } - -// frames, err := GetFrames(f) -// if err != nil { -// t.Errorf("GetFrames failed: %s", err) -// } - -// titleFrame, ok := frames["TIT2"] -// if !ok { -// t.Errorf("GetFrames failed: no %s in frames", "TIT2") -// } - -// if util.ToStringLossy(titleFrame.Contents) != "title" { -// t.Errorf("GetFrames failed: expected title to be %s; got %s", "title", titleFrame.Contents) -// } -// } diff --git a/v2/header.go b/v2/header.go index c8feb34..70292c0 100644 --- a/v2/header.go +++ b/v2/header.go @@ -8,6 +8,8 @@ import ( "github.com/Unbewohnte/id3ed/util" ) +var ErrDoesNotUseID3v2 error = fmt.Errorf("does not use ID3v2") + type HeaderFlags struct { Unsynchronisated bool HasExtendedHeader bool @@ -25,7 +27,7 @@ type Header struct { // Reads and structuralises ID3v2 header from given bytes. // 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) if err != nil { return Header{}, fmt.Errorf("could not seek: %s", err) @@ -42,7 +44,7 @@ func ReadHeader(rs io.ReadSeeker) (Header, error) { // check if has identifier ID3v2 if !bytes.Equal([]byte(HEADERIDENTIFIER), identifier) { - return Header{}, fmt.Errorf("no ID3v2 identifier found") + return Header{}, ErrDoesNotUseID3v2 } header.Identifier = string(identifier) diff --git a/v2/header_test.go b/v2/header_test.go index e35fe59..dc6da18 100644 --- a/v2/header_test.go +++ b/v2/header_test.go @@ -8,13 +8,13 @@ import ( var TESTDATAPATH string = filepath.Join("..", "testData") -func TestGetHeader(t *testing.T) { +func TestReadHeader(t *testing.T) { f, err := os.Open(filepath.Join(TESTDATAPATH, "testreadv2.mp3")) if err != nil { t.Errorf("%s", err) } - header, err := ReadHeader(f) + header, err := readHeader(f) if err != nil { t.Errorf("GetHeader failed: %s", err) } diff --git a/v2/read.go b/v2/read.go index 37583d4..d2d399f 100644 --- a/v2/read.go +++ b/v2/read.go @@ -7,9 +7,12 @@ import ( // Reads the whole ID3v2 tag from rs func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { - header, err := ReadHeader(rs) - if err != nil { + header, err := readHeader(rs) + if err == ErrDoesNotUseID3v2 { + return nil, err + } else if err != nil { return nil, fmt.Errorf("could not get header: %s", err) + } // collect frames @@ -20,8 +23,8 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { break } - frame, r, err := ReadNextFrame(rs, header) - if err == ErrGotPadding || err == ErrBiggerThanSize { + frame, r, err := readNextFrame(rs, header) + if err == ErrGotPadding || err == ErrBiggerThanSize || err == ErrInvalidID { break } @@ -38,4 +41,4 @@ func ReadV2Tag(rs io.ReadSeeker) (*ID3v2Tag, error) { Header: header, Frames: frames, }, nil -} \ No newline at end of file +}