diff --git a/lofty/Cargo.toml b/lofty/Cargo.toml index 8551b194..4703a4ce 100644 --- a/lofty/Cargo.toml +++ b/lofty/Cargo.toml @@ -34,6 +34,7 @@ id3v2_compression_support = ["dep:flate2"] [dev-dependencies] # WAV properties validity tests hound = { git = "https://github.com/ruuda/hound.git", rev = "02e66effb33683dd6acb92df792683ee46ad6a59" } +rusty-fork = "0.3.0" # tag_writer example structopt = { version = "0.3.26", default-features = false } tempfile = "3.15.0" diff --git a/lofty/tests/taglib/data/005411.id3 b/lofty/tests/taglib/data/005411.id3 new file mode 100644 index 00000000..ab2e0997 Binary files /dev/null and b/lofty/tests/taglib/data/005411.id3 differ diff --git a/lofty/tests/taglib/data/alaw.aifc b/lofty/tests/taglib/data/alaw.aifc new file mode 100644 index 00000000..33b4ea2a Binary files /dev/null and b/lofty/tests/taglib/data/alaw.aifc differ diff --git a/lofty/tests/taglib/data/alaw.wav b/lofty/tests/taglib/data/alaw.wav new file mode 100644 index 00000000..cf548eff Binary files /dev/null and b/lofty/tests/taglib/data/alaw.wav differ diff --git a/lofty/tests/taglib/data/bladeenc.mp3 b/lofty/tests/taglib/data/bladeenc.mp3 new file mode 100644 index 00000000..e3d1a4b5 Binary files /dev/null and b/lofty/tests/taglib/data/bladeenc.mp3 differ diff --git a/lofty/tests/taglib/data/blank_video.m4v b/lofty/tests/taglib/data/blank_video.m4v new file mode 100644 index 00000000..4bb15ded Binary files /dev/null and b/lofty/tests/taglib/data/blank_video.m4v differ diff --git a/lofty/tests/taglib/data/broken-tenc.id3 b/lofty/tests/taglib/data/broken-tenc.id3 new file mode 100644 index 00000000..80904050 Binary files /dev/null and b/lofty/tests/taglib/data/broken-tenc.id3 differ diff --git a/lofty/tests/taglib/data/click.mpc b/lofty/tests/taglib/data/click.mpc new file mode 100644 index 00000000..a41f14e9 Binary files /dev/null and b/lofty/tests/taglib/data/click.mpc differ diff --git a/lofty/tests/taglib/data/click.wv b/lofty/tests/taglib/data/click.wv new file mode 100644 index 00000000..f8bd1a85 Binary files /dev/null and b/lofty/tests/taglib/data/click.wv differ diff --git a/lofty/tests/taglib/data/compressed_id3_frame.mp3 b/lofty/tests/taglib/data/compressed_id3_frame.mp3 new file mode 100644 index 00000000..824d036f Binary files /dev/null and b/lofty/tests/taglib/data/compressed_id3_frame.mp3 differ diff --git a/lofty/tests/taglib/data/correctness_gain_silent_output.opus b/lofty/tests/taglib/data/correctness_gain_silent_output.opus new file mode 100644 index 00000000..00972c42 Binary files /dev/null and b/lofty/tests/taglib/data/correctness_gain_silent_output.opus differ diff --git a/lofty/tests/taglib/data/covr-junk.m4a b/lofty/tests/taglib/data/covr-junk.m4a new file mode 100644 index 00000000..ac80cb29 Binary files /dev/null and b/lofty/tests/taglib/data/covr-junk.m4a differ diff --git a/lofty/tests/taglib/data/dsd_stereo.wv b/lofty/tests/taglib/data/dsd_stereo.wv new file mode 100644 index 00000000..80619270 Binary files /dev/null and b/lofty/tests/taglib/data/dsd_stereo.wv differ diff --git a/lofty/tests/taglib/data/duplicate_id3v2.aiff b/lofty/tests/taglib/data/duplicate_id3v2.aiff new file mode 100644 index 00000000..6703583f Binary files /dev/null and b/lofty/tests/taglib/data/duplicate_id3v2.aiff differ diff --git a/lofty/tests/taglib/data/duplicate_id3v2.mp3 b/lofty/tests/taglib/data/duplicate_id3v2.mp3 new file mode 100644 index 00000000..34f4f158 Binary files /dev/null and b/lofty/tests/taglib/data/duplicate_id3v2.mp3 differ diff --git a/lofty/tests/taglib/data/duplicate_tags.wav b/lofty/tests/taglib/data/duplicate_tags.wav new file mode 100644 index 00000000..b9865bbd Binary files /dev/null and b/lofty/tests/taglib/data/duplicate_tags.wav differ diff --git a/lofty/tests/taglib/data/empty-seektable.flac b/lofty/tests/taglib/data/empty-seektable.flac new file mode 100644 index 00000000..20dd90d9 Binary files /dev/null and b/lofty/tests/taglib/data/empty-seektable.flac differ diff --git a/lofty/tests/taglib/data/empty.aiff b/lofty/tests/taglib/data/empty.aiff new file mode 100644 index 00000000..849b762d Binary files /dev/null and b/lofty/tests/taglib/data/empty.aiff differ diff --git a/lofty/tests/taglib/data/empty.ogg b/lofty/tests/taglib/data/empty.ogg new file mode 100644 index 00000000..aa533104 Binary files /dev/null and b/lofty/tests/taglib/data/empty.ogg differ diff --git a/lofty/tests/taglib/data/empty.spx b/lofty/tests/taglib/data/empty.spx new file mode 100644 index 00000000..70572b45 Binary files /dev/null and b/lofty/tests/taglib/data/empty.spx differ diff --git a/lofty/tests/taglib/data/empty.wav b/lofty/tests/taglib/data/empty.wav new file mode 100644 index 00000000..74b5a6de Binary files /dev/null and b/lofty/tests/taglib/data/empty.wav differ diff --git a/lofty/tests/taglib/data/empty_alac.m4a b/lofty/tests/taglib/data/empty_alac.m4a new file mode 100644 index 00000000..8c678321 Binary files /dev/null and b/lofty/tests/taglib/data/empty_alac.m4a differ diff --git a/lofty/tests/taglib/data/empty_flac.oga b/lofty/tests/taglib/data/empty_flac.oga new file mode 100644 index 00000000..444587fd Binary files /dev/null and b/lofty/tests/taglib/data/empty_flac.oga differ diff --git a/lofty/tests/taglib/data/empty_vorbis.oga b/lofty/tests/taglib/data/empty_vorbis.oga new file mode 100644 index 00000000..aa533104 Binary files /dev/null and b/lofty/tests/taglib/data/empty_vorbis.oga differ diff --git a/lofty/tests/taglib/data/excessive_alloc.aif b/lofty/tests/taglib/data/excessive_alloc.aif new file mode 100644 index 00000000..9cb3a6e1 Binary files /dev/null and b/lofty/tests/taglib/data/excessive_alloc.aif differ diff --git a/lofty/tests/taglib/data/excessive_alloc.mp3 b/lofty/tests/taglib/data/excessive_alloc.mp3 new file mode 100644 index 00000000..cd8aa2ab Binary files /dev/null and b/lofty/tests/taglib/data/excessive_alloc.mp3 differ diff --git a/lofty/tests/taglib/data/float64.wav b/lofty/tests/taglib/data/float64.wav new file mode 100644 index 00000000..d34f692b Binary files /dev/null and b/lofty/tests/taglib/data/float64.wav differ diff --git a/lofty/tests/taglib/data/garbage.mp3 b/lofty/tests/taglib/data/garbage.mp3 new file mode 100644 index 00000000..730b74e7 Binary files /dev/null and b/lofty/tests/taglib/data/garbage.mp3 differ diff --git a/lofty/tests/taglib/data/gnre.m4a b/lofty/tests/taglib/data/gnre.m4a new file mode 100644 index 00000000..f925ea9e Binary files /dev/null and b/lofty/tests/taglib/data/gnre.m4a differ diff --git a/lofty/tests/taglib/data/has-tags.m4a b/lofty/tests/taglib/data/has-tags.m4a new file mode 100644 index 00000000..f48a28b5 Binary files /dev/null and b/lofty/tests/taglib/data/has-tags.m4a differ diff --git a/lofty/tests/taglib/data/id3v22-tda.mp3 b/lofty/tests/taglib/data/id3v22-tda.mp3 new file mode 100644 index 00000000..b0545ea6 Binary files /dev/null and b/lofty/tests/taglib/data/id3v22-tda.mp3 differ diff --git a/lofty/tests/taglib/data/ilst-is-last.m4a b/lofty/tests/taglib/data/ilst-is-last.m4a new file mode 100644 index 00000000..c56c8049 Binary files /dev/null and b/lofty/tests/taglib/data/ilst-is-last.m4a differ diff --git a/lofty/tests/taglib/data/infloop.m4a b/lofty/tests/taglib/data/infloop.m4a new file mode 100644 index 00000000..bbf76db8 Binary files /dev/null and b/lofty/tests/taglib/data/infloop.m4a differ diff --git a/lofty/tests/taglib/data/infloop.mpc b/lofty/tests/taglib/data/infloop.mpc new file mode 100644 index 00000000..46861ab3 Binary files /dev/null and b/lofty/tests/taglib/data/infloop.mpc differ diff --git a/lofty/tests/taglib/data/infloop.wav b/lofty/tests/taglib/data/infloop.wav new file mode 100644 index 00000000..c220baa8 Binary files /dev/null and b/lofty/tests/taglib/data/infloop.wav differ diff --git a/lofty/tests/taglib/data/infloop.wv b/lofty/tests/taglib/data/infloop.wv new file mode 100644 index 00000000..d8c720cf Binary files /dev/null and b/lofty/tests/taglib/data/infloop.wv differ diff --git a/lofty/tests/taglib/data/invalid-frames1.mp3 b/lofty/tests/taglib/data/invalid-frames1.mp3 new file mode 100644 index 00000000..c076712c Binary files /dev/null and b/lofty/tests/taglib/data/invalid-frames1.mp3 differ diff --git a/lofty/tests/taglib/data/invalid-frames2.mp3 b/lofty/tests/taglib/data/invalid-frames2.mp3 new file mode 100644 index 00000000..01976fc5 Binary files /dev/null and b/lofty/tests/taglib/data/invalid-frames2.mp3 differ diff --git a/lofty/tests/taglib/data/invalid-frames3.mp3 b/lofty/tests/taglib/data/invalid-frames3.mp3 new file mode 100644 index 00000000..6bbd2d39 Binary files /dev/null and b/lofty/tests/taglib/data/invalid-frames3.mp3 differ diff --git a/lofty/tests/taglib/data/lame_cbr.mp3 b/lofty/tests/taglib/data/lame_cbr.mp3 new file mode 100644 index 00000000..b7badeb0 Binary files /dev/null and b/lofty/tests/taglib/data/lame_cbr.mp3 differ diff --git a/lofty/tests/taglib/data/lame_vbr.mp3 b/lofty/tests/taglib/data/lame_vbr.mp3 new file mode 100644 index 00000000..643056ef Binary files /dev/null and b/lofty/tests/taglib/data/lame_vbr.mp3 differ diff --git a/lofty/tests/taglib/data/longloop.ape b/lofty/tests/taglib/data/longloop.ape new file mode 100644 index 00000000..3800387a Binary files /dev/null and b/lofty/tests/taglib/data/longloop.ape differ diff --git a/lofty/tests/taglib/data/lowercase-fields.ogg b/lofty/tests/taglib/data/lowercase-fields.ogg new file mode 100644 index 00000000..0ddd4935 Binary files /dev/null and b/lofty/tests/taglib/data/lowercase-fields.ogg differ diff --git a/lofty/tests/taglib/data/mac-390-hdr.ape b/lofty/tests/taglib/data/mac-390-hdr.ape new file mode 100644 index 00000000..c703e2e2 Binary files /dev/null and b/lofty/tests/taglib/data/mac-390-hdr.ape differ diff --git a/lofty/tests/taglib/data/mac-396.ape b/lofty/tests/taglib/data/mac-396.ape new file mode 100644 index 00000000..fa7ae414 Binary files /dev/null and b/lofty/tests/taglib/data/mac-396.ape differ diff --git a/lofty/tests/taglib/data/mac-399-id3v2.ape b/lofty/tests/taglib/data/mac-399-id3v2.ape new file mode 100644 index 00000000..2ea97fc4 Binary files /dev/null and b/lofty/tests/taglib/data/mac-399-id3v2.ape differ diff --git a/lofty/tests/taglib/data/mac-399-tagged.ape b/lofty/tests/taglib/data/mac-399-tagged.ape new file mode 100644 index 00000000..3f5a656e Binary files /dev/null and b/lofty/tests/taglib/data/mac-399-tagged.ape differ diff --git a/lofty/tests/taglib/data/mac-399.ape b/lofty/tests/taglib/data/mac-399.ape new file mode 100644 index 00000000..3b0661ee Binary files /dev/null and b/lofty/tests/taglib/data/mac-399.ape differ diff --git a/lofty/tests/taglib/data/mpeg2.mp3 b/lofty/tests/taglib/data/mpeg2.mp3 new file mode 100644 index 00000000..13e8d53d Binary files /dev/null and b/lofty/tests/taglib/data/mpeg2.mp3 differ diff --git a/lofty/tests/taglib/data/multiple-vc.flac b/lofty/tests/taglib/data/multiple-vc.flac new file mode 100644 index 00000000..93d9a8a1 Binary files /dev/null and b/lofty/tests/taglib/data/multiple-vc.flac differ diff --git a/lofty/tests/taglib/data/no-extension b/lofty/tests/taglib/data/no-extension new file mode 100644 index 00000000..65f57c2e Binary files /dev/null and b/lofty/tests/taglib/data/no-extension differ diff --git a/lofty/tests/taglib/data/no-tags.3g2 b/lofty/tests/taglib/data/no-tags.3g2 new file mode 100644 index 00000000..d31a6ce9 Binary files /dev/null and b/lofty/tests/taglib/data/no-tags.3g2 differ diff --git a/lofty/tests/taglib/data/no-tags.ape b/lofty/tests/taglib/data/no-tags.ape new file mode 100644 index 00000000..2ce41cfe Binary files /dev/null and b/lofty/tests/taglib/data/no-tags.ape differ diff --git a/lofty/tests/taglib/data/no-tags.flac b/lofty/tests/taglib/data/no-tags.flac new file mode 100644 index 00000000..41714416 Binary files /dev/null and b/lofty/tests/taglib/data/no-tags.flac differ diff --git a/lofty/tests/taglib/data/no-tags.m4a b/lofty/tests/taglib/data/no-tags.m4a new file mode 100644 index 00000000..ba4e92ba Binary files /dev/null and b/lofty/tests/taglib/data/no-tags.m4a differ diff --git a/lofty/tests/taglib/data/no-tags.mpc b/lofty/tests/taglib/data/no-tags.mpc new file mode 100644 index 00000000..3405545a Binary files /dev/null and b/lofty/tests/taglib/data/no-tags.mpc differ diff --git a/lofty/tests/taglib/data/no_length.wv b/lofty/tests/taglib/data/no_length.wv new file mode 100644 index 00000000..c06d1071 Binary files /dev/null and b/lofty/tests/taglib/data/no_length.wv differ diff --git a/lofty/tests/taglib/data/non-full-meta.m4a b/lofty/tests/taglib/data/non-full-meta.m4a new file mode 100644 index 00000000..724cc675 Binary files /dev/null and b/lofty/tests/taglib/data/non-full-meta.m4a differ diff --git a/lofty/tests/taglib/data/non_standard_rate.wv b/lofty/tests/taglib/data/non_standard_rate.wv new file mode 100644 index 00000000..ccc90277 Binary files /dev/null and b/lofty/tests/taglib/data/non_standard_rate.wv differ diff --git a/lofty/tests/taglib/data/pcm_with_fact_chunk.wav b/lofty/tests/taglib/data/pcm_with_fact_chunk.wav new file mode 100644 index 00000000..a6dc1d6c Binary files /dev/null and b/lofty/tests/taglib/data/pcm_with_fact_chunk.wav differ diff --git a/lofty/tests/taglib/data/rare_frames.mp3 b/lofty/tests/taglib/data/rare_frames.mp3 new file mode 100644 index 00000000..e485337f Binary files /dev/null and b/lofty/tests/taglib/data/rare_frames.mp3 differ diff --git a/lofty/tests/taglib/data/segfault.aif b/lofty/tests/taglib/data/segfault.aif new file mode 100644 index 00000000..5dce192b Binary files /dev/null and b/lofty/tests/taglib/data/segfault.aif differ diff --git a/lofty/tests/taglib/data/segfault.mpc b/lofty/tests/taglib/data/segfault.mpc new file mode 100644 index 00000000..2c7e29fb Binary files /dev/null and b/lofty/tests/taglib/data/segfault.mpc differ diff --git a/lofty/tests/taglib/data/segfault.oga b/lofty/tests/taglib/data/segfault.oga new file mode 100644 index 00000000..e23c2170 Binary files /dev/null and b/lofty/tests/taglib/data/segfault.oga differ diff --git a/lofty/tests/taglib/data/segfault.wav b/lofty/tests/taglib/data/segfault.wav new file mode 100644 index 00000000..0385e99b Binary files /dev/null and b/lofty/tests/taglib/data/segfault.wav differ diff --git a/lofty/tests/taglib/data/segfault2.mpc b/lofty/tests/taglib/data/segfault2.mpc new file mode 100644 index 00000000..fcfa982f --- /dev/null +++ b/lofty/tests/taglib/data/segfault2.mpc @@ -0,0 +1 @@ +MPCKSH \ No newline at end of file diff --git a/lofty/tests/taglib/data/silence-1.wma b/lofty/tests/taglib/data/silence-1.wma new file mode 100644 index 00000000..e06f9176 Binary files /dev/null and b/lofty/tests/taglib/data/silence-1.wma differ diff --git a/lofty/tests/taglib/data/silence-44-s.flac b/lofty/tests/taglib/data/silence-44-s.flac new file mode 100644 index 00000000..24e15deb Binary files /dev/null and b/lofty/tests/taglib/data/silence-44-s.flac differ diff --git a/lofty/tests/taglib/data/sinewave.flac b/lofty/tests/taglib/data/sinewave.flac new file mode 100644 index 00000000..25d31b2d Binary files /dev/null and b/lofty/tests/taglib/data/sinewave.flac differ diff --git a/lofty/tests/taglib/data/sv4_header.mpc b/lofty/tests/taglib/data/sv4_header.mpc new file mode 100644 index 00000000..214f7ac4 Binary files /dev/null and b/lofty/tests/taglib/data/sv4_header.mpc differ diff --git a/lofty/tests/taglib/data/sv5_header.mpc b/lofty/tests/taglib/data/sv5_header.mpc new file mode 100644 index 00000000..6d17e65f Binary files /dev/null and b/lofty/tests/taglib/data/sv5_header.mpc differ diff --git a/lofty/tests/taglib/data/sv8_header.mpc b/lofty/tests/taglib/data/sv8_header.mpc new file mode 100644 index 00000000..3405545a Binary files /dev/null and b/lofty/tests/taglib/data/sv8_header.mpc differ diff --git a/lofty/tests/taglib/data/tagged.wv b/lofty/tests/taglib/data/tagged.wv new file mode 100644 index 00000000..333f8687 Binary files /dev/null and b/lofty/tests/taglib/data/tagged.wv differ diff --git a/lofty/tests/taglib/data/uint8we.wav b/lofty/tests/taglib/data/uint8we.wav new file mode 100644 index 00000000..9623db22 Binary files /dev/null and b/lofty/tests/taglib/data/uint8we.wav differ diff --git a/lofty/tests/taglib/data/unsupported-extension.xx b/lofty/tests/taglib/data/unsupported-extension.xx new file mode 100644 index 00000000..65f57c2e Binary files /dev/null and b/lofty/tests/taglib/data/unsupported-extension.xx differ diff --git a/lofty/tests/taglib/data/unsynch.id3 b/lofty/tests/taglib/data/unsynch.id3 new file mode 100644 index 00000000..cfe6ee1a Binary files /dev/null and b/lofty/tests/taglib/data/unsynch.id3 differ diff --git a/lofty/tests/taglib/data/w000.mp3 b/lofty/tests/taglib/data/w000.mp3 new file mode 100644 index 00000000..f9c22617 Binary files /dev/null and b/lofty/tests/taglib/data/w000.mp3 differ diff --git a/lofty/tests/taglib/data/xing.mp3 b/lofty/tests/taglib/data/xing.mp3 new file mode 100644 index 00000000..0c880151 Binary files /dev/null and b/lofty/tests/taglib/data/xing.mp3 differ diff --git a/lofty/tests/taglib/data/zero-length-mdat.m4a b/lofty/tests/taglib/data/zero-length-mdat.m4a new file mode 100644 index 00000000..578d2ef7 Binary files /dev/null and b/lofty/tests/taglib/data/zero-length-mdat.m4a differ diff --git a/lofty/tests/taglib/data/zero-size-chunk.wav b/lofty/tests/taglib/data/zero-size-chunk.wav new file mode 100644 index 00000000..8517e797 Binary files /dev/null and b/lofty/tests/taglib/data/zero-size-chunk.wav differ diff --git a/lofty/tests/taglib/data/zero-sized-padding.flac b/lofty/tests/taglib/data/zero-sized-padding.flac new file mode 100644 index 00000000..86ab8bf7 Binary files /dev/null and b/lofty/tests/taglib/data/zero-sized-padding.flac differ diff --git a/lofty/tests/taglib/data/zerodiv.ape b/lofty/tests/taglib/data/zerodiv.ape new file mode 100644 index 00000000..683bc2dd Binary files /dev/null and b/lofty/tests/taglib/data/zerodiv.ape differ diff --git a/lofty/tests/taglib/data/zerodiv.mpc b/lofty/tests/taglib/data/zerodiv.mpc new file mode 100644 index 00000000..d3ea57c7 Binary files /dev/null and b/lofty/tests/taglib/data/zerodiv.mpc differ diff --git a/lofty/tests/taglib/main.rs b/lofty/tests/taglib/main.rs new file mode 100644 index 00000000..f695aeb6 --- /dev/null +++ b/lofty/tests/taglib/main.rs @@ -0,0 +1,24 @@ +#![allow(missing_docs)] + +pub(crate) mod util; + +mod test_aiff; +mod test_ape; +mod test_apetag; +mod test_fileref; +mod test_flac; +mod test_flacpicture; +mod test_id3v1; +mod test_id3v2; +mod test_info; +mod test_mp4; +mod test_mpc; +mod test_mpeg; +mod test_ogaflac; +mod test_ogg; +mod test_opus; +mod test_speex; +mod test_synchdata; +mod test_wav; +mod test_wavpack; +mod test_xiphcomment; diff --git a/lofty/tests/taglib/test_aiff.rs b/lofty/tests/taglib/test_aiff.rs new file mode 100644 index 00000000..a4b5a963 --- /dev/null +++ b/lofty/tests/taglib/test_aiff.rs @@ -0,0 +1,126 @@ +use crate::util::get_file; +use crate::{assert_delta, temp_file}; + +use std::io::Seek; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::{AudioFile, FileType}; +use lofty::id3::v2::Id3v2Tag; +use lofty::iff::aiff::{AiffCompressionType, AiffFile}; +use lofty::probe::Probe; +use lofty::tag::{Accessor, TagType}; + +#[test_log::test] +fn test_aiff_properties() { + let file = get_file::("tests/taglib/data/empty.aiff"); + + let properties = file.properties(); + assert_eq!(properties.duration().as_secs(), 0); + assert_delta!(properties.duration().as_millis(), 67, 1); + assert_delta!(properties.audio_bitrate(), 706, 1); + assert_eq!(properties.sample_rate(), 44100); + assert_eq!(properties.channels(), 1); + assert_eq!(properties.sample_size(), 16); + // TODO: get those options in lofty + // CPPUNIT_ASSERT_EQUAL(2941U, f.audioProperties()->sampleFrames()); + assert!(properties.compression_type().is_none()); +} + +#[test_log::test] +fn test_aifc_properties() { + let file = get_file::("tests/taglib/data/alaw.aifc"); + + let properties = file.properties(); + assert_eq!(properties.duration().as_secs(), 0); + assert_delta!(properties.duration().as_millis(), 37, 1); + assert_eq!(properties.audio_bitrate(), 355); + assert_eq!(properties.sample_rate(), 44100); + assert_eq!(properties.channels(), 1); + assert_eq!(properties.sample_size(), 16); + // TODO: get those options in lofty + // CPPUNIT_ASSERT_EQUAL(1622U, f.audioProperties()->sampleFrames()); + assert!(properties.compression_type().is_some()); + assert_eq!( + properties.compression_type().unwrap().clone(), + AiffCompressionType::ALAW + ); + // NOTE: The file's compression name is actually "SGI CCITT G.711 A-law" + // + // We have a hardcoded value for any of the concrete AiffCompressionType variants, as the messages + // are more or less standardized. This is not that big of a deal, especially as many encoders choose + // not to even write a compression name in the first place. + assert_eq!( + properties.compression_type().unwrap().compression_name(), + "CCITT G.711 A-law" + ); +} + +#[test_log::test] +fn test_save_id3v2() { + let mut file = temp_file!("tests/taglib/data/empty.aiff"); + + { + let mut tfile = AiffFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + assert!(tfile.id3v2().is_none()); + + let mut id3v2 = Id3v2Tag::new(); + id3v2.set_title("TitleXXX".to_string()); + tfile.set_id3v2(id3v2); + file.rewind().unwrap(); + tfile.save_to(&mut file, WriteOptions::default()).unwrap(); + assert!(tfile.contains_tag_type(TagType::Id3v2)); + } + + file.rewind().unwrap(); + + { + let mut tfile = AiffFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + let mut id3v2 = tfile.id3v2().unwrap().to_owned(); + assert_eq!(id3v2.title().as_deref(), Some("TitleXXX")); + // NOTE: TagLib sets an empty title, which will implicitly remove it from the tag. Lofty will allow empty tag items to exist. + // What's important is that these are equivalent in behavior. + id3v2.remove_title(); + tfile.set_id3v2(id3v2); + file.rewind().unwrap(); + tfile.save_to(&mut file, WriteOptions::default()).unwrap(); + } + + file.rewind().unwrap(); + + { + let tfile = AiffFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(!tfile.contains_tag_type(TagType::Id3v2)); + } +} + +#[test_log::test] +#[ignore] +fn test_save_id3v23() { + todo!("Support writing ID3v2.3 tags") +} + +#[test_log::test] +#[ignore] +fn test_duplicate_id3v2() { + // Marker test, Lofty will overwrite values in the original tag with any new values it finds in the next tag. +} + +#[test_log::test] +fn test_fuzzed_file1() { + assert_eq!( + Probe::open("tests/taglib/data/segfault.aif") + .unwrap() + .guess_file_type() + .unwrap() + .file_type(), + Some(FileType::Aiff) + ); +} + +#[test_log::test] +#[ignore] +fn test_fuzzed_file2() { + // Marker test, this file doesn't even have a valid signature. No idea how TagLib manages to read it. +} diff --git a/lofty/tests/taglib/test_ape.rs b/lofty/tests/taglib/test_ape.rs new file mode 100644 index 00000000..d100d71a --- /dev/null +++ b/lofty/tests/taglib/test_ape.rs @@ -0,0 +1,398 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::io::Seek; +use std::time::Duration; + +use lofty::ape::{ApeFile, ApeItem, ApeTag}; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::{AudioFile, FileType}; +use lofty::id3::v1::Id3v1Tag; +use lofty::probe::Probe; +use lofty::tag::{Accessor, ItemValue, TagExt}; + +fn test_399(path: &str) { + let f = get_file::(path); + let properties = f.properties(); + + assert_eq!(properties.duration(), Duration::from_millis(3550)); + assert_eq!(properties.bitrate(), 192); + assert_eq!(properties.channels(), 2); + assert_eq!(properties.sample_rate(), 44100); + assert_eq!(properties.bit_depth(), 16); + // TODO + // assert_eq!(properties.sample_frames(), 156556); + assert_eq!(properties.version(), 3990) +} + +#[test_log::test] +fn test_properties_399() { + test_399("tests/taglib/data/mac-399.ape") +} + +#[test_log::test] +fn test_properties_399_tagged() { + test_399("tests/taglib/data/mac-399-tagged.ape") +} + +#[test_log::test] +fn test_properties_399_id3v2() { + test_399("tests/taglib/data/mac-399-id3v2.ape") +} + +#[test_log::test] +fn test_properties_396() { + let f = get_file::("tests/taglib/data/mac-396.ape"); + let properties = f.properties(); + + assert_eq!(properties.duration(), Duration::from_millis(3685)); + assert_eq!(properties.bitrate(), 0); + assert_eq!(properties.channels(), 2); + assert_eq!(properties.sample_rate(), 44100); + assert_eq!(properties.bit_depth(), 16); + // TODO + // assert_eq!(properties.sample_frames(), 162496); + assert_eq!(properties.version(), 3960) +} + +#[test_log::test] +fn test_properties_390() { + let f = get_file::("tests/taglib/data/mac-390-hdr.ape"); + let properties = f.properties(); + + assert_eq!(properties.duration(), Duration::from_millis(15630)); + assert_eq!(properties.bitrate(), 0); + assert_eq!(properties.channels(), 2); + assert_eq!(properties.sample_rate(), 44100); + assert_eq!(properties.bit_depth(), 16); + // TODO + // assert_eq!(properties.sample_frames(), 689262); + assert_eq!(properties.version(), 3900) +} + +#[test_log::test] +fn test_fuzzed_file_1() { + assert_eq!( + Probe::open("tests/taglib/data/longloop.ape") + .unwrap() + .guess_file_type() + .unwrap() + .file_type(), + Some(FileType::Ape) + ); +} + +#[test_log::test] +fn test_fuzzed_file_2() { + assert_eq!( + Probe::open("tests/taglib/data/zerodiv.ape") + .unwrap() + .guess_file_type() + .unwrap() + .file_type(), + Some(FileType::Ape) + ); +} + +#[test_log::test] +fn test_strip_and_properties() { + let mut file = temp_file!("tests/taglib/data/mac-399.ape"); + { + let mut ape_file = ApeFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + let mut ape_tag = ApeTag::default(); + ape_tag.set_title(String::from("APE")); + ape_file.set_ape(ape_tag); + + let mut id3v1_tag = Id3v1Tag::default(); + id3v1_tag.set_title(String::from("ID3v1")); + ape_file.set_id3v1(id3v1_tag); + + file.rewind().unwrap(); + ape_file + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + } + { + file.rewind().unwrap(); + let mut ape_file = ApeFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + assert_eq!(ape_file.ape().unwrap().title().as_deref(), Some("APE")); + ape_file.remove_ape(); + + assert_eq!(ape_file.id3v1().unwrap().title().as_deref(), Some("ID3v1")); + ape_file.remove_id3v1(); + + assert!(!ape_file.contains_tag()); + } +} + +#[test_log::test] +fn test_properties() { + let mut tag = ApeTag::default(); + tag.insert( + ApeItem::new( + String::from("ALBUM"), + ItemValue::Text(String::from("Album")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ALBUMARTIST"), + ItemValue::Text(String::from("Album Artist")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ALBUMARTISTSORT"), + ItemValue::Text(String::from("Album Artist Sort")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ALBUMSORT"), + ItemValue::Text(String::from("Album Sort")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ARTIST"), + ItemValue::Text(String::from("Artist")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ARTISTS"), + ItemValue::Text(String::from("Artists")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ARTISTSORT"), + ItemValue::Text(String::from("Artist Sort")), + ) + .unwrap(), + ); + tag.insert(ApeItem::new(String::from("ASIN"), ItemValue::Text(String::from("ASIN"))).unwrap()); + tag.insert( + ApeItem::new( + String::from("BARCODE"), + ItemValue::Text(String::from("Barcode")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("CATALOGNUMBER"), + ItemValue::Text(String::from("Catalog Number 1")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("COMMENT"), + ItemValue::Text(String::from("Comment")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("DATE"), + ItemValue::Text(String::from("2021-01-10")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("DISCNUMBER"), + ItemValue::Text(String::from("3/5")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("GENRE"), + ItemValue::Text(String::from("Genre")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ISRC"), + ItemValue::Text(String::from("UKAAA0500001")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("LABEL"), + ItemValue::Text(String::from("Label 1")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MEDIA"), + ItemValue::Text(String::from("Media")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MUSICBRAINZ_ALBUMARTISTID"), + ItemValue::Text(String::from("MusicBrainz_AlbumartistID")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MUSICBRAINZ_ALBUMID"), + ItemValue::Text(String::from("MusicBrainz_AlbumID")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MUSICBRAINZ_ARTISTID"), + ItemValue::Text(String::from("MusicBrainz_ArtistID")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MUSICBRAINZ_RELEASEGROUPID"), + ItemValue::Text(String::from("MusicBrainz_ReleasegroupID")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MUSICBRAINZ_RELEASETRACKID"), + ItemValue::Text(String::from("MusicBrainz_ReleasetrackID")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("MUSICBRAINZ_TRACKID"), + ItemValue::Text(String::from("MusicBrainz_TrackID")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("ORIGINALDATE"), + ItemValue::Text(String::from("2021-01-09")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("RELEASECOUNTRY"), + ItemValue::Text(String::from("Release Country")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("RELEASESTATUS"), + ItemValue::Text(String::from("Release Status")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("RELEASETYPE"), + ItemValue::Text(String::from("Release Type")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("SCRIPT"), + ItemValue::Text(String::from("Script")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("TITLE"), + ItemValue::Text(String::from("Title")), + ) + .unwrap(), + ); + tag.insert( + ApeItem::new( + String::from("TRACKNUMBER"), + ItemValue::Text(String::from("2/3")), + ) + .unwrap(), + ); + + let mut file = temp_file!("tests/taglib/data/mac-399.ape"); + { + let mut ape_file = ApeFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + ape_file.set_ape(tag.clone()); + + file.rewind().unwrap(); + ape_file + .ape() + .unwrap() + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + } + { + file.rewind().unwrap(); + let ape_file = ApeFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + assert_eq!(ape_file.ape(), Some(&tag)); + } +} + +#[test_log::test] +fn test_repeated_save() { + let mut file = temp_file!("tests/taglib/data/mac-399.ape"); + { + let mut ape_file = ApeFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(ape_file.ape().is_none()); + assert!(ape_file.id3v1().is_none()); + + let mut ape_tag = ApeTag::default(); + ape_tag.set_title(String::from("01234 56789 ABCDE FGHIJ")); + ape_file.set_ape(ape_tag); + ape_file + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + file.rewind().unwrap(); + + ape_file.ape_mut().unwrap().set_title(String::from("0")); + ape_file + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + file.rewind().unwrap(); + + let mut id3v1_tag = Id3v1Tag::default(); + id3v1_tag.set_title(String::from("01234 56789 ABCDE FGHIJ")); + ape_file.set_id3v1(id3v1_tag); + ape_file.ape_mut().unwrap().set_title(String::from( + "01234 56789 ABCDE FGHIJ 01234 56789 ABCDE FGHIJ 01234 56789", + )); + ape_file + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + } + { + file.rewind().unwrap(); + let ape_file = ApeFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + assert!(ape_file.ape().is_some()); + assert!(ape_file.id3v1().is_some()); + } +} diff --git a/lofty/tests/taglib/test_apetag.rs b/lofty/tests/taglib/test_apetag.rs new file mode 100644 index 00000000..fc984458 --- /dev/null +++ b/lofty/tests/taglib/test_apetag.rs @@ -0,0 +1,125 @@ +use crate::temp_file; + +use std::io::Seek; + +use lofty::ape::{ApeItem, ApeTag}; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::musepack::MpcFile; +use lofty::tag::{Accessor, ItemValue, TagExt}; + +#[test_log::test] +fn test_is_empty() { + let mut tag = ApeTag::default(); + assert!(tag.is_empty()); + tag.insert( + ApeItem::new( + String::from("COMPOSER"), + ItemValue::Text(String::from("Mike Oldfield")), + ) + .unwrap(), + ); + assert!(!tag.is_empty()); +} + +#[test_log::test] +fn test_is_empty_2() { + let mut tag = ApeTag::default(); + assert!(tag.is_empty()); + tag.set_artist(String::from("Mike Oldfield")); + assert!(!tag.is_empty()); +} + +#[test_log::test] +#[ignore] +fn test_property_interface_1() { + // Marker test, Lofty does not replicate the TagLib property API +} + +#[test_log::test] +#[ignore] +fn test_property_interface_2() { + // Marker test, Lofty does not replicate the TagLib property API +} + +#[test_log::test] +fn test_invalid_keys() { + static INVALID_KEY_ONE_CHARACTER: &str = "A"; + static INVALID_KEY_FORBIDDEN_STRING: &str = "MP+"; + static INVALID_KEY_UNICODE: &str = "\x1234\x3456"; + static VALID_KEY_SPACE_AND_TILDE: &str = "A B~C"; + static VALID_KEY_NORMAL_ONE: &str = "ARTIST"; + + assert!( + ApeItem::new( + String::from(INVALID_KEY_ONE_CHARACTER), + ItemValue::Text(String::from("invalid key: one character")) + ) + .is_err() + ); + assert!( + ApeItem::new( + String::from(INVALID_KEY_FORBIDDEN_STRING), + ItemValue::Text(String::from("invalid key: forbidden string")) + ) + .is_err() + ); + assert!( + ApeItem::new( + String::from(INVALID_KEY_UNICODE), + ItemValue::Text(String::from("invalid key: Unicode")) + ) + .is_err() + ); + + let valid_space_and_tilde = ApeItem::new( + String::from(VALID_KEY_SPACE_AND_TILDE), + ItemValue::Text(String::from("valid key: space and tilde")), + ); + assert!(valid_space_and_tilde.is_ok()); + + let valid_normal_one = ApeItem::new( + String::from(VALID_KEY_NORMAL_ONE), + ItemValue::Text(String::from("valid key: normal one")), + ); + assert!(valid_normal_one.is_ok()); + + let mut tag = ApeTag::default(); + tag.insert(valid_space_and_tilde.unwrap()); + tag.insert(valid_normal_one.unwrap()); + assert_eq!(tag.len(), 2); +} + +#[test_log::test] +#[ignore] +fn test_text_binary() { + // Marker test, this is useless as Lofty does not have a similar API that the test is based upon: + // https://github.com/taglib/taglib/blob/a31356e330674640a07bef7d71d08242cae8e9bf/tests/test_apetag.cpp#L153 +} + +// TODO: Does not work! We fall for this collision. +#[test_log::test] +#[ignore] +fn test_id3v1_collision() { + let mut file = temp_file!("tests/taglib/data/no-tags.mpc"); + { + let mut mpc_file = + MpcFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + assert!(mpc_file.ape().is_none()); + assert!(mpc_file.id3v1().is_none()); + + let mut ape_tag = ApeTag::default(); + ape_tag.set_artist(String::from("Filltointersect ")); + ape_tag.set_title(String::from("Filltointersect ")); + mpc_file.set_ape(ape_tag); + mpc_file + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + } + { + file.rewind().unwrap(); + let mpc_file = + MpcFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + assert!(mpc_file.id3v1().is_none()); + } +} diff --git a/lofty/tests/taglib/test_fileref.rs b/lofty/tests/taglib/test_fileref.rs new file mode 100644 index 00000000..537eaabf --- /dev/null +++ b/lofty/tests/taglib/test_fileref.rs @@ -0,0 +1,294 @@ +use crate::temp_file; + +use std::io::{Read, Seek}; + +use lofty::config::{GlobalOptions, ParseOptions, WriteOptions}; +use lofty::error::{ErrorKind, LoftyError}; +use lofty::file::{AudioFile, FileType, TaggedFile, TaggedFileExt}; +use lofty::resolve::FileResolver; +use lofty::tag::{Accessor, Tag, TagExt, TagType}; + +fn file_ref_save(path: &str, expected_file_type: FileType) { + let path = format!("tests/taglib/data/{path}"); + let mut file = temp_file!(path); + { + let mut f = lofty::read_from(&mut file).unwrap(); + file.rewind().unwrap(); + + assert_eq!(f.file_type(), expected_file_type); + + let tag = match f.primary_tag_mut() { + Some(tag) => tag, + None => { + f.insert_tag(Tag::new(f.primary_tag_type())); + f.primary_tag_mut().unwrap() + }, + }; + tag.set_artist(String::from("test artist")); + tag.set_title(String::from("test title")); + tag.set_genre(String::from("Test!")); + tag.set_album(String::from("albummmm")); + tag.set_comment(String::from("a comment")); + tag.set_track(5); + tag.set_year(2020); + tag.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = lofty::read_from(&mut file).unwrap(); + file.rewind().unwrap(); + + let tag = f.primary_tag_mut().unwrap(); + assert_eq!(tag.artist().as_deref(), Some("test artist")); + assert_eq!(tag.title().as_deref(), Some("test title")); + assert_eq!(tag.genre().as_deref(), Some("Test!")); + assert_eq!(tag.album().as_deref(), Some("albummmm")); + assert_eq!(tag.comment().as_deref(), Some("a comment")); + assert_eq!(tag.track(), Some(5)); + assert_eq!(tag.year(), Some(2020)); + tag.set_artist(String::from("ttest artist")); + tag.set_title(String::from("ytest title")); + tag.set_genre(String::from("uTest!")); + tag.set_album(String::from("ialbummmm")); + tag.set_comment(String::from("another comment")); + tag.set_track(7); + tag.set_year(2080); + tag.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = lofty::read_from(&mut file).unwrap(); + file.rewind().unwrap(); + + let tag = f.primary_tag_mut().unwrap(); + assert_eq!(tag.artist().as_deref(), Some("ttest artist")); + assert_eq!(tag.title().as_deref(), Some("ytest title")); + assert_eq!(tag.genre().as_deref(), Some("uTest!")); + assert_eq!(tag.album().as_deref(), Some("ialbummmm")); + assert_eq!(tag.comment().as_deref(), Some("another comment")); + assert_eq!(tag.track(), Some(7)); + assert_eq!(tag.year(), Some(2080)); + } + + // NOTE: All tests following this in the TagLib suite are doing the exact same procedures, just + // using their other types: `FileStream` and `ByteVectorStream`. We do not have similar types, + // no need to replicate these. +} + +#[test_log::test] +fn test_musepack() { + file_ref_save("click.mpc", FileType::Mpc); +} + +#[test_log::test] +#[ignore] +fn test_asf() { + // TODO: We don't support ASF yet + // file_ref_save("silence-1.asf", FileType::ASF); +} + +#[test_log::test] +fn test_vorbis() { + file_ref_save("empty.ogg", FileType::Vorbis); +} + +#[test_log::test] +fn test_speex() { + file_ref_save("empty.spx", FileType::Speex); +} + +#[test_log::test] +fn test_flac() { + file_ref_save("no-tags.flac", FileType::Flac); +} + +#[test_log::test] +fn test_mp3() { + file_ref_save("xing.mp3", FileType::Mpeg); +} + +#[test_log::test] +#[ignore] +fn test_true_audio() { + // TODO: We don't support TTA yet + // file_ref_save("empty.tta", FileType::TrueAudio); +} + +#[test_log::test] +fn test_mp4_1() { + file_ref_save("has-tags.m4a", FileType::Mp4); +} + +#[test_log::test] +#[ignore] // TODO: The file has a malformed `free` atom. How does TagLib handle this? Currently we mess up entirely and just write a duplicate tag. +fn test_mp4_2() { + file_ref_save("no-tags.m4a", FileType::Mp4); +} + +#[test_log::test] +#[ignore] // TODO: We are able to write the first tag and even reread, but the second save causes a `SizeMismatch`. +fn test_mp4_3() { + file_ref_save("no-tags.3g2", FileType::Mp4); +} + +#[test_log::test] +fn test_mp4_4() { + file_ref_save("blank_video.m4v", FileType::Mp4); +} + +#[test_log::test] +fn test_wav() { + file_ref_save("empty.wav", FileType::Wav); +} + +#[test_log::test] +#[ignore] // TODO: We don't yet support FLAC in oga +fn test_oga_flac() { + file_ref_save("empty_flac.oga", FileType::Flac); +} + +#[test_log::test] +fn test_oga_vorbis() { + file_ref_save("empty_vorbis.oga", FileType::Vorbis); +} + +#[test_log::test] +fn test_ape() { + file_ref_save("mac-399.ape", FileType::Ape); +} + +#[test_log::test] +fn test_aiff_1() { + file_ref_save("empty.aiff", FileType::Aiff); +} + +#[test_log::test] +fn test_aiff_2() { + file_ref_save("alaw.aifc", FileType::Aiff); +} + +#[test_log::test] +fn test_wavpack() { + file_ref_save("click.wv", FileType::WavPack); +} + +#[test_log::test] +fn test_opus() { + file_ref_save("correctness_gain_silent_output.opus", FileType::Opus); +} + +#[test_log::test] +fn test_unsupported() { + let f1 = lofty::read_from_path("tests/taglib/data/no-extension"); + match f1 { + Err(err) if matches!(err.kind(), ErrorKind::UnknownFormat) => {}, + _ => panic!("File with no extension got through `read_from_path!`"), + } + + let f2 = lofty::read_from_path("tests/taglib/data/unsupported-extension.xx"); + match f2 { + Err(err) if matches!(err.kind(), ErrorKind::UnknownFormat) => {}, + _ => panic!("File with unsupported extension got through `read_from_path!`"), + } +} + +#[test_log::test] +#[ignore] +fn test_create() { + // Marker test, Lofty does not replicate this API +} + +#[test_log::test] +fn test_audio_properties() { + let file = lofty::read_from_path("tests/taglib/data/xing.mp3").unwrap(); + let properties = file.properties(); + assert_eq!(properties.duration().as_secs(), 2); + assert_eq!(properties.duration().as_millis(), 2064); +} + +#[test_log::test] +#[ignore] +fn test_default_file_extensions() { + // Marker test, Lofty does not replicate this API +} + +use lofty::io::{FileLike, Length, Truncate}; +use lofty::properties::FileProperties; +use rusty_fork::rusty_fork_test; + +rusty_fork_test! { + #[test_log::test] + fn test_file_resolver() { + lofty::config::apply_global_options(GlobalOptions::new().use_custom_resolvers(true)); + + { + let file = lofty::read_from_path("tests/taglib/data/xing.mp3").unwrap(); + assert_eq!(file.file_type(), FileType::Mpeg); + } + + struct DummyResolver; + impl Into for DummyResolver { + fn into(self) -> TaggedFile { + TaggedFile::new(FileType::Vorbis, FileProperties::default(), Vec::new()) + } + } + + impl AudioFile for DummyResolver { + type Properties = (); + + fn read_from(_: &mut R, _: ParseOptions) -> lofty::error::Result + where + R: Read + Seek, + Self: Sized, + { + Ok(Self) + } + + fn save_to(&self, _: &mut F, _: WriteOptions) -> lofty::error::Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error> + { + unimplemented!() + } + + fn properties(&self) -> &Self::Properties { + unimplemented!() + } + + fn contains_tag(&self) -> bool { + unimplemented!() + } + + fn contains_tag_type(&self, _: TagType) -> bool { + unimplemented!() + } + } + + impl FileResolver for DummyResolver { + fn extension() -> Option<&'static str> { + Some("mp3") + } + + fn primary_tag_type() -> TagType { + unimplemented!() + } + + fn supported_tag_types() -> &'static [TagType] { + unimplemented!() + } + + fn guess(_: &[u8]) -> Option { + Some(FileType::Vorbis) + } + } + + lofty::resolve::register_custom_resolver::("Dummy"); + + { + let file = lofty::read_from_path("tests/taglib/data/xing.mp3").unwrap(); + assert_eq!(file.file_type(), FileType::Vorbis); + } + } +} diff --git a/lofty/tests/taglib/test_flac.rs b/lofty/tests/taglib/test_flac.rs new file mode 100644 index 00000000..696c228e --- /dev/null +++ b/lofty/tests/taglib/test_flac.rs @@ -0,0 +1,669 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::io::{Read, Seek, SeekFrom}; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::flac::FlacFile; +use lofty::id3::v2::Id3v2Tag; +use lofty::ogg::{OggPictureStorage, VorbisComments}; +use lofty::picture::{MimeType, Picture, PictureInformation, PictureType}; +use lofty::tag::{Accessor, TagExt}; + +#[test_log::test] +fn test_signature() { + let f = get_file::("tests/taglib/data/no-tags.flac"); + assert_eq!( + format!("{:x}", f.properties().signature()), + "a1b141f766e9849ac3db1030a20a3c77" + ); +} + +#[test_log::test] +#[ignore] +fn test_multiple_comment_blocks() { + // Marker test, Lofty does not replicate TagLib's behavior + // + // TagLib will use the *first* tag in the stream, while we use the latest. +} + +#[test_log::test] +fn test_read_picture() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + let lst = f.pictures(); + assert_eq!(lst.len(), 1); + + let (pic, info) = &lst[0]; + assert_eq!(pic.pic_type(), PictureType::CoverFront); + assert_eq!(info.width, 1); + assert_eq!(info.height, 1); + assert_eq!(info.color_depth, 24); + assert_eq!(info.num_colors, 0); + assert_eq!(pic.mime_type(), Some(&MimeType::Png)); + assert_eq!(pic.description(), Some("A pixel.")); + assert_eq!(pic.data().len(), 150); +} + +#[test_log::test] +fn test_add_picture() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let lst = f.pictures(); + assert_eq!(lst.len(), 1); + + let new_pic = Picture::new_unchecked( + PictureType::CoverBack, + Some(MimeType::Jpeg), + Some(String::from("new image")), + Vec::from("JPEG data"), + ); + let new_pic_info = PictureInformation { + width: 5, + height: 6, + color_depth: 16, + num_colors: 7, + }; + + f.insert_picture(new_pic, Some(new_pic_info)).unwrap(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let lst = f.pictures(); + assert_eq!(lst.len(), 2); + + let (pic, info) = &lst[0]; + assert_eq!(pic.pic_type(), PictureType::CoverFront); + assert_eq!(info.width, 1); + assert_eq!(info.height, 1); + assert_eq!(info.color_depth, 24); + assert_eq!(info.num_colors, 0); + assert_eq!(pic.mime_type(), Some(&MimeType::Png)); + assert_eq!(pic.description(), Some("A pixel.")); + assert_eq!(pic.data().len(), 150); + + let (pic, info) = &lst[1]; + assert_eq!(pic.pic_type(), PictureType::CoverBack); + assert_eq!(info.width, 5); + assert_eq!(info.height, 6); + assert_eq!(info.color_depth, 16); + assert_eq!(info.num_colors, 7); + assert_eq!(pic.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(pic.description(), Some("new image")); + assert_eq!(pic.data(), b"JPEG data"); + } +} + +#[test_log::test] +fn test_replace_picture() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let lst = f.pictures(); + assert_eq!(lst.len(), 1); + + let new_pic = Picture::new_unchecked( + PictureType::CoverBack, + Some(MimeType::Jpeg), + Some(String::from("new image")), + Vec::from("JPEG data"), + ); + let new_pic_info = PictureInformation { + width: 5, + height: 6, + color_depth: 16, + num_colors: 7, + }; + + f.remove_pictures(); + f.insert_picture(new_pic, Some(new_pic_info)).unwrap(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let lst = f.pictures(); + assert_eq!(lst.len(), 1); + + let (pic, info) = &lst[0]; + assert_eq!(pic.pic_type(), PictureType::CoverBack); + assert_eq!(info.width, 5); + assert_eq!(info.height, 6); + assert_eq!(info.color_depth, 16); + assert_eq!(info.num_colors, 7); + assert_eq!(pic.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(pic.description(), Some("new image")); + assert_eq!(pic.data(), b"JPEG data"); + } +} + +#[test_log::test] +fn test_remove_all_pictures() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let lst = f.pictures(); + assert_eq!(lst.len(), 1); + + f.remove_pictures(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let lst = f.pictures(); + assert_eq!(lst.len(), 0); + } +} + +#[test_log::test] +fn test_repeated_save_1() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("Silence") + ); + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from("NEW TITLE")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + + file.rewind().unwrap(); + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("NEW TITLE") + ); + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from("NEW TITLE 2")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("NEW TITLE 2") + ); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("NEW TITLE 2") + ); + } +} + +#[test_log::test] +#[ignore] +fn test_repeated_save_2() { + // Marker test, this test relies on saving an ID3v2 tag in a FLAC file, something Lofty does not and will not support. +} + +// TODO: We don't make use of padding blocks yet +#[test_log::test] +#[ignore] +fn test_repeated_save_3() { + let mut file = temp_file!("tests/taglib/data/no-tags.flac"); + + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = VorbisComments::default(); + tag.set_title(String::from_utf8(vec![b'X'; 8 * 1024]).unwrap()); + f.set_vorbis_comments(tag); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + assert_eq!(file.metadata().unwrap().len(), 12862); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + assert_eq!(file.metadata().unwrap().len(), 12862); +} + +#[test_log::test] +fn test_save_multiple_values() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .insert(String::from("ARTIST"), String::from("artist 1")); + f.vorbis_comments_mut() + .unwrap() + .push(String::from("ARTIST"), String::from("artist 2")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + let mut m = f.vorbis_comments().unwrap().get_all("ARTIST"); + assert_eq!(m.next(), Some("artist 1")); + assert_eq!(m.next(), Some("artist 2")); + assert_eq!(m.next(), None); + } +} + +#[test_log::test] +#[ignore] +fn test_dict() { + // Marker test, Lofty does not replicate the dict API +} + +#[test_log::test] +fn test_properties() { + let mut tag = VorbisComments::default(); + tag.push(String::from("ALBUM"), String::from("Album")); + tag.push(String::from("ALBUMARTIST"), String::from("Album Artist")); + tag.push( + String::from("ALBUMARTISTSORT"), + String::from("Album Artist Sort"), + ); + tag.push(String::from("ALBUMSORT"), String::from("Album Sort")); + tag.push(String::from("ARTIST"), String::from("Artist")); + tag.push(String::from("ARTISTS"), String::from("Artists")); + tag.push(String::from("ARTISTSORT"), String::from("Artist Sort")); + tag.push(String::from("ASIN"), String::from("ASIN")); + tag.push(String::from("BARCODE"), String::from("Barcode")); + tag.push( + String::from("CATALOGNUMBER"), + String::from("Catalog Number 1"), + ); + tag.push( + String::from("CATALOGNUMBER"), + String::from("Catalog Number 2"), + ); + tag.push(String::from("COMMENT"), String::from("Comment")); + tag.push(String::from("DATE"), String::from("2021-01-10")); + tag.push(String::from("DISCNUMBER"), String::from("3/5")); + tag.push(String::from("GENRE"), String::from("Genre")); + tag.push(String::from("ISRC"), String::from("UKAAA0500001")); + tag.push(String::from("LABEL"), String::from("Label 1")); + tag.push(String::from("LABEL"), String::from("Label 2")); + tag.push(String::from("MEDIA"), String::from("Media")); + tag.push( + String::from("MUSICBRAINZ_ALBUMARTISTID"), + String::from("MusicBrainz_AlbumartistID"), + ); + tag.push( + String::from("MUSICBRAINZ_ALBUMID"), + String::from("MusicBrainz_AlbumID"), + ); + tag.push( + String::from("MUSICBRAINZ_ARTISTID"), + String::from("MusicBrainz_ArtistID"), + ); + tag.push( + String::from("MUSICBRAINZ_RELEASEGROUPID"), + String::from("MusicBrainz_ReleasegroupID"), + ); + tag.push( + String::from("MUSICBRAINZ_RELEASETRACKID"), + String::from("MusicBrainz_ReleasetrackID"), + ); + tag.push( + String::from("MUSICBRAINZ_TRACKID"), + String::from("MusicBrainz_TrackID"), + ); + tag.push(String::from("ORIGINALDATE"), String::from("2021-01-09")); + tag.push( + String::from("RELEASECOUNTRY"), + String::from("Release Country"), + ); + tag.push( + String::from("RELEASESTATUS"), + String::from("Release Status"), + ); + tag.push(String::from("RELEASETYPE"), String::from("Release Type")); + tag.push(String::from("SCRIPT"), String::from("Script")); + tag.push(String::from("TITLE"), String::from("Title")); + tag.push(String::from("TRACKNUMBER"), String::from("2")); + tag.push(String::from("TRACKTOTAL"), String::from("4")); + + let mut file = temp_file!("tests/taglib/data/no-tags.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + f.set_vorbis_comments(tag.clone()); + + file.rewind().unwrap(); + f.vorbis_comments() + .unwrap() + .save_to(&mut file, WriteOptions::default()) + .unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + assert_eq!(f.vorbis_comments(), Some(&tag)); + } +} + +#[test_log::test] +fn test_invalid() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + // NOTE: In TagLib, there's a `setProperties` method. This is equivalent. + f.vorbis_comments_mut().unwrap().clear(); + + f.vorbis_comments_mut() + .unwrap() + .push(String::from("H\x00c4\x00d6"), String::from("bla")); + assert!(f.vorbis_comments().unwrap().is_empty()); +} + +#[test_log::test] +fn test_audio_properties() { + let f = get_file::("tests/taglib/data/sinewave.flac"); + + let properties = f.properties(); + assert_eq!(properties.duration().as_secs(), 3); + assert_eq!(properties.duration().as_millis(), 3550); + assert_eq!(properties.audio_bitrate(), 145); + assert_eq!(properties.sample_rate(), 44100); + assert_eq!(properties.channels(), 2); + assert_eq!(properties.bit_depth(), 16); + // TODO + // CPPUNIT_ASSERT_EQUAL(156556ULL, f.audioProperties()->sampleFrames()); + assert_eq!( + format!("{:X}", f.properties().signature()), + "CFE3D9DABADEAB2CBF2CA235274B7F76" + ); +} + +#[test_log::test] +fn test_zero_sized_padding_1() { + let _f = get_file::("tests/taglib/data/zero-sized-padding.flac"); +} + +#[test_log::test] +fn test_zero_sized_padding_2() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from("ABC")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from_utf8(vec![b'X'; 3067]).unwrap()); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let _ = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + } +} + +// TODO: We don't make use of padding blocks yet +#[test_log::test] +#[ignore] +fn test_shrink_padding() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from_utf8(vec![b'X'; 128 * 1024]).unwrap()); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + assert!(file.metadata().unwrap().len() > 128 * 1024); + } + file.rewind().unwrap(); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from("0123456789")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + assert!(file.metadata().unwrap().len() < 8 * 1024); + } +} + +#[test_log::test] +#[ignore] +fn test_save_id3v1() { + // Marker test, this test relies on saving an ID3v1 tag in a FLAC file, something Lofty does not and will not support. +} + +#[test_log::test] +#[ignore] +fn test_update_id3v2() { + // Marker test, this test relies on saving an ID3v2 tag in a FLAC file, something Lofty does not and will not support. +} + +#[test_log::test] +fn test_empty_id3v2() { + let mut file = temp_file!("tests/taglib/data/no-tags.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.set_id3v2(Id3v2Tag::default()); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_none()); + } +} + +// TODO: TagLib doesn't fully remove Vorbis Comments when stripping. It will preserve the vendor string. Should we do the same? +#[test_log::test] +#[ignore] +fn test_strip_tags() { + // NOTE: In the TagLib test suite, this also tests ID3v1 and ID3v2. That is not replicated here. + + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from("XiphComment Title")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.vorbis_comments().is_some()); + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("XiphComment Title") + ); + f.remove_vorbis_comments(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.vorbis_comments().is_some()); + assert!(f.vorbis_comments().unwrap().is_empty()); + assert_eq!( + f.vorbis_comments().unwrap().vendor(), + "reference libFLAC 1.1.0 20030126" + ); + } +} + +#[test_log::test] +fn test_remove_xiph_field() { + let mut file = temp_file!("tests/taglib/data/silence-44-s.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .unwrap() + .set_title(String::from("XiphComment Title")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("XiphComment Title") + ); + let _ = f.vorbis_comments_mut().unwrap().remove("TITLE"); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.vorbis_comments().unwrap().title().is_none()); + } +} + +#[test_log::test] +fn test_empty_seek_table() { + let mut file = temp_file!("tests/taglib/data/empty-seektable.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = VorbisComments::default(); + tag.set_title(String::from("XiphComment Title")); + f.set_vorbis_comments(tag); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.seek(SeekFrom::Start(42)).unwrap(); + assert!(f.vorbis_comments().is_some()); + + let mut data = [0; 4]; + file.read_exact(&mut data).unwrap(); + assert_eq!(data, [3, 0, 0, 0]); + } +} + +#[test_log::test] +fn test_picture_stored_after_comment() { + // Blank.png from https://commons.wikimedia.org/wiki/File:Blank.png + const BLANK_PNG_DATA: &[u8] = &[ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, + 0x52, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x08, 0x06, 0x00, 0x00, 0x00, 0x9D, + 0x74, 0x66, 0x1A, 0x00, 0x00, 0x00, 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, + 0xE9, 0x00, 0x00, 0x00, 0x04, 0x67, 0x41, 0x4D, 0x41, 0x00, 0x00, 0xB1, 0x8F, 0x0B, 0xFC, + 0x61, 0x05, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0E, 0xC3, 0x00, + 0x00, 0x0E, 0xC3, 0x01, 0xC7, 0x6F, 0xA8, 0x64, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, + 0x54, 0x18, 0x57, 0x63, 0xC0, 0x01, 0x18, 0x18, 0x00, 0x00, 0x1A, 0x00, 0x01, 0x82, 0x92, + 0x4D, 0x60, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, + ]; + + let mut file = temp_file!("tests/taglib/data/no-tags.flac"); + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.id3v2().is_none()); + assert!(f.vorbis_comments().is_none()); + assert!(f.pictures().is_empty()); + + let pic = Picture::new_unchecked( + PictureType::CoverFront, + Some(MimeType::Png), + Some(String::from("blank.png")), + BLANK_PNG_DATA.to_vec(), + ); + let pic_information = PictureInformation { + width: 3, + height: 2, + color_depth: 32, + num_colors: 0, + }; + f.insert_picture(pic, Some(pic_information)).unwrap(); + + let mut tag = VorbisComments::default(); + tag.set_title(String::from("Title")); + f.set_vorbis_comments(tag); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.id3v2().is_none()); + assert!(f.vorbis_comments().is_some()); + + let pictures = f.pictures(); + assert_eq!(pictures.len(), 1); + assert_eq!(pictures[0].0.data(), BLANK_PNG_DATA); + assert_eq!(pictures[0].0.pic_type(), PictureType::CoverFront); + assert_eq!(pictures[0].0.mime_type(), Some(&MimeType::Png)); + assert_eq!(pictures[0].0.description(), Some("blank.png")); + assert_eq!(pictures[0].1.width, 3); + assert_eq!(pictures[0].1.height, 2); + assert_eq!(pictures[0].1.color_depth, 32); + assert_eq!(pictures[0].1.num_colors, 0); + assert_eq!( + f.vorbis_comments().unwrap().title().as_deref(), + Some("Title") + ); + } + + const EXPECTED_HEAD_DATA: &[u8] = &[ + b'f', b'L', b'a', b'C', 0x00, 0x00, 0x00, 0x22, 0x12, 0x00, 0x12, 0x00, 0x00, 0x00, 0x0E, + 0x00, 0x00, 0x10, 0x0A, 0xC4, 0x42, 0xF0, 0x00, 0x02, 0x7A, 0xC0, 0xA1, 0xB1, 0x41, 0xF7, + 0x66, 0xE9, 0x84, 0x9A, 0xC3, 0xDB, 0x10, 0x30, 0xA2, 0x0A, 0x3C, 0x77, 0x04, 0x00, 0x00, + 0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, b'T', b'I', + b'T', b'L', b'E', b'=', b'T', b'i', b't', b'l', b'e', 0x06, 0x00, 0x00, 0xA9, 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x09, b'i', b'm', b'a', b'g', b'e', b'/', b'p', b'n', b'g', + 0x00, 0x00, 0x00, 0x09, b'b', b'l', b'a', b'n', b'k', b'.', b'p', b'n', b'g', 0x00, 0x00, + 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x77, + ]; + + let mut file_data = Vec::new(); + file.read_to_end(&mut file_data).unwrap(); + + assert!(file_data.starts_with(EXPECTED_HEAD_DATA)); +} diff --git a/lofty/tests/taglib/test_flacpicture.rs b/lofty/tests/taglib/test_flacpicture.rs new file mode 100644 index 00000000..41a5ea65 --- /dev/null +++ b/lofty/tests/taglib/test_flacpicture.rs @@ -0,0 +1,38 @@ +use lofty::config::ParsingMode; +use lofty::picture::{MimeType, Picture, PictureType}; + +const DATA: &[u8] = &[ + 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x09, 0x69, 0x6D, 0x61, 0x67, 0x65, 0x2F, 0x70, 0x6E, + 0x67, 0x00, 0x00, 0x00, 0x08, 0x41, 0x20, 0x70, 0x69, 0x78, 0x65, 0x6C, 0x2E, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x96, 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, + 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, + 0x53, 0xDE, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00, + 0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49, 0x4D, 0x45, 0x07, + 0xD6, 0x0B, 0x1C, 0x0A, 0x36, 0x06, 0x08, 0x44, 0x3D, 0x32, 0x00, 0x00, 0x00, 0x1D, 0x74, 0x45, + 0x58, 0x74, 0x43, 0x6F, 0x6D, 0x6D, 0x65, 0x6E, 0x74, 0x00, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x54, 0x68, 0x65, 0x20, 0x47, 0x49, 0x4D, 0x50, 0xEF, + 0x64, 0x25, 0x6E, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xFF, + 0xFF, 0x3F, 0x00, 0x05, 0xFE, 0x02, 0xFE, 0xDC, 0xCC, 0x59, 0xE7, 0x00, 0x00, 0x00, 0x00, 0x49, + 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, +]; + +#[test_log::test] +fn test_parse() { + let (picture, info) = Picture::from_flac_bytes(DATA, false, ParsingMode::Strict).unwrap(); + + assert_eq!(picture.pic_type(), PictureType::CoverFront); + assert_eq!(info.width, 1); + assert_eq!(info.height, 1); + assert_eq!(info.color_depth, 24); + assert_eq!(info.num_colors, 0); + assert_eq!(picture.mime_type(), Some(&MimeType::Png)); + assert_eq!(picture.description(), Some("A pixel.")); + assert_eq!(picture.data().len(), 150); +} + +#[test_log::test] +fn test_pass_through() { + let (picture, info) = Picture::from_flac_bytes(DATA, false, ParsingMode::Strict).unwrap(); + assert_eq!(DATA, picture.as_flac_bytes(info, false).as_slice()); +} diff --git a/lofty/tests/taglib/test_id3v1.rs b/lofty/tests/taglib/test_id3v1.rs new file mode 100644 index 00000000..8255b359 --- /dev/null +++ b/lofty/tests/taglib/test_id3v1.rs @@ -0,0 +1,31 @@ +use lofty::id3::v1::GENRES; + +#[test_log::test] +#[ignore] +fn test_strip_whitespace() { + // Marker test, we'd be overstepping to remove trailing whitespace that may be intentional +} + +#[test_log::test] +fn test_genres() { + assert_eq!("Darkwave", GENRES[50]); + assert_eq!( + 100, + GENRES.iter().position(|genre| *genre == "Humour").unwrap() + ); + assert!(GENRES.contains(&"Heavy Metal")); + assert_eq!( + 79, + GENRES + .iter() + .position(|genre| *genre == "Hard Rock") + .unwrap() + ); +} + +#[test_log::test] +#[ignore] +fn test_renamed_genres() { + // Marker test, this covers a change where TagLib deviated from the list of genres available on Wikipedia. + // For now, Lofty has no reason to change. +} diff --git a/lofty/tests/taglib/test_id3v2.rs b/lofty/tests/taglib/test_id3v2.rs new file mode 100644 index 00000000..b3fbe8df --- /dev/null +++ b/lofty/tests/taglib/test_id3v2.rs @@ -0,0 +1,1400 @@ +use crate::temp_file; + +use std::borrow::Cow; +use std::collections::HashMap; +use std::io::{Read, Seek}; + +use lofty::TextEncoding; +use lofty::config::{ParseOptions, ParsingMode, WriteOptions}; +use lofty::file::AudioFile; +use lofty::id3::v2::{ + AttachedPictureFrame, ChannelInformation, ChannelType, CommentFrame, Event, + EventTimingCodesFrame, EventType, ExtendedTextFrame, ExtendedUrlFrame, Frame, FrameFlags, + FrameId, GeneralEncapsulatedObject, Id3v2Tag, Id3v2Version, KeyValueFrame, OwnershipFrame, + PopularimeterFrame, PrivateFrame, RelativeVolumeAdjustmentFrame, SyncTextContentType, + SynchronizedTextFrame, TextInformationFrame, TimestampFormat, TimestampFrame, + UniqueFileIdentifierFrame, UnsynchronizedTextFrame, UrlLinkFrame, +}; +use lofty::mpeg::MpegFile; +use lofty::picture::{MimeType, Picture, PictureType}; +use lofty::tag::items::Timestamp; +use lofty::tag::{Accessor, TagExt}; + +#[test_log::test] +fn test_unsynch_decode() { + let mut file = temp_file!("tests/taglib/data/unsynch.id3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + + assert!(f.id3v2().is_some()); + assert_eq!( + f.id3v2().unwrap().title().as_deref(), + Some("My babe just cares for me") + ); +} + +#[test_log::test] +fn test_downgrade_utf8_for_id3v23_1() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + let f = TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TPE1")), + TextEncoding::UTF8, + String::from("Foo"), + ); + + let mut id3v2 = Id3v2Tag::new(); + id3v2.insert(Frame::Text(f.clone())); + id3v2 + .save_to(&mut file, WriteOptions::new().use_id3v23(true)) + .unwrap(); + + let data = f.as_bytes(true); + assert_eq!(data.len(), 1 + 6 + 2); // NOTE: This does not include frame headers like TagLib does + + let f2 = TextInformationFrame::parse( + &mut &data[..], + FrameId::Valid(Cow::Borrowed("TPE1")), + FrameFlags::default(), + Id3v2Version::V3, + ) + .unwrap() + .unwrap(); + + assert_eq!(f.value, f2.value); + assert_eq!(f2.encoding, TextEncoding::UTF16); +} + +#[test_log::test] +fn test_downgrade_utf8_for_id3v23_2() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + let f = UnsynchronizedTextFrame::new( + TextEncoding::UTF8, + *b"XXX", + String::new(), + String::from("Foo"), + ); + + let mut id3v2 = Id3v2Tag::new(); + id3v2.insert(Frame::UnsynchronizedText(f.clone())); + id3v2 + .save_to(&mut file, WriteOptions::new().use_id3v23(true)) + .unwrap(); + + let data = f.as_bytes(true).unwrap(); + assert_eq!(data.len(), 1 + 3 + 2 + 2 + 6 + 2); // NOTE: This does not include frame headers like TagLib does + + let f2 = + UnsynchronizedTextFrame::parse(&mut &data[..], FrameFlags::default(), Id3v2Version::V3) + .unwrap() + .unwrap(); + + assert_eq!(f2.content, String::from("Foo")); + assert_eq!(f2.encoding, TextEncoding::UTF16); +} + +#[test_log::test] +fn test_utf16be_delimiter() { + let mut f = TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TIT2")), + TextEncoding::UTF16BE, + String::from("Foo\0Bar"), + ); + + let data = f.as_bytes(false); + + let no_bom_be_data = b"\x02\ + \0F\0o\0o\0\0\ + \0B\0a\0r"; + + assert_eq!(data, no_bom_be_data); + f = TextInformationFrame::parse( + &mut &data[..], + FrameId::Valid(Cow::Borrowed("TIT2")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + assert_eq!(f.value, "Foo\0Bar"); +} + +#[test_log::test] +fn test_utf16_delimiter() { + let mut f = TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TIT2")), + TextEncoding::UTF16, + String::from("Foo\0Bar"), + ); + + let data = f.as_bytes(false); + + // TODO: TagLib writes a BOM to every string, making the output identical to `mutli_bom_le_data`, + // rather than `single_bom_le_data` in Lofty's case. Not sure if we should be writing the BOM + // to every string? + let single_bom_le_data = b"\x01\xff\xfe\ + F\0o\0o\0\0\0\ + B\0a\0r\0"; + + assert_eq!(data, single_bom_le_data); + f = TextInformationFrame::parse( + &mut &data[..], + FrameId::Valid(Cow::Borrowed("TIT2")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + assert_eq!(f.value, "Foo\0Bar"); + + let multi_bom_le_data = b"\x01\xff\xfe\ + F\0o\0o\0\0\0\xff\xfe\ + B\0a\0r\0"; + f = TextInformationFrame::parse( + &mut &multi_bom_le_data[..], + FrameId::Valid(Cow::Borrowed("TIT2")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + assert_eq!(f.value, "Foo\0Bar"); + + let multi_bom_be_data = b"\x01\xfe\xff\ + \0F\0o\0o\0\0\xfe\xff\ + \0B\0a\0r"; + f = TextInformationFrame::parse( + &mut &multi_bom_be_data[..], + FrameId::Valid(Cow::Borrowed("TIT2")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + assert_eq!(f.value, "Foo\0Bar"); + + let single_bom_be_data = b"\x01\xfe\xff\ + \0F\0o\0o\0\0\ + \0B\0a\0r"; + f = TextInformationFrame::parse( + &mut &single_bom_be_data[..], + FrameId::Valid(Cow::Borrowed("TIT2")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + assert_eq!(f.value, "Foo\0Bar"); +} + +#[test_log::test] +#[ignore] +fn test_broken_frame1() { + // TODO: Determine if it is worth supporting unsychronized frame sizes in ID3v2.4 + // This is apparently an issue iTunes had at some point in the past. + // let mut file = temp_file!("tests/taglib/data/broken-tenc.id3"); + // let f = MpegFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + // + // assert!(f + // .id3v2() + // .unwrap() + // .contains(&FrameId::Valid(Cow::from("TENC")))); +} + +#[test_log::test] +#[ignore] +fn test_read_string_field() { + // Marker test, this is not an API Lofty replicates +} + +#[test_log::test] +fn test_parse_apic() { + let f = AttachedPictureFrame::parse( + &mut &b"\ + \x00\ + m\x00\ + \x01\ + d\x00\ + \x00"[..], + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap(); + assert_eq!( + f.picture.mime_type(), + Some(&MimeType::Unknown(String::from("m"))) + ); + assert_eq!(f.picture.pic_type(), PictureType::Icon); + assert_eq!(f.picture.description(), Some("d")); +} + +#[test_log::test] +fn test_parse_apic_utf16_bom() { + let f = AttachedPictureFrame::parse( + &mut &b"\ + \x01\x69\x6d\x61\x67\x65\ + \x2f\x6a\x70\x65\x67\x00\x00\xfe\xff\x00\x63\x00\x6f\x00\x76\x00\ + \x65\x00\x72\x00\x2e\x00\x6a\x00\x70\x00\x67\x00\x00\xff\xd8\xff"[..], + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap(); + + assert_eq!(f.picture.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(f.picture.pic_type(), PictureType::Other); + assert_eq!(f.picture.description(), Some("cover.jpg")); + assert_eq!(f.picture.data(), b"\xff\xd8\xff"); +} + +#[test_log::test] +fn test_parse_apicv22() { + let frame = AttachedPictureFrame::parse( + &mut &b"\ + \x00\ + JPG\ + \x01\ + d\x00\ + \x00"[..], + FrameFlags::default(), + Id3v2Version::V2, + ) + .unwrap(); + + assert_eq!(frame.picture.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(frame.picture.pic_type(), PictureType::Icon); + assert_eq!(frame.picture.description(), Some("d")); +} + +#[test_log::test] +fn test_render_apic() { + let f = AttachedPictureFrame::new( + TextEncoding::UTF8, + Picture::new_unchecked( + PictureType::CoverBack, + Some(MimeType::Png), + Some(String::from("Description")), + b"PNG data".to_vec(), + ), + ); + + assert_eq!( + f.as_bytes(Id3v2Version::V4).unwrap(), + b"\ + \x03\ + image/png\x00\ + \x04\ + Description\x00\ + PNG data" + ); +} + +#[test_log::test] +#[ignore] +fn test_dont_render22() { + // Marker test, not sure what's going on here? +} + +#[test_log::test] +fn test_parse_geob() { + let f = GeneralEncapsulatedObject::parse( + b"\ + \x00\ + m\x00\ + f\x00\ + d\x00\ + \x00", + FrameFlags::default(), + ) + .unwrap(); + assert_eq!(f.mime_type.as_deref(), Some("m")); + assert_eq!(f.file_name.as_deref(), Some("f")); + assert_eq!(f.descriptor.as_deref(), Some("d")); +} + +#[test_log::test] +fn test_render_geob() { + let f = GeneralEncapsulatedObject::new( + TextEncoding::Latin1, + Some(String::from("application/octet-stream")), + Some(String::from("test.bin")), + Some(String::from("Description")), + vec![0x01; 3], + ); + + assert_eq!( + f.as_bytes(), + b"\ + \x00\ + application/octet-stream\x00\ + test.bin\x00\ + Description\x00\ + \x01\x01\x01" + ); +} + +#[test_log::test] +fn test_parse_popm() { + let f = PopularimeterFrame::parse( + &mut &b"\ + email@example.com\x00\ + \x02\ + \x00\x00\x00\x03"[..], + FrameFlags::default(), + ) + .unwrap(); + assert_eq!(f.email, "email@example.com"); + assert_eq!(f.rating, 2); + assert_eq!(f.counter, 3); +} + +#[test_log::test] +fn test_parse_popm_without_counter() { + let f = PopularimeterFrame::parse( + &mut &b"\ + email@example.com\x00\ + \x02"[..], + FrameFlags::default(), + ) + .unwrap(); + assert_eq!(f.email, "email@example.com"); + assert_eq!(f.rating, 2); + assert_eq!(f.counter, 0); +} + +#[test_log::test] +fn test_render_popm() { + let f = PopularimeterFrame::new(String::from("email@example.com"), 2, 3); + + assert_eq!( + f.as_bytes().unwrap(), + b"\ + email@example.com\x00\ + \x02\ + \x00\x00\x00\x03" + ); +} + +#[test_log::test] +#[ignore] +fn test_popm_to_string() { + // Marker test, Lofty doesn't have a display impl for Popularimeter +} + +#[test_log::test] +fn test_popm_from_file() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + let f = PopularimeterFrame::new(String::from("email@example.com"), 200, 3); + + { + let mut foo = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.insert(Frame::Popularimeter(f)); + foo.set_id3v2(tag); + foo.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let bar = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + let popm_frame = bar + .id3v2() + .unwrap() + .get(&FrameId::Valid(Cow::Borrowed("POPM"))) + .unwrap(); + let Frame::Popularimeter(popularimeter) = popm_frame else { + unreachable!() + }; + + assert_eq!(popularimeter.email, "email@example.com"); + assert_eq!(popularimeter.rating, 200); + } +} + +#[test_log::test] +#[allow(clippy::float_cmp)] +fn test_parse_relative_volume_frame() { + let f = RelativeVolumeAdjustmentFrame::parse( + &mut &b"\ + ident\x00\ + \x02\ + \x00\x0F\ + \x08\ + \x45"[..], + FrameFlags::default(), + ParsingMode::Strict, + ) + .unwrap() + .unwrap(); + + assert_eq!(f.identification, "ident"); + let front_right = f.channels.get(&ChannelType::FrontRight).unwrap(); + assert_eq!( + f32::from(front_right.volume_adjustment) / 512.0f32, + 15.0f32 / 512.0f32 + ); + assert_eq!(front_right.volume_adjustment, 15); + assert_eq!(front_right.bits_representing_peak, 8); + assert_eq!(front_right.peak_volume, Some(vec![0x45])); + let channels = f.channels; + assert_eq!(channels.len(), 1); +} + +#[test_log::test] +fn test_render_relative_volume_frame() { + let f = RelativeVolumeAdjustmentFrame::new(String::from("ident"), { + let mut m = HashMap::new(); + m.insert( + ChannelType::FrontRight, + ChannelInformation { + channel_type: ChannelType::FrontRight, + volume_adjustment: 15, + bits_representing_peak: 8, + peak_volume: Some(vec![0x45]), + }, + ); + m + }); + + assert_eq!( + f.as_bytes(), + b"\ + ident\x00\ + \x02\ + \x00\x0F\ + \x08\ + \x45" + ); +} + +#[test_log::test] +fn test_parse_unique_file_identifier_frame() { + let f = UniqueFileIdentifierFrame::parse( + &mut &b"\ + owner\x00\ + \x00\x01\x02"[..], + FrameFlags::default(), + ParsingMode::Strict, + ) + .unwrap() + .unwrap(); + + assert_eq!(f.owner, "owner"); + assert_eq!(f.identifier, &[0x00, 0x01, 0x02]); +} + +#[test_log::test] +fn test_parse_empty_unique_file_identifier_frame() { + let f = UniqueFileIdentifierFrame::parse( + &mut &b"\ + \x00\ + "[..], + FrameFlags::default(), + ParsingMode::Strict, + ); + + // NOTE: TagLib considers a missing owner to be valid, we do not + assert!(f.is_err()); +} + +#[test_log::test] +fn test_render_unique_file_identifier_frame() { + let f = UniqueFileIdentifierFrame::new(String::from("owner"), b"\x01\x02\x03".to_vec()); + + assert_eq!( + f.as_bytes(), + b"\ +owner\x00\ +\x01\x02\x03" + ); +} + +#[test_log::test] +fn test_parse_url_link_frame() { + let f = UrlLinkFrame::parse( + &mut &b"http://example.com"[..], + FrameId::Valid(Cow::Borrowed("WPUB")), + FrameFlags::default(), + ) + .unwrap() + .unwrap(); + assert_eq!(f.url(), "http://example.com"); +} + +#[test_log::test] +fn test_render_url_link_frame() { + let f = UrlLinkFrame::parse( + &mut &b"http://example.com"[..], + FrameId::Valid(Cow::Borrowed("WPUB")), + FrameFlags::default(), + ) + .unwrap() + .unwrap(); + assert_eq!(f.as_bytes(), b"http://example.com"); +} + +#[test_log::test] +fn test_parse_user_url_link_frame() { + let f = ExtendedUrlFrame::parse( + &mut &b"\ + \x00\ + foo\x00\ + http://example.com"[..], + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + assert_eq!(f.description, String::from("foo")); + assert_eq!(f.content, String::from("http://example.com")); +} + +#[test_log::test] +fn test_render_user_url_link_frame() { + let f = ExtendedUrlFrame::new( + TextEncoding::Latin1, + String::from("foo"), + String::from("http://example.com"), + ); + + assert_eq!( + f.as_bytes(false), + b"\ + \x00\ + foo\x00\ + http://example.com" + ); +} + +#[test_log::test] +fn test_parse_ownership_frame() { + let f = OwnershipFrame::parse( + &mut &b"\ + \x00\ + GBP1.99\x00\ + 20120905\ + Beatport"[..], + FrameFlags::default(), + ) + .unwrap() + .unwrap(); + + assert_eq!(f.price_paid, "GBP1.99"); + assert_eq!(f.date_of_purchase, "20120905"); + assert_eq!(f.seller, "Beatport"); +} + +#[test_log::test] +fn test_render_ownership_frame() { + let f = OwnershipFrame::new( + TextEncoding::Latin1, + String::from("GBP1.99"), + String::from("20120905"), + String::from("Beatport"), + ); + + assert_eq!( + f.as_bytes(false).unwrap(), + b"\ + \x00\ + GBP1.99\x00\ + 20120905\ + Beatport"[..] + ) +} + +#[test_log::test] +fn test_parse_synchronized_lyrics_frame() { + let f = SynchronizedTextFrame::parse( + b"\ + \x00\ +eng\ +\x02\ +\x01\ +foo\x00\ +Example\x00\ +\x00\x00\x04\xd2\ +Lyrics\x00\ +\x00\x00\x11\xd7", + FrameFlags::default(), + ) + .unwrap(); + + assert_eq!(f.encoding, TextEncoding::Latin1); + assert_eq!(f.language, *b"eng"); + assert_eq!(f.timestamp_format, TimestampFormat::MS); + assert_eq!(f.content_type, SyncTextContentType::Lyrics); + assert_eq!(f.description.as_deref(), Some("foo")); + + assert_eq!(f.content.len(), 2); + assert_eq!(f.content[0].1, "Example"); + assert_eq!(f.content[0].0, 1234); + assert_eq!(f.content[1].1, "Lyrics"); + assert_eq!(f.content[1].0, 4567); +} + +#[test_log::test] +fn test_parse_synchronized_lyrics_frame_with_empty_description() { + let f = SynchronizedTextFrame::parse( + b"\ + \x00\ + eng\ + \x02\ + \x01\ + \x00\ + Example\x00\ + \x00\x00\x04\xd2\ + Lyrics\x00\ + \x00\x00\x11\xd7", + FrameFlags::default(), + ) + .unwrap(); + + assert_eq!(f.encoding, TextEncoding::Latin1); + assert_eq!(f.language, *b"eng"); + assert_eq!(f.timestamp_format, TimestampFormat::MS); + assert_eq!(f.content_type, SyncTextContentType::Lyrics); + assert!(f.description.is_none()); + + assert_eq!(f.content.len(), 2); + assert_eq!(f.content[0].1, "Example"); + assert_eq!(f.content[0].0, 1234); + assert_eq!(f.content[1].1, "Lyrics"); + assert_eq!(f.content[1].0, 4567); +} + +#[test_log::test] +fn test_render_synchronized_lyrics_frame() { + let f = SynchronizedTextFrame::new( + TextEncoding::Latin1, + *b"eng", + TimestampFormat::MS, + SyncTextContentType::Lyrics, + Some(String::from("foo")), + vec![ + (1234, String::from("Example")), + (4567, String::from("Lyrics")), + ], + ); + + assert_eq!( + f.as_bytes().unwrap(), + b"\ + \x00\ + eng\ + \x02\ + \x01\ + foo\x00\ + Example\x00\ + \x00\x00\x04\xd2\ + Lyrics\x00\ + \x00\x00\x11\xd7" + ); +} + +#[test_log::test] +fn test_parse_event_timing_codes_frame() { + let f = EventTimingCodesFrame::parse( + &mut &b"\ + \x02\ + \x02\ + \x00\x00\xf3\x5c\ + \xfe\ + \x00\x36\xee\x80"[..], + FrameFlags::default(), + ) + .unwrap() + .unwrap(); + + assert_eq!(f.timestamp_format, TimestampFormat::MS); + + let sel = f.events; + assert_eq!(sel.len(), 2); + assert_eq!(sel[0].event_type, EventType::IntroStart); + assert_eq!(sel[0].timestamp, 62300); + assert_eq!(sel[1].event_type, EventType::AudioFileEnds); + assert_eq!(sel[1].timestamp, 3_600_000); +} + +#[test_log::test] +fn test_render_event_timing_codes_frame() { + let f = EventTimingCodesFrame::new( + TimestampFormat::MS, + vec![ + Event { + event_type: EventType::IntroStart, + timestamp: 62300, + }, + Event { + event_type: EventType::AudioFileEnds, + timestamp: 3_600_000, + }, + ], + ); + + assert_eq!( + f.as_bytes(), + b"\ + \x02\ + \x02\ + \x00\x00\xf3\x5c\ + \xfe\ + \x00\x36\xee\x80" + ) +} + +#[test_log::test] +fn test_parse_comments_frame() { + let f = CommentFrame::parse( + &mut &b"\x03\ + deu\ + Description\x00\ + Text"[..], + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + assert_eq!(f.encoding, TextEncoding::UTF8); + assert_eq!(f.language, *b"deu"); + assert_eq!(f.description, String::from("Description")); + assert_eq!(f.content, String::from("Text")); +} + +#[test_log::test] +fn test_render_comments_frame() { + let f = CommentFrame::new( + TextEncoding::UTF16, + *b"eng", + String::from("Description"), + String::from("Text"), + ); + + assert_eq!( + f.as_bytes(false).unwrap(), + b"\ + \x01\ + eng\ + \xff\xfeD\0e\0s\0c\0r\0i\0p\0t\0i\0o\0n\0\x00\x00\ + \xff\xfeT\0e\0x\0t\0" + ); +} + +#[test_log::test] +#[ignore] +fn test_parse_podcast_frame() { + // Marker test, Lofty doesn't have dedicated support for PCST frames, it seems unnecessary +} + +#[test_log::test] +#[ignore] +fn test_render_podcast_frame() { + // Marker test, Lofty doesn't have dedicated support for PCST frames, it seems unnecessary +} + +#[test_log::test] +fn test_parse_private_frame() { + let f = PrivateFrame::parse( + &mut &b"\ + WM/Provider\x00\ + TL"[..], + FrameFlags::default(), + ) + .unwrap() + .unwrap(); + + assert_eq!(f.owner, "WM/Provider"); + assert_eq!(f.private_data, b"TL"); +} + +#[test_log::test] +fn test_render_private_frame() { + let f = PrivateFrame::new(String::from("WM/Provider"), b"TL".to_vec()); + + assert_eq!( + f.as_bytes().unwrap(), + b"\ + WM/Provider\x00\ + TL" + ); +} + +#[test_log::test] +fn test_parse_user_text_identification_frame() { + let frame_without_description = ExtendedUrlFrame::parse( + &mut &b"\ + \x00\ + \x00\ + Text"[..], + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + assert_eq!(frame_without_description.description, String::new()); + assert_eq!(frame_without_description.content, String::from("Text")); + + let frame_with_description = ExtendedUrlFrame::parse( + &mut &b"\ + \x00\ + Description\x00\ + Text"[..], + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + assert_eq!( + frame_with_description.description, + String::from("Description") + ); + assert_eq!(frame_with_description.content, String::from("Text")); +} + +#[test_log::test] +fn test_render_user_text_identification_frame() { + let mut f = ExtendedTextFrame::new(TextEncoding::Latin1, String::new(), String::from("Text")); + + assert_eq!( + f.as_bytes(false), + b"\ + \x00\ + \x00\ + Text" + ); + + f.description = String::from("Description"); + + assert_eq!( + f.as_bytes(false), + b"\ + \x00\ + Description\x00\ + Text" + ); +} + +// TODO: iTunes, being the great application it is writes unsynchronized integers for sizes. There's no *great* way to detect this. +#[test_log::test] +#[ignore] +fn test_itunes_24_frame_size() { + let mut file = temp_file!("tests/taglib/data/005411.id3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + + assert!( + f.id3v2() + .unwrap() + .contains(&FrameId::Valid(Cow::from("TIT2"))) + ); + assert_eq!( + f.id3v2() + .unwrap() + .get_text(&FrameId::Valid(Cow::Borrowed("TIT2"))) + .unwrap(), + "Sunshine Superman" + ); +} + +#[test_log::test] +fn test_save_utf16_comment() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + // NOTE: You can change the default encoding in TagLib, Lofty does not support this + { + let mut foo = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.insert(Frame::Comment(CommentFrame::new( + TextEncoding::UTF16, + *b"eng", + String::new(), + String::from("Test comment!"), + ))); + foo.set_id3v2(tag); + foo.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let bar = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!( + bar.id3v2().unwrap().comment().as_deref(), + Some("Test comment!") + ); + } +} + +// TODO: Probably won't ever support this, it's a weird edge case with +// duplicate genres. That can be up to the caller to figure out. +#[test_log::test] +#[ignore] +fn test_update_genre_23_1() { + // "Refinement" is the same as the ID3v1 genre - duplicate + let frame_value = TextInformationFrame::parse( + &mut &b"\x00\ + (22)Death Metal"[..], + FrameId::Valid(Cow::Borrowed("TCON")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.insert(Frame::Text(frame_value)); + + let mut genres = tag.genres().unwrap(); + assert_eq!(genres.next(), Some("Death Metal")); + assert!(genres.next().is_none()); + + assert_eq!(tag.genre().as_deref(), Some("Death Metal")); +} + +#[test_log::test] +fn test_update_genre23_2() { + // "Refinement" is different from the ID3v1 genre + let frame_value = TextInformationFrame::parse( + &mut &b"\x00\ + (4)Eurodisco"[..], + FrameId::Valid(Cow::Borrowed("TCON")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.insert(Frame::Text(frame_value)); + + let mut genres = tag.genres().unwrap(); + assert_eq!(genres.next(), Some("Disco")); + assert_eq!(genres.next(), Some("Eurodisco")); + assert!(genres.next().is_none()); + + assert_eq!(tag.genre().as_deref(), Some("Disco / Eurodisco")); +} + +#[test_log::test] +fn test_update_genre23_3() { + // Multiple references and a refinement + let frame_value = TextInformationFrame::parse( + &mut &b"\x00\ + (9)(138)Viking Metal"[..], + FrameId::Valid(Cow::Borrowed("TCON")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.insert(Frame::Text(frame_value)); + + let mut genres = tag.genres().unwrap(); + assert_eq!(genres.next(), Some("Metal")); + assert_eq!(genres.next(), Some("Black Metal")); + assert_eq!(genres.next(), Some("Viking Metal")); + assert!(genres.next().is_none()); + + assert_eq!( + tag.genre().as_deref(), + Some("Metal / Black Metal / Viking Metal") + ); +} + +#[test_log::test] +fn test_update_genre_24() { + let frame_value = TextInformationFrame::parse( + &mut &b"\x00\ + 14\0Eurodisco"[..], + FrameId::Valid(Cow::Borrowed("TCON")), + FrameFlags::default(), + Id3v2Version::V4, + ) + .unwrap() + .unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.insert(Frame::Text(frame_value)); + + assert_eq!(tag.genre().as_deref(), Some("R&B / Eurodisco")); +} + +#[test_log::test] +fn test_update_date22() { + let mut file = temp_file!("tests/taglib/data/id3v22-tda.mp3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_some()); + assert_eq!(f.id3v2().unwrap().year(), Some(2010)); +} + +// TODO: Determine if this is even worth doing. It is just combining TYE+TDA when upgrading ID3v2.2 to 2.4 +#[test_log::test] +#[ignore] +fn test_update_full_date22() { + let mut file = temp_file!("tests/taglib/data/id3v22-tda.mp3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_some()); + assert_eq!( + f.id3v2() + .unwrap() + .get_text(&FrameId::Valid(Cow::Borrowed("TDRC"))) + .unwrap(), + "2010-04-03" + ); +} + +#[test_log::test] +fn test_downgrade_to_23() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + { + let mut id3v2 = Id3v2Tag::new(); + + id3v2.insert(Frame::Timestamp(TimestampFrame::new( + FrameId::Valid(Cow::Borrowed("TDOR")), + TextEncoding::Latin1, + Timestamp::parse(&mut &b"2011-03-16"[..], ParsingMode::Strict) + .unwrap() + .unwrap(), + ))); + + id3v2.insert(Frame::Timestamp(TimestampFrame::new( + FrameId::Valid(Cow::Borrowed("TDRC")), + TextEncoding::Latin1, + Timestamp::parse(&mut &b"2012-04-17T12:01"[..], ParsingMode::Strict) + .unwrap() + .unwrap(), + ))); + + id3v2.insert(Frame::KeyValue(KeyValueFrame::new( + FrameId::Valid(Cow::Borrowed("TMCL")), + TextEncoding::Latin1, + vec![ + (String::from("Guitar"), String::from("Artist 1")), + (String::from("Drums"), String::from("Artist 2")), + ], + ))); + + id3v2.insert(Frame::KeyValue(KeyValueFrame::new( + FrameId::Valid(Cow::Borrowed("TIPL")), + TextEncoding::Latin1, + vec![ + (String::from("Producer"), String::from("Artist 3")), + (String::from("Mastering"), String::from("Artist 4")), + ], + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TCON")), + TextEncoding::Latin1, + String::from("51\039\0Power Noise"), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TDRL")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TDTG")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TMOO")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TPRO")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TSOA")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TSOT")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TSST")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2.insert(Frame::Text(TextInformationFrame::new( + FrameId::Valid(Cow::Borrowed("TSOP")), + TextEncoding::Latin1, + String::new(), + ))); + + id3v2 + .save_to(&mut file, WriteOptions::new().use_id3v23(true)) + .unwrap(); + } + file.rewind().unwrap(); + { + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_some()); + + let id3v2 = f.id3v2().unwrap(); + let tf = id3v2.get(&FrameId::Valid(Cow::Borrowed("TDOR"))).unwrap(); + let Frame::Timestamp(TimestampFrame { timestamp, .. }) = tf else { + unreachable!() + }; + assert_eq!(timestamp.to_string(), "2011"); + + let tf = id3v2.get(&FrameId::Valid(Cow::Borrowed("TDRC"))).unwrap(); + let Frame::Timestamp(TimestampFrame { timestamp, .. }) = tf else { + unreachable!() + }; + assert_eq!(timestamp.to_string(), "2012-04-17T12:01"); + + let tf = id3v2.get(&FrameId::Valid(Cow::Borrowed("TIPL"))).unwrap(); + let Frame::KeyValue(KeyValueFrame { + key_value_pairs, .. + }) = tf + else { + unreachable!() + }; + assert_eq!(key_value_pairs.len(), 4); + assert_eq!( + key_value_pairs[0], + (String::from("Guitar"), String::from("Artist 1")) + ); + assert_eq!( + key_value_pairs[1], + (String::from("Drums"), String::from("Artist 2")) + ); + assert_eq!( + key_value_pairs[2], + (String::from("Producer"), String::from("Artist 3")) + ); + assert_eq!( + key_value_pairs[3], + (String::from("Mastering"), String::from("Artist 4")) + ); + + // NOTE: Lofty upgrades the first genre (originally 51) to "Techno-Industrial" + // TagLib retains the original genre index. + let tf = id3v2.genres().unwrap().collect::>(); + assert_eq!(tf.join("\0"), "Techno-Industrial\0Noise\0Power Noise"); + + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TDRL")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TDTG")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TMOO")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TPRO")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSOA")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSOT")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSST")))); + assert!(!id3v2.contains(&FrameId::Valid(Cow::Borrowed("TSOP")))); + } + file.rewind().unwrap(); + { + #[allow(clippy::items_after_statements)] + const EXPECTED_ID3V23_DATA: &[u8] = b"ID3\x03\x00\x00\x00\x00\x09\x28\ + TORY\x00\x00\x00\x05\x00\x00\x002011\ + TYER\x00\x00\x00\x05\x00\x00\x002012\ + TDAT\x00\x00\x00\x05\x00\x00\x001704\ + TIME\x00\x00\x00\x05\x00\x00\x001201\ + TCON\x00\x00\x00\x14\x00\x00\x00(51)(39)Power Noise\ + IPLS\x00\x00\x00\x44\x00\x00\x00Guitar\x00\ + Artist 1\x00Drums\x00Artist 2\x00Producer\x00\ + Artist 3\x00Mastering\x00Artist 4"; + + let mut file_id3v2 = vec![0; EXPECTED_ID3V23_DATA.len()]; + file.read_exact(&mut file_id3v2).unwrap(); + assert_eq!(file_id3v2.as_slice(), EXPECTED_ID3V23_DATA); + } + { + let mut file = temp_file!("tests/taglib/data/rare_frames.mp3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_some()); + file.rewind().unwrap(); + f.save_to(&mut file, WriteOptions::new().use_id3v23(true)) + .unwrap(); + + file.rewind().unwrap(); + let mut file_content = Vec::new(); + file.read_to_end(&mut file_content).unwrap(); + + let tcon_pos = file_content.windows(4).position(|w| w == b"TCON").unwrap(); + let tcon = &file_content[tcon_pos + 11..]; + assert_eq!(&tcon[..4], &b"(13)"[..]); + } +} + +#[test_log::test] +fn test_compressed_frame_with_broken_length() { + let mut file = temp_file!("tests/taglib/data/compressed_id3_frame.mp3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + assert!( + f.id3v2() + .unwrap() + .contains(&FrameId::Valid(Cow::from("APIC"))) + ); + + let frame = f + .id3v2() + .unwrap() + .get(&FrameId::Valid(Cow::Borrowed("APIC"))) + .unwrap(); + let Frame::Picture(AttachedPictureFrame { picture, .. }) = frame else { + unreachable!() + }; + + assert_eq!(picture.mime_type(), Some(&MimeType::Bmp)); + assert_eq!(picture.pic_type(), PictureType::Other); + assert!(picture.description().is_none()); + assert_eq!(picture.data().len(), 86414); +} + +#[test_log::test] +fn test_w000() { + let mut file = temp_file!("tests/taglib/data/w000.mp3"); + let f = MpegFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + + assert!( + f.id3v2() + .unwrap() + .contains(&FrameId::Valid(Cow::from("W000"))) + ); + let frame = f + .id3v2() + .unwrap() + .get(&FrameId::Valid(Cow::Borrowed("W000"))) + .unwrap(); + let Frame::Url(url_frame) = frame else { + unreachable!() + }; + assert_eq!(url_frame.url(), "lukas.lalinsky@example.com____"); +} + +#[test_log::test] +#[ignore] +fn test_property_interface() { + // Marker test, Lofty does not replicate the property interface +} + +#[test_log::test] +#[ignore] +fn test_property_interface2() { + // Marker test, Lofty does not replicate the property interface +} + +#[test_log::test] +#[ignore] +fn test_properties_movement() { + // Marker test, Lofty does not replicate the property interface. + // Outside of that, this is simply a text frame parsing test, which is redundant. +} + +#[test_log::test] +#[ignore] +fn test_property_grouping() { + // Marker test, Lofty does not replicate the property interface. + // Outside of that, this is simply a text frame parsing test, which is redundant. +} + +#[test_log::test] +fn test_delete_frame() { + let mut file = temp_file!("tests/taglib/data/rare_frames.mp3"); + + { + let mut f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let t = f.id3v2_mut().unwrap(); + let _ = t.remove(&FrameId::Valid(Cow::Borrowed("TCON"))); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f2 = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + let t = f2.id3v2().unwrap(); + assert!(!t.contains(&FrameId::Valid(Cow::from("TCON")))); + } +} + +#[test_log::test] +fn test_save_and_strip_id3v1_should_not_add_frame_from_id3v1_to_id3v2() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + { + let mut foo = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = Id3v2Tag::new(); + tag.set_artist(String::from("Artist")); + foo.set_id3v2(tag); + foo.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut bar = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let _ = bar + .id3v2_mut() + .unwrap() + .remove(&FrameId::Valid(Cow::Borrowed("TPE1"))); + + bar.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_none()); +} + +// TODO: Support CHAP frames (#189) +#[test_log::test] +#[ignore] +fn test_parse_chapter_frame() {} + +// TODO: Support CHAP frames (#189) +#[test_log::test] +#[ignore] +fn test_render_chapter_frame() {} + +// TODO: Support CTOC frames (#189) +#[test_log::test] +#[ignore] +fn test_parse_table_of_contents_frame() {} + +// TODO: Support CTOC frames (#189) +#[test_log::test] +#[ignore] +fn test_render_table_of_contents_frame() {} + +#[test_log::test] +#[ignore] +fn test_empty_frame() { + // Marker test, Lofty will not remove empty frames, as they can be valid +} + +#[test_log::test] +#[ignore] +fn test_duplicate_tags() { + // Marker test, Lofty will combine duplicated tags +} + +// TODO: Support CTOC frames (#189) +#[test_log::test] +#[ignore] +fn test_parse_toc_frame_with_many_children() {} diff --git a/lofty/tests/taglib/test_info.rs b/lofty/tests/taglib/test_info.rs new file mode 100644 index 00000000..a123806c --- /dev/null +++ b/lofty/tests/taglib/test_info.rs @@ -0,0 +1,30 @@ +use lofty::iff::wav::RiffInfoList; +use lofty::tag::Accessor; + +#[test_log::test] +fn test_title() { + let mut tag = RiffInfoList::default(); + + assert!(tag.title().is_none()); + tag.set_title(String::from("Test title 1")); + tag.insert(String::from("TEST"), String::from("Dummy Text")); + + assert_eq!(tag.title().as_deref(), Some("Test title 1")); + assert_eq!(tag.get("INAM"), Some("Test title 1")); + assert_eq!(tag.get("TEST"), Some("Dummy Text")); +} + +#[test_log::test] +fn test_numeric_fields() { + let mut tag = RiffInfoList::default(); + + assert!(tag.track().is_none()); + tag.set_track(1234); + assert_eq!(tag.track(), Some(1234)); + assert_eq!(tag.get("IPRT"), Some("1234")); + + assert!(tag.year().is_none()); + tag.set_year(1234); + assert_eq!(tag.year(), Some(1234)); + assert_eq!(tag.get("ICRD"), Some("1234")); +} diff --git a/lofty/tests/taglib/test_mp4.rs b/lofty/tests/taglib/test_mp4.rs new file mode 100644 index 00000000..7df7738b --- /dev/null +++ b/lofty/tests/taglib/test_mp4.rs @@ -0,0 +1,504 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::borrow::Cow; +use std::io::{Read, Seek}; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::mp4::{Atom, AtomData, AtomIdent, Ilst, Mp4Codec, Mp4File}; +use lofty::picture::{MimeType, Picture, PictureType}; +use lofty::tag::{Accessor, TagExt, TagType}; + +#[test_log::test] +fn test_properties_aac() { + let f = get_file::("tests/taglib/data/has-tags.m4a"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3708); + assert_eq!(f.properties().audio_bitrate(), 3); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + // NOTE: TagLib reports 16, but the stream is a lossy codec. We ignore it in this case. + assert!(f.properties().bit_depth().is_none()); + assert!(!f.properties().is_drm_protected()); + assert_eq!(f.properties().codec(), &Mp4Codec::AAC); +} + +#[test_log::test] +#[allow(clippy::needless_range_loop)] +fn test_properties_aac_without_bitrate() { + let mut file = temp_file!("tests/taglib/data/has-tags.m4a"); + let mut aac_data = Vec::new(); + file.read_to_end(&mut aac_data).unwrap(); + + assert!(aac_data.len() > 1960); + assert_eq!(&aac_data[1890..1894], b"mp4a"); + for i in 1956..1960 { + // Zero out the bitrate + aac_data[i] = 0; + } + + let f = Mp4File::read_from(&mut std::io::Cursor::new(aac_data), ParseOptions::new()).unwrap(); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3708); + assert_eq!(f.properties().audio_bitrate(), 3); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().bit_depth(), None); // TagLib reports 16, but the stream is a lossy codec + assert!(!f.properties().is_drm_protected()); + assert_eq!(f.properties().codec(), &Mp4Codec::AAC); +} + +#[test_log::test] +fn test_properties_alac() { + let f = get_file::("tests/taglib/data/empty_alac.m4a"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3705); + assert_eq!(f.properties().audio_bitrate(), 2); // TagLib is off by one (reports 3) + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().bit_depth(), Some(16)); + assert!(!f.properties().is_drm_protected()); + assert_eq!(f.properties().codec(), &Mp4Codec::ALAC); +} + +#[test_log::test] +#[allow(clippy::needless_range_loop)] +fn test_properties_alac_without_bitrate() { + let mut file = temp_file!("tests/taglib/data/empty_alac.m4a"); + let mut alac_data = Vec::new(); + file.read_to_end(&mut alac_data).unwrap(); + + assert!(alac_data.len() > 474); + assert_eq!(&alac_data[446..450], b"alac"); + for i in 470..474 { + // Zero out the bitrate + alac_data[i] = 0; + } + + let f = Mp4File::read_from(&mut std::io::Cursor::new(alac_data), ParseOptions::new()).unwrap(); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3705); + assert_eq!(f.properties().audio_bitrate(), 2); // TagLib is off by one (reports 3) + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().bit_depth(), Some(16)); + assert!(!f.properties().is_drm_protected()); + assert_eq!(f.properties().codec(), &Mp4Codec::ALAC); +} + +#[test_log::test] +#[ignore] // TODO: FFmpeg reports a bitrate of 95kb/s, we report 104 +fn test_properties_m4v() { + let f = get_file::("tests/taglib/data/blank_video.m4v"); + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 975); + assert_eq!(f.properties().audio_bitrate(), 95); // TagLib is off by one (reports 96) + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().bit_depth(), None); // TagLib reports 16, but the stream is a lossy codec + assert!(!f.properties().is_drm_protected()); + assert_eq!(f.properties().codec(), &Mp4Codec::AAC); +} + +#[test_log::test] +fn test_check_valid() { + let mut file = temp_file!("tests/taglib/data/empty.aiff"); + assert!(Mp4File::read_from(&mut file, ParseOptions::new()).is_err()); +} + +#[test_log::test] +fn test_has_tag() { + { + let f = get_file::("tests/taglib/data/has-tags.m4a"); + assert!(f.ilst().is_some()); + } + + let mut file = temp_file!("tests/taglib/data/no-tags.m4a"); + + { + let mut f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.ilst().is_none()); + let mut tag = Ilst::default(); + tag.set_title(String::from("TITLE")); + f.set_ilst(tag); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.ilst().is_some()); + } +} + +#[test_log::test] +fn test_is_empty() { + let mut t1 = Ilst::default(); + assert!(t1.is_empty()); + t1.set_artist(String::from("Foo")); + assert!(!t1.is_empty()); +} + +#[test_log::test] +#[ignore] // TODO: The atom parsing internals are not exposed yet +fn test_update_stco() { + let mut file = temp_file!("no-tags.3g2"); + + { + let mut f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = Ilst::default(); + tag.set_artist("X".repeat(3000)); + f.set_ilst(tag); + + // Find and collect all `stco` offsets + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let _f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + + // Find and collect all `stco` offsets, compare with previous + } +} + +#[test_log::test] +fn test_freeform() { + let mut file = temp_file!("tests/taglib/data/has-tags.m4a"); + + { + let mut f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.ilst().unwrap().contains(&AtomIdent::Freeform { + mean: Cow::Borrowed("com.apple.iTunes"), + name: Cow::Borrowed("iTunNORM"), + })); + + f.ilst_mut().unwrap().insert(Atom::new( + AtomIdent::Freeform { + mean: Cow::Borrowed("org.kde.TagLib"), + name: Cow::Borrowed("Foo"), + }, + AtomData::UTF8(String::from("Bar")), + )); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.ilst().unwrap().contains(&AtomIdent::Freeform { + mean: Cow::Borrowed("org.kde.TagLib"), + name: Cow::Borrowed("Foo"), + })); + assert_eq!( + f.ilst() + .unwrap() + .get(&AtomIdent::Freeform { + mean: Cow::Borrowed("org.kde.TagLib"), + name: Cow::Borrowed("Foo"), + }) + .unwrap() + .data() + .next(), + Some(&AtomData::UTF8(String::from("Bar"))) + ); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } +} + +#[test_log::test] +fn test_save_existing_when_ilst_is_last() { + let mut file = temp_file!("tests/taglib/data/ilst-is-last.m4a"); + + { + let mut f = + Mp4File::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + file.rewind().unwrap(); + + let ilst = f.ilst_mut().unwrap(); + assert_eq!( + ilst.get(&AtomIdent::Freeform { + mean: Cow::Borrowed("com.apple.iTunes"), + name: Cow::Borrowed("replaygain_track_minmax"), + }) + .unwrap() + .data() + .next() + .unwrap(), + &AtomData::UTF8(String::from("82,164")) + ); + assert_eq!(ilst.artist().as_deref(), Some("Pearl Jam")); + ilst.set_comment(String::from("foo")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = Mp4File::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); + let ilst = f.ilst().unwrap(); + + assert_eq!( + ilst.get(&AtomIdent::Freeform { + mean: Cow::Borrowed("com.apple.iTunes"), + name: Cow::Borrowed("replaygain_track_minmax"), + }) + .unwrap() + .data() + .next() + .unwrap(), + &AtomData::UTF8(String::from("82,164")) + ); + assert_eq!(ilst.artist().as_deref(), Some("Pearl Jam")); + assert_eq!(ilst.comment().as_deref(), Some("foo")); + } +} + +#[test_log::test] +#[ignore] +fn test_64bit_atom() { + // Marker test, this just checks the moov atom's length. We don't retain any atoms we don't need. +} + +#[test_log::test] +fn test_gnre() { + let f = get_file::("tests/taglib/data/gnre.m4a"); + assert_eq!(f.ilst().unwrap().genre().as_deref(), Some("Ska")); +} + +#[test_log::test] +fn test_covr_read() { + let f = get_file::("tests/taglib/data/has-tags.m4a"); + let tag = f.ilst().unwrap(); + assert!(tag.contains(&AtomIdent::Fourcc(*b"covr"))); + let mut covrs = tag.get(&AtomIdent::Fourcc(*b"covr")).unwrap().data(); + let Some(AtomData::Picture(picture1)) = covrs.next() else { + unreachable!() + }; + let Some(AtomData::Picture(picture2)) = covrs.next() else { + unreachable!() + }; + + assert!(covrs.next().is_none()); + assert_eq!(picture1.mime_type(), Some(&MimeType::Png)); + assert_eq!(picture1.data().len(), 79); + assert_eq!(picture2.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(picture2.data().len(), 287); +} + +#[test_log::test] +fn test_covr_write() { + let mut file = temp_file!("tests/taglib/data/has-tags.m4a"); + + { + let mut f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let tag = f.ilst_mut().unwrap(); + assert!(tag.contains(&AtomIdent::Fourcc(*b"covr"))); + tag.insert_picture(Picture::new_unchecked( + PictureType::Other, + Some(MimeType::Png), + None, + b"foo".to_vec(), + )); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + let tag = f.ilst().unwrap(); + assert!(tag.contains(&AtomIdent::Fourcc(*b"covr"))); + + let mut covrs = tag.get(&AtomIdent::Fourcc(*b"covr")).unwrap().data(); + let Some(AtomData::Picture(picture1)) = covrs.next() else { + unreachable!() + }; + let Some(AtomData::Picture(picture2)) = covrs.next() else { + unreachable!() + }; + let Some(AtomData::Picture(picture3)) = covrs.next() else { + unreachable!() + }; + + assert!(covrs.next().is_none()); + assert_eq!(picture1.mime_type(), Some(&MimeType::Png)); + assert_eq!(picture1.data().len(), 79); + assert_eq!(picture2.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(picture2.data().len(), 287); + assert_eq!(picture3.mime_type(), Some(&MimeType::Png)); + assert_eq!(picture3.data().len(), 3); + } +} + +#[test_log::test] +fn test_covr_read2() { + let f = get_file::("tests/taglib/data/covr-junk.m4a"); + let tag = f.ilst().unwrap(); + assert!(tag.contains(&AtomIdent::Fourcc(*b"covr"))); + let mut covrs = tag.get(&AtomIdent::Fourcc(*b"covr")).unwrap().data(); + let Some(AtomData::Picture(picture1)) = covrs.next() else { + unreachable!() + }; + let Some(AtomData::Picture(picture2)) = covrs.next() else { + unreachable!() + }; + + assert!(covrs.next().is_none()); + assert_eq!(picture1.mime_type(), Some(&MimeType::Png)); + assert_eq!(picture1.data().len(), 79); + assert_eq!(picture2.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(picture2.data().len(), 287); +} + +#[test_log::test] +#[ignore] +fn test_properties() { + // Marker test, Lofty does not replicate the properties API +} + +#[test_log::test] +#[ignore] +fn test_properties_all_supported() { + // Marker test, Lofty does not replicate the properties API +} + +#[test_log::test] +#[ignore] +fn test_properties_movement() { + // Marker test, Lofty does not replicate the properties API +} + +#[test_log::test] +fn test_fuzzed_file() { + let _f = get_file::("tests/taglib/data/infloop.m4a"); +} + +#[test_log::test] +fn test_repeated_save() { + let mut file = temp_file!("tests/taglib/data/no-tags.m4a"); + let mut f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = Ilst::default(); + tag.set_title(String::from("0123456789")); + f.set_ilst(tag); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + + let mut file_bytes = Vec::new(); + file.read_to_end(&mut file_bytes).unwrap(); + + assert_eq!( + file_bytes + .windows(10) + .position(|window| window == b"0123456789"), + Some(2862) + ); + assert_ne!(file_bytes.get(2863..2873), Some(b"0123456789".as_slice())); +} + +#[test_log::test] +fn test_with_zero_length_atom() { + let f = get_file::("tests/taglib/data/zero-length-mdat.m4a"); + assert_eq!(f.properties().duration().as_millis(), 1115); + assert_eq!(f.properties().sample_rate(), 22050); +} + +#[test_log::test] +#[ignore] +fn test_empty_values_remove_items() { + // Marker test, Lofty treats empty values as valid +} + +#[test_log::test] +fn test_remove_metadata() { + let mut file = temp_file!("tests/taglib/data/no-tags.m4a"); + + { + let mut f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.ilst().is_none()); + let mut tag = Ilst::default(); + assert!(tag.is_empty()); + tag.set_title(String::from("TITLE")); + f.set_ilst(tag); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.ilst().is_some()); + assert!(!f.ilst().unwrap().is_empty()); + TagType::Mp4Ilst.remove_from(&mut file).unwrap(); + } + file.rewind().unwrap(); + { + let f = Mp4File::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.ilst().is_none()); + + let mut original_file_bytes = Vec::new(); + let mut new_file_bytes = Vec::new(); + + let mut original_file = temp_file!("tests/taglib/data/no-tags.m4a"); + original_file.read_to_end(&mut original_file_bytes).unwrap(); + file.read_to_end(&mut new_file_bytes).unwrap(); + + // We need to do some editing, since we preserve the `meta` atom unlike TagLib + + // Remove the `udta` atom, which should be 45 bytes in length + new_file_bytes.splice(2785..2785 + 45, std::iter::empty()); + + // Fix the length of the `moov` atom + new_file_bytes[1500] = 8; + + // Fix the length of the `udta` atom + new_file_bytes[2780] = 8; + + assert_eq!(original_file_bytes, new_file_bytes); + } +} + +#[test_log::test] +fn test_non_full_meta_atom() { + let f = get_file::("tests/taglib/data/non-full-meta.m4a"); + assert!(f.ilst().is_some()); + + let tag = f.ilst().unwrap(); + assert!(tag.contains(&AtomIdent::Fourcc(*b"covr"))); + let mut covrs = tag.get(&AtomIdent::Fourcc(*b"covr")).unwrap().data(); + let Some(AtomData::Picture(picture1)) = covrs.next() else { + unreachable!() + }; + let Some(AtomData::Picture(picture2)) = covrs.next() else { + unreachable!() + }; + + assert!(covrs.next().is_none()); + assert_eq!(picture1.mime_type(), Some(&MimeType::Png)); + assert_eq!(picture1.data().len(), 79); + assert_eq!(picture2.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(picture2.data().len(), 287); + + assert_eq!(tag.artist().as_deref(), Some("Test Artist!!!!")); + assert_eq!( + tag.get(&AtomIdent::Fourcc(*b"\xa9too")) + .unwrap() + .data() + .next() + .unwrap(), + &AtomData::UTF8(String::from("FAAC 1.24")) + ); +} diff --git a/lofty/tests/taglib/test_mpc.rs b/lofty/tests/taglib/test_mpc.rs new file mode 100644 index 00000000..00d4e25a --- /dev/null +++ b/lofty/tests/taglib/test_mpc.rs @@ -0,0 +1,176 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::io::Seek; + +use lofty::ape::ApeTag; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::id3::v1::Id3v1Tag; +use lofty::musepack::{MpcFile, MpcProperties}; +use lofty::probe::Probe; +use lofty::tag::{Accessor, TagExt}; + +#[test_log::test] +fn test_properties_sv8() { + let f = get_file::("tests/taglib/data/sv8_header.mpc"); + + let MpcProperties::Sv8(properties) = f.properties() else { + panic!("Got the wrong properties somehow") + }; + + assert_eq!(properties.version(), 8); + assert_eq!(properties.duration().as_secs(), 1); + assert_eq!(properties.duration().as_millis(), 1497); + // NOTE: TagLib reports 1, but since it's an empty stream, it should be 0 (FFmpeg reports 0) + assert_eq!(properties.average_bitrate(), 0); + assert_eq!(properties.channels(), 2); + assert_eq!(properties.sample_rate(), 44100); + // TODO + // assert_eq!(properties.sample_frames(), 66014); +} + +#[test_log::test] +fn test_properties_sv7() { + let f = get_file::("tests/taglib/data/click.mpc"); + + let MpcProperties::Sv7(properties) = f.properties() else { + panic!("Got the wrong properties somehow") + }; + + assert_eq!(properties.duration().as_secs(), 0); + // NOTE: TagLib reports 70, we report 78 like FFmpeg + assert_eq!(properties.duration().as_millis(), 78); + // No decoder can agree on this, TagLib and FFmpeg report wildly different values. + // We are able to produce the same value as `mpcdec` (the reference Musepack decoder), so + // we'll stick with that. + assert_eq!(properties.average_bitrate(), 206); + assert_eq!(properties.channels(), 2); + assert_eq!(properties.sample_rate(), 44100); + // TODO + // assert_eq!(properties.sample_frames(), 1760); + + assert_eq!(properties.title_gain(), 14221); + assert_eq!(properties.title_peak(), 19848); + assert_eq!(properties.album_gain(), 14221); + assert_eq!(properties.album_peak(), 19848); +} + +#[test_log::test] +fn test_properties_sv5() { + // Marker test, TagLib doesn't seem to produce the correct properties for SV5 +} + +#[test_log::test] +#[ignore] +fn test_properties_sv4() { + // Marker test, TagLib doesn't seem to produce the correct properties for SV4 +} + +#[test_log::test] +fn test_fuzzed_file1() { + let _ = Probe::open("tests/taglib/data/zerodiv.mpc") + .unwrap() + .guess_file_type() + .unwrap(); +} + +#[test_log::test] +fn test_fuzzed_file2() { + let _ = Probe::open("tests/taglib/data/infloop.mpc") + .unwrap() + .guess_file_type() + .unwrap(); +} + +#[test_log::test] +fn test_fuzzed_file3() { + let _ = Probe::open("tests/taglib/data/segfault.mpc") + .unwrap() + .guess_file_type() + .unwrap(); +} + +#[test_log::test] +fn test_fuzzed_file4() { + let _ = Probe::open("tests/taglib/data/segfault2.mpc") + .unwrap() + .guess_file_type() + .unwrap(); +} + +#[test_log::test] +fn test_strip_and_properties() { + let mut file = temp_file!("tests/taglib/data/click.mpc"); + + { + let mut f = MpcFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut ape = ApeTag::new(); + ape.set_title(String::from("APE")); + f.set_ape(ape); + + let mut id3v1 = Id3v1Tag::new(); + id3v1.set_title(String::from("ID3v1")); + f.set_id3v1(id3v1); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = MpcFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert_eq!(f.ape().unwrap().title().as_deref(), Some("APE")); + f.ape_mut().unwrap().clear(); + assert_eq!(f.id3v1().unwrap().title().as_deref(), Some("ID3v1")); + f.id3v1_mut().unwrap().clear(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = MpcFile::read_from(&mut file, ParseOptions::new()).unwrap(); + + assert!(f.ape().is_none()); + assert!(f.id3v1().is_none()); + } +} + +#[test_log::test] +fn test_repeated_save() { + let mut file = temp_file!("tests/taglib/data/click.mpc"); + + { + let mut f = MpcFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert!(f.ape().is_none()); + assert!(f.id3v1().is_none()); + + let mut ape = ApeTag::new(); + ape.set_title(String::from("01234 56789 ABCDE FGHIJ")); + f.set_ape(ape); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + + f.ape_mut().unwrap().set_title(String::from("0")); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + + let mut id3v1 = Id3v1Tag::new(); + id3v1.set_title(String::from("01234 56789 ABCDE FGHIJ")); + f.set_id3v1(id3v1); + f.ape_mut().unwrap().set_title(String::from( + "01234 56789 ABCDE FGHIJ 01234 56789 ABCDE FGHIJ 01234 56789", + )); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = MpcFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.ape().is_some()); + assert!(f.id3v1().is_some()); + } +} diff --git a/lofty/tests/taglib/test_mpeg.rs b/lofty/tests/taglib/test_mpeg.rs new file mode 100644 index 00000000..fdb36d12 --- /dev/null +++ b/lofty/tests/taglib/test_mpeg.rs @@ -0,0 +1,313 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::fs::File; +use std::io::Seek; + +use lofty::ape::ApeTag; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::id3::v1::Id3v1Tag; +use lofty::id3::v2::{Id3v2Tag, Id3v2Version}; +use lofty::mpeg::MpegFile; +use lofty::tag::Accessor; + +#[test_log::test] +fn test_audio_properties_xing_header_cbr() { + let f = get_file::("tests/taglib/data/lame_cbr.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 1887); // TODO: Off by 9 + assert_eq!(f.properties().duration().as_millis(), 1_887_164); + assert_eq!(f.properties().audio_bitrate(), 64); + assert_eq!(f.properties().channels(), 1); + assert_eq!(f.properties().sample_rate(), 44100); + // TODO? + // CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::Xing, f.audioProperties()->xingHeader()->type()); +} + +#[test_log::test] +fn test_audio_properties_xing_header_vbr() { + let f = get_file::("tests/taglib/data/lame_vbr.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 1887); // TODO: Off by 9 + assert_eq!(f.properties().duration().as_millis(), 1_887_164); + assert_eq!(f.properties().audio_bitrate(), 70); + assert_eq!(f.properties().channels(), 1); + assert_eq!(f.properties().sample_rate(), 44100); + // TODO? + // CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::Xing, f.audioProperties()->xingHeader()->type()); +} + +#[test_log::test] +fn test_audio_properties_vbri_header() { + let f = get_file::("tests/taglib/data/rare_frames.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 222); // TODO: Off by 1 + assert_eq!(f.properties().duration().as_millis(), 222_198); + assert_eq!(f.properties().audio_bitrate(), 233); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + // TODO? + // CPPUNIT_ASSERT_EQUAL(MPEG::XingHeader::VBRI, f.audioProperties()->xingHeader()->type()); +} + +#[test_log::test] +fn test_audio_properties_no_vbr_headers() { + let f = get_file::("tests/taglib/data/bladeenc.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3553); + assert_eq!(f.properties().audio_bitrate(), 64); + assert_eq!(f.properties().channels(), 1); + assert_eq!(f.properties().sample_rate(), 44100); + + // NOTE: This test also checks the last frame of the file. That information is not saved + // in Lofty, and it doesn't seem too useful to expose. +} + +#[test_log::test] +fn test_skip_invalid_frames_1() { + let f = get_file::("tests/taglib/data/invalid-frames1.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 392); + assert_eq!(f.properties().audio_bitrate(), 160); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); +} + +#[test_log::test] +#[ignore] // TODO: Duration off by 27ms, as reported by FFmpeg +fn test_skip_invalid_frames_2() { + let f = get_file::("tests/taglib/data/invalid-frames2.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 314); + assert_eq!(f.properties().audio_bitrate(), 192); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); +} + +#[test_log::test] +#[ignore] // TODO: Duration off by 26ms, as reported by FFmpeg +fn test_skip_invalid_frames_3() { + let f = get_file::("tests/taglib/data/invalid-frames3.mp3"); + + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 183); + assert_eq!(f.properties().audio_bitrate(), 362); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); +} + +#[test_log::test] +fn test_version_2_duration_with_xing_header() { + let f = get_file::("tests/taglib/data/mpeg2.mp3"); + assert_eq!(f.properties().duration().as_secs(), 5387); // TODO: Off by 15 + assert_eq!(f.properties().duration().as_millis(), 5_387_285); +} + +#[test_log::test] +fn test_save_id3v24() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + let xxx = "X".repeat(254); + { + let mut f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_none()); + + let mut tag = Id3v2Tag::default(); + tag.set_title(xxx.clone()); + tag.set_artist(String::from("Artist A")); + f.set_id3v2(tag); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(f.id3v2().unwrap().original_version(), Id3v2Version::V4); + assert_eq!(f.id3v2().unwrap().artist().as_deref(), Some("Artist A")); + assert_eq!(f.id3v2().unwrap().title().as_deref(), Some(xxx.as_str())); + } +} + +#[test_log::test] +#[ignore] +fn test_save_id3v24_wrong_param() { + // Marker test, Lofty does not replicate the TagLib saving API +} + +// TODO: We don't yet support writing an ID3v23 tag (#62) +#[test_log::test] +#[ignore] +fn test_save_id3v23() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + let xxx = "X".repeat(254); + { + let mut f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_none()); + + let mut tag = Id3v2Tag::default(); + tag.set_title(xxx.clone()); + tag.set_artist(String::from("Artist A")); + f.set_id3v2(tag); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(f.id3v2().unwrap().original_version(), Id3v2Version::V3); + assert_eq!(f.id3v2().unwrap().artist().as_deref(), Some("Artist A")); + assert_eq!(f.id3v2().unwrap().title().as_deref(), Some(xxx.as_str())); + } +} + +#[test_log::test] +fn test_duplicate_id3v2() { + let f = get_file::("tests/taglib/data/duplicate_id3v2.mp3"); + assert_eq!(f.properties().sample_rate(), 44100); +} + +#[test_log::test] +fn test_fuzzed_file() { + let mut file = File::open("tests/taglib/data/excessive_alloc.mp3").unwrap(); + assert!(MpegFile::read_from(&mut file, ParseOptions::new()).is_err()) +} + +#[test_log::test] +#[ignore] +fn test_frame_offset() { + // Marker test, Lofty does not replicate this API. Doesn't seem useful to retain frame offsets. +} + +#[test_log::test] +fn test_strip_and_properties() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + { + let mut f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut id3v2 = Id3v2Tag::default(); + id3v2.set_title(String::from("ID3v2")); + f.set_id3v2(id3v2); + let mut ape = ApeTag::default(); + ape.set_title(String::from("APE")); + f.set_ape(ape); + let mut id3v1 = Id3v1Tag::default(); + id3v1.set_title(String::from("ID3v1")); + f.set_id3v1(id3v1); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(f.id3v2().unwrap().title().as_deref(), Some("ID3v2")); + f.remove_id3v2(); + assert_eq!(f.ape().unwrap().title().as_deref(), Some("APE")); + f.remove_ape(); + assert_eq!(f.id3v1().unwrap().title().as_deref(), Some("ID3v1")); + f.remove_id3v1(); + assert!(!f.contains_tag()); + } +} + +#[test_log::test] +fn test_properties() {} + +#[test_log::test] +#[ignore] +fn test_repeated_save_1() { + // Marker test, yet another case of checking frame offsets that Lofty does not expose. +} + +#[test_log::test] +#[ignore] +fn test_repeated_save_2() { + // Marker test, not entirely sure what's even being tested here? +} + +#[test_log::test] +fn test_repeated_save_3() { + let mut file = temp_file!("tests/taglib/data/xing.mp3"); + + { + let mut f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.ape().is_none()); + assert!(f.id3v1().is_none()); + + { + let mut ape = ApeTag::default(); + ape.set_title(String::from("01234 56789 ABCDE FGHIJ")); + f.set_ape(ape); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + f.ape_mut().unwrap().set_title(String::from("0")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + { + let mut id3v1 = Id3v1Tag::default(); + id3v1.set_title(String::from("01234 56789 ABCDE FGHIJ")); + f.set_id3v1(id3v1); + } + file.rewind().unwrap(); + { + f.ape_mut().unwrap().set_title(String::from( + "01234 56789 ABCDE FGHIJ 01234 56789 ABCDE FGHIJ 01234 56789", + )); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + } + file.rewind().unwrap(); + { + let f = MpegFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.ape().is_some()); + assert!(f.id3v1().is_some()); + } +} + +#[test_log::test] +#[ignore] +fn test_empty_id3v2() { + // Marker test, Lofty accepts empty strings as valid values +} + +#[test_log::test] +#[ignore] +fn test_empty_id3v1() { + // Marker test, Lofty accepts empty strings as valid values +} + +#[test_log::test] +#[ignore] +fn test_empty_ape() { + // Marker test, Lofty accepts empty strings as valid values +} + +#[test_log::test] +fn test_ignore_garbage() { + let mut file = temp_file!("tests/taglib/data/garbage.mp3"); + + { + let mut f = + MpegFile::read_from(&mut file, ParseOptions::new().max_junk_bytes(3000)).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_some()); + + assert_eq!(f.id3v2().unwrap().title().as_deref(), Some("Title A")); + f.id3v2_mut().unwrap().set_title(String::from("Title B")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = MpegFile::read_from(&mut file, ParseOptions::new().max_junk_bytes(3000)).unwrap(); + assert!(f.id3v2().is_some()); + assert_eq!(f.id3v2().unwrap().title().as_deref(), Some("Title B")); + } +} diff --git a/lofty/tests/taglib/test_ogaflac.rs b/lofty/tests/taglib/test_ogaflac.rs new file mode 100644 index 00000000..52b23035 --- /dev/null +++ b/lofty/tests/taglib/test_ogaflac.rs @@ -0,0 +1,51 @@ +use crate::temp_file; + +use std::io::{Seek, SeekFrom}; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::flac::FlacFile; +use lofty::ogg::VorbisComments; +use lofty::tag::Accessor; + +// TODO: We don't support FLAC in OGA (#172) +#[test_log::test] +#[ignore] +fn test_framing_bit() { + let mut file = temp_file!("tests/taglib/data/empty_flac.oga"); + + { + let mut f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut vorbis_comments = VorbisComments::new(); + vorbis_comments.set_artist(String::from("The Artist")); + f.set_vorbis_comments(vorbis_comments); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = FlacFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!( + f.vorbis_comments().unwrap().artist().as_deref(), + Some("The Artist") + ); + + assert_eq!(file.seek(SeekFrom::End(0)).unwrap(), 9134); + } +} + +// TODO: We don't support FLAC in OGA (#172) +#[test_log::test] +#[ignore] +fn test_fuzzed_file() { + let mut file = temp_file!("tests/taglib/data/segfault.oga"); + let f = FlacFile::read_from(&mut file, ParseOptions::new()); + assert!(f.is_err()); +} + +#[test_log::test] +#[ignore] +fn test_split_packets() { + // Marker test, Lofty does not retain the packet information +} diff --git a/lofty/tests/taglib/test_ogg.rs b/lofty/tests/taglib/test_ogg.rs new file mode 100644 index 00000000..42daa620 --- /dev/null +++ b/lofty/tests/taglib/test_ogg.rs @@ -0,0 +1,160 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::io::{Read, Seek, SeekFrom}; + +use byteorder::{LittleEndian, ReadBytesExt}; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::ogg::VorbisFile; +use lofty::tag::Accessor; + +#[test_log::test] +fn test_simple() { + let mut file = temp_file!("tests/taglib/data/empty.ogg"); + + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .set_artist(String::from("The Artist")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(f.vorbis_comments().artist().as_deref(), Some("The Artist")); + } +} + +#[test_log::test] +#[ignore] +fn test_split_packets1() { + // Marker test, Lofty doesn't retain packet information +} + +#[test_log::test] +fn test_split_packets2() { + let mut file = temp_file!("tests/taglib/data/empty.ogg"); + + let text = "X".repeat(60890); + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut().set_title(text.clone()); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert_eq!(f.vorbis_comments().title().as_deref(), Some(&*text)); + + f.vorbis_comments_mut().set_title(String::from("ABCDE")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(f.vorbis_comments().title().as_deref(), Some("ABCDE")); + } +} + +#[test_log::test] +#[ignore] +fn test_dict_interface1() { + // Marker test, Lofty doesn't replicate the dictionary interface +} + +#[test_log::test] +#[ignore] +fn test_dict_interface2() { + // Marker test, Lofty doesn't replicate the dictionary interface +} + +#[test_log::test] +fn test_audio_properties() { + let f = get_file::("tests/taglib/data/empty.ogg"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3685); + assert_eq!(f.properties().audio_bitrate(), 112); // TagLib reports 1? That is not correct. + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().version(), 0); + assert_eq!(f.properties().bitrate_max(), 0); + assert_eq!(f.properties().bitrate_nominal(), 112_000); + assert_eq!(f.properties().bitrate_min(), 0); +} + +// TODO: Need to look into this one, not sure why there's a difference in checksums +#[test_log::test] +#[ignore] +fn test_page_checksum() { + let mut file = temp_file!("tests/taglib/data/empty.ogg"); + + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .set_title(String::from("The Artist")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + + file.seek(SeekFrom::Start(0x50)).unwrap(); + assert_eq!(file.read_u32::().unwrap(), 0x3D3B_D92D); + } + file.rewind().unwrap(); + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .set_title(String::from("The Artist 2")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + + file.seek(SeekFrom::Start(0x50)).unwrap(); + assert_eq!(file.read_u32::().unwrap(), 0xD985_291C); + } +} + +#[test_log::test] +fn test_page_granule_position() { + let mut file = temp_file!("tests/taglib/data/empty.ogg"); + + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + // Force the Vorbis comment packet to span more than one page and + // check if the granule position is -1 indicating that no packets + // finish on this page. + f.vorbis_comments_mut().set_comment("A".repeat(70000)); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + + file.seek(SeekFrom::Start(0x3A)).unwrap(); + let mut buf = [0; 6]; + file.read_exact(&mut buf).unwrap(); + assert_eq!(buf, *b"OggS\0\0"); + assert_eq!(file.read_i64::().unwrap(), -1); + } + file.rewind().unwrap(); + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + // Use a small Vorbis comment package which ends on the seconds page and + // check if the granule position is zero. + f.vorbis_comments_mut() + .set_comment(String::from("A small comment")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + + file.seek(SeekFrom::Start(0x3A)).unwrap(); + let mut buf = [0; 6]; + file.read_exact(&mut buf).unwrap(); + assert_eq!(buf, *b"OggS\0\0"); + assert_eq!(file.read_i64::().unwrap(), 0); + } +} diff --git a/lofty/tests/taglib/test_opus.rs b/lofty/tests/taglib/test_opus.rs new file mode 100644 index 00000000..591845d2 --- /dev/null +++ b/lofty/tests/taglib/test_opus.rs @@ -0,0 +1,62 @@ +use crate::temp_file; +use crate::util::get_file; + +use std::io::Seek; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::ogg::OpusFile; +use lofty::tag::Accessor; + +#[test_log::test] +fn test_audio_properties() { + let f = get_file::("tests/taglib/data/correctness_gain_silent_output.opus"); + assert_eq!(f.properties().duration().as_secs(), 7); + assert_eq!(f.properties().duration().as_millis(), 7737); + assert_eq!(f.properties().audio_bitrate(), 36); + assert_eq!(f.properties().channels(), 1); + assert_eq!(f.properties().input_sample_rate(), 48000); + assert_eq!(f.properties().version(), 1); +} + +#[test_log::test] +fn test_read_comments() { + let f = get_file::("tests/taglib/data/correctness_gain_silent_output.opus"); + assert_eq!( + f.vorbis_comments().get("ENCODER"), + Some("Xiph.Org Opus testvectormaker") + ); + assert!(f.vorbis_comments().get("TESTDESCRIPTION").is_some()); + assert!(f.vorbis_comments().artist().is_none()); + assert_eq!(f.vorbis_comments().vendor(), "libopus 0.9.11-66-g64c2dd7"); +} + +#[test_log::test] +fn test_write_comments() { + let mut file = temp_file!("tests/taglib/data/correctness_gain_silent_output.opus"); + + { + let mut f = OpusFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + f.vorbis_comments_mut() + .set_artist(String::from("Your Tester")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = OpusFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!( + f.vorbis_comments().get("ENCODER"), + Some("Xiph.Org Opus testvectormaker") + ); + assert!(f.vorbis_comments().get("TESTDESCRIPTION").is_some()); + assert_eq!(f.vorbis_comments().artist().as_deref(), Some("Your Tester")); + assert_eq!(f.vorbis_comments().vendor(), "libopus 0.9.11-66-g64c2dd7"); + } +} + +#[test_log::test] +#[ignore] +fn test_split_packets() { + // Marker test, Lofty does not retain packet information +} diff --git a/lofty/tests/taglib/test_speex.rs b/lofty/tests/taglib/test_speex.rs new file mode 100644 index 00000000..9b58aa4b --- /dev/null +++ b/lofty/tests/taglib/test_speex.rs @@ -0,0 +1,76 @@ +use crate::temp_file; +use crate::util::get_file; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::ogg::{SpeexFile, VorbisComments}; +use lofty::tag::Accessor; + +use std::io::Seek; + +#[test_log::test] +fn test_audio_properties() { + let f = get_file::("tests/taglib/data/empty.spx"); + + assert_eq!(f.properties().duration().as_secs(), 3); + // TODO: We report 3684, we're off by one + assert_eq!(f.properties().duration().as_millis(), 3685); + // TODO: We report zero, we aren't properly calculating bitrates for Speex + assert_eq!(f.properties().audio_bitrate(), 53); + assert_eq!(f.properties().nominal_bitrate(), -1); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); +} + +// TODO: This test doesn't work, it's very specific with file/packet sizes. Have to determine whether or not to even keep this one. +#[test_log::test] +#[ignore] +fn test_split_packets() { + let mut file = temp_file!("tests/taglib/data/empty.spx"); + + let text = String::from_utf8(vec![b'X'; 128 * 1024]).unwrap(); + + { + let f = SpeexFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut tag = VorbisComments::default(); + tag.set_title(text.clone()); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = SpeexFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert_eq!(file.metadata().unwrap().len(), 156_330); + assert_eq!(f.vorbis_comments().title().as_deref(), Some(text.as_str())); + + // NOTE: TagLib exposes the packets and page headers through `Speex::File`. + // Lofty does not keep this information around, so we just double check with `ogg_pager`. + let packets = ogg_pager::Packets::read(&mut file).unwrap(); + assert_eq!(packets.get(0).unwrap().len(), 80); + assert_eq!(packets.get(1).unwrap().len(), 131_116); + assert_eq!(packets.get(2).unwrap().len(), 93); + assert_eq!(packets.get(3).unwrap().len(), 93); + + assert_eq!(f.properties().duration().as_millis(), 3685); + + f.vorbis_comments_mut().set_title(String::from("ABCDE")); + file.rewind().unwrap(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = SpeexFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(file.metadata().unwrap().len(), 24317); + assert_eq!(f.vorbis_comments().title().as_deref(), Some("ABCDE")); + + let packets = ogg_pager::Packets::read(&mut file).unwrap(); + assert_eq!(packets.get(0).unwrap().len(), 80); + assert_eq!(packets.get(1).unwrap().len(), 49); + assert_eq!(packets.get(2).unwrap().len(), 93); + assert_eq!(packets.get(3).unwrap().len(), 93); + + assert_eq!(f.properties().duration().as_millis(), 3685); + } +} diff --git a/lofty/tests/taglib/test_synchdata.rs b/lofty/tests/taglib/test_synchdata.rs new file mode 100644 index 00000000..fb16a578 --- /dev/null +++ b/lofty/tests/taglib/test_synchdata.rs @@ -0,0 +1,91 @@ +use std::io::Read; + +use lofty::id3::v2::util::synchsafe::{SynchsafeInteger, UnsynchronizedStream}; + +#[test_log::test] +fn test1() { + let v = u32::from_be_bytes([0, 0, 0, 127]); + + assert_eq!(v.unsynch(), 127); + assert_eq!(127u32.synch().unwrap(), v); +} + +#[test_log::test] +fn test2() { + let v = u32::from_be_bytes([0, 0, 1, 0]); + + assert_eq!(v.unsynch(), 128); + assert_eq!(128u32.synch().unwrap(), v); +} + +#[test_log::test] +fn test3() { + let v = u32::from_be_bytes([0, 0, 1, 1]); + + assert_eq!(v.unsynch(), 129); + assert_eq!(129u32.synch().unwrap(), v); +} + +#[test_log::test] +#[ignore] +fn test_to_uint_broken() { + // Marker test, this behavior is not replicated in Lofty +} + +#[test_log::test] +#[ignore] +fn test_to_uint_broken_and_too_large() { + // Marker test, this behavior is not replicated in Lofty +} + +#[test_log::test] +fn test_decode1() { + let a = [0xFFu8, 0x00u8, 0x00u8]; + + let mut a2 = Vec::new(); + UnsynchronizedStream::new(&mut &a[..]) + .read_to_end(&mut a2) + .unwrap(); + + assert_eq!(a2.len(), 2); + assert_eq!(a2, &[0xFF, 0x00]); +} + +#[test_log::test] +fn test_decode2() { + let a = [0xFFu8, 0x44u8]; + + let mut a2 = Vec::new(); + UnsynchronizedStream::new(&mut &a[..]) + .read_to_end(&mut a2) + .unwrap(); + + assert_eq!(a2.len(), 2); + assert_eq!(a2, &[0xFF, 0x44]); +} + +#[test_log::test] +fn test_decode3() { + let a = [0xFFu8, 0xFFu8, 0x00u8]; + + let mut a2 = Vec::new(); + UnsynchronizedStream::new(&mut &a[..]) + .read_to_end(&mut a2) + .unwrap(); + + assert_eq!(a2.len(), 2); + assert_eq!(a2, &[0xFFu8, 0xFFu8]); +} + +#[test_log::test] +fn test_decode4() { + let a = [0xFFu8, 0xFFu8, 0xFFu8]; + + let mut a2 = Vec::new(); + UnsynchronizedStream::new(&mut &a[..]) + .read_to_end(&mut a2) + .unwrap(); + + assert_eq!(a2.len(), 3); + assert_eq!(a2, &[0xFFu8, 0xFFu8, 0xFFu8]); +} diff --git a/lofty/tests/taglib/test_wav.rs b/lofty/tests/taglib/test_wav.rs new file mode 100644 index 00000000..8da0d9a5 --- /dev/null +++ b/lofty/tests/taglib/test_wav.rs @@ -0,0 +1,336 @@ +use crate::temp_file; +use crate::util::get_file; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::id3::v2::{Id3v2Tag, Id3v2Version}; +use lofty::iff::wav::{RiffInfoList, WavFile, WavFormat}; +use lofty::tag::{Accessor, TagType}; + +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; + +#[test_log::test] +fn test_pcm_properties() { + let f = get_file::("tests/taglib/data/empty.wav"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3675); + assert_eq!(f.properties().bitrate(), 32); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 1000); + assert_eq!(f.properties().bit_depth(), 16); + // TODO: assert_eq!(f.properties().total_samples(), 3675); + assert_eq!(*f.properties().format(), WavFormat::PCM); +} + +#[test_log::test] +fn test_alaw_properties() { + let f = get_file::("tests/taglib/data/alaw.wav"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3550); + assert_eq!(f.properties().bitrate(), 128); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 8000); + assert_eq!(f.properties().bit_depth(), 8); + // TODO: assert_eq!(f.properties().total_samples(), 28400); + assert_eq!(*f.properties().format(), WavFormat::Other(6)); +} + +#[test_log::test] +fn test_float_properties() { + let f = get_file::("tests/taglib/data/float64.wav"); + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 97); + assert_eq!(f.properties().bitrate(), 5645); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().bit_depth(), 64); + // TODO: assert_eq!(f.properties().total_samples(), 4281); + assert_eq!(*f.properties().format(), WavFormat::IEEE_FLOAT); +} + +#[test_log::test] +fn test_float_without_fact_chunk_properties() { + let mut wav_data = std::fs::read("tests/taglib/data/float64.wav").unwrap(); + assert_eq!(&wav_data[36..40], b"fact"); + + // Remove the fact chunk by renaming it to fakt + wav_data[38] = b'k'; + + let f = WavFile::read_from(&mut Cursor::new(wav_data), ParseOptions::new()).unwrap(); + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 97); + assert_eq!(f.properties().bitrate(), 5645); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 44100); + assert_eq!(f.properties().bit_depth(), 64); + // TODO: assert_eq!(f.properties().total_samples(), 4281); + assert_eq!(*f.properties().format(), WavFormat::IEEE_FLOAT); +} + +#[test_log::test] +fn test_zero_size_data_chunk() { + let mut file = temp_file!("tests/taglib/data/zero-size-chunk.wav"); + let _f = WavFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); +} + +#[test_log::test] +fn test_id3v2_tag() { + let mut file = temp_file!("tests/taglib/data/empty.wav"); + + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_none()); + + let mut id3v2 = Id3v2Tag::default(); + id3v2.set_title(String::from("Title")); + id3v2.set_artist(String::from("Artist")); + f.set_id3v2(id3v2); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + assert!(f.id3v2().is_some()); + } + file.rewind().unwrap(); + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_some()); + + assert_eq!(f.id3v2().unwrap().title().as_deref(), Some("Title")); + assert_eq!(f.id3v2().unwrap().artist().as_deref(), Some("Artist")); + + f.id3v2_mut().unwrap().remove_title(); + f.id3v2_mut().unwrap().remove_artist(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_none()); + } +} + +#[test_log::test] +fn test_save_id3v23() { + let mut file = temp_file!("tests/taglib/data/empty.wav"); + + let xxx = "X".repeat(254); + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_none()); + + let mut id3v2 = Id3v2Tag::new(); + id3v2.set_title(xxx.clone()); + id3v2.set_artist(String::from("Artist A")); + f.set_id3v2(id3v2); + + f.save_to(&mut file, WriteOptions::new().use_id3v23(true)) + .unwrap(); + assert!(f.id3v2().is_some()); + } + file.rewind().unwrap(); + { + let f2 = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + let tag = f2.id3v2().unwrap(); + assert_eq!(tag.original_version(), Id3v2Version::V3); + assert_eq!(tag.artist().as_deref(), Some("Artist A")); + assert_eq!(tag.title().as_deref(), Some(&*xxx)); + } +} + +#[test_log::test] +fn test_info_tag() { + let mut file = temp_file!("tests/taglib/data/empty.wav"); + + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.riff_info().is_none()); + + let mut riff_info = RiffInfoList::default(); + riff_info.set_title(String::from("Title")); + riff_info.set_artist(String::from("Artist")); + f.set_riff_info(riff_info); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.riff_info().is_some()); + assert_eq!(f.riff_info().unwrap().title().as_deref(), Some("Title")); + assert_eq!(f.riff_info().unwrap().artist().as_deref(), Some("Artist")); + + f.riff_info_mut().unwrap().remove_title(); + f.riff_info_mut().unwrap().remove_artist(); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.riff_info().is_none()); + } +} + +#[test_log::test] +fn test_strip_tags() { + let mut file = temp_file!("tests/taglib/data/empty.wav"); + + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut id3v2 = Id3v2Tag::default(); + id3v2.set_title(String::from("test title")); + f.set_id3v2(id3v2); + + let mut riff_info = RiffInfoList::default(); + riff_info.set_title(String::from("test title")); + f.set_riff_info(riff_info); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_some()); + assert!(f.riff_info().is_some()); + + TagType::RiffInfo.remove_from(&mut file).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_some()); + assert!(f.riff_info().is_none()); + + let mut riff_info = RiffInfoList::default(); + riff_info.set_title(String::from("test title")); + f.set_riff_info(riff_info); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.id3v2().is_some()); + assert!(f.riff_info().is_some()); + + TagType::Id3v2.remove_from(&mut file).unwrap(); + } + file.rewind().unwrap(); + { + let f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.id3v2().is_none()); + assert!(f.riff_info().is_some()); + } +} + +#[test_log::test] +#[ignore] +fn test_duplicate_tags() { + // Marker test, TagLib will ignore any tag except for the first. Lofty will *not* do this. + // Every tag in the stream is read and merged into the previous one. Whichever tag ends up being + // the latest in the stream will have precedence. +} + +#[test_log::test] +fn test_fuzzed_file1() { + let f1 = get_file::("tests/taglib/data/infloop.wav"); + // The file has problems: + // Chunk 'ISTt' has invalid size (larger than the file size). + // Its properties can nevertheless be read. + let properties = f1.properties(); + assert_eq!(1, properties.channels()); + assert_eq!(88, properties.bitrate()); + assert_eq!(8, properties.bit_depth()); + assert_eq!(11025, properties.sample_rate()); + assert!(f1.riff_info().is_none()); + assert!(f1.id3v2().is_none()); +} + +#[test_log::test] +fn test_fuzzed_file2() { + let mut file = temp_file!("tests/taglib/data/segfault.wav"); + let _f2 = WavFile::read_from(&mut file, ParseOptions::new().read_properties(false)).unwrap(); +} + +#[test_log::test] +fn test_file_with_garbage_appended() { + let mut file = temp_file!("tests/taglib/data/empty.wav"); + let contents_before_modification; + { + file.seek(SeekFrom::End(0)).unwrap(); + + let garbage = b"12345678"; + file.write_all(garbage).unwrap(); + file.rewind().unwrap(); + + let mut file_contents = Vec::new(); + file.read_to_end(&mut file_contents).unwrap(); + + contents_before_modification = file_contents; + } + file.rewind().unwrap(); + { + let mut f = WavFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut id3v2 = Id3v2Tag::default(); + id3v2.set_title(String::from("ID3v2 Title")); + f.set_id3v2(id3v2); + + let mut riff_info = RiffInfoList::default(); + riff_info.set_title(String::from("INFO Title")); + f.set_riff_info(riff_info); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + TagType::Id3v2.remove_from(&mut file).unwrap(); + file.rewind().unwrap(); + TagType::RiffInfo.remove_from(&mut file).unwrap(); + } + file.rewind().unwrap(); + { + let mut contents_after_modification = Vec::new(); + file.read_to_end(&mut contents_after_modification).unwrap(); + assert_eq!(contents_before_modification, contents_after_modification); + } +} + +#[test_log::test] +#[ignore] +fn test_strip_and_properties() { + // Marker test, Lofty does not replicate the properties API +} + +#[test_log::test] +fn test_pcm_with_fact_chunk() { + let f = get_file::("tests/taglib/data/pcm_with_fact_chunk.wav"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3675); + assert_eq!(f.properties().bitrate(), 32); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 1000); + assert_eq!(f.properties().bit_depth(), 16); + // TODO: assert_eq!(f.properties().total_samples(), 3675); + assert_eq!(*f.properties().format(), WavFormat::PCM); +} + +#[test_log::test] +fn test_wave_format_extensible() { + let f = get_file::("tests/taglib/data/uint8we.wav"); + assert_eq!(f.properties().duration().as_secs(), 2); + assert_eq!(f.properties().duration().as_millis(), 2937); + assert_eq!(f.properties().bitrate(), 128); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().sample_rate(), 8000); + assert_eq!(f.properties().bit_depth(), 8); + // TODO: assert_eq!(f.properties().total_samples(), 23493); + assert_eq!(*f.properties().format(), WavFormat::PCM); +} diff --git a/lofty/tests/taglib/test_wavpack.rs b/lofty/tests/taglib/test_wavpack.rs new file mode 100644 index 00000000..05662326 --- /dev/null +++ b/lofty/tests/taglib/test_wavpack.rs @@ -0,0 +1,151 @@ +use crate::temp_file; +use crate::util::get_file; +use std::fs::File; + +use std::io::Seek; + +use lofty::ape::ApeTag; +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::id3::v1::Id3v1Tag; +use lofty::tag::Accessor; +use lofty::wavpack::WavPackFile; + +#[test_log::test] +#[ignore] // TODO: Should we even bother supporting this? FFmpeg also reports zeroed out properties. +fn test_no_length_properties() { + let f = get_file::("tests/taglib/data/no_length.wv"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3705); + assert_eq!(f.properties().audio_bitrate(), 1); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().bit_depth(), 16); + assert!(f.properties().is_lossless()); + assert_eq!(f.properties().sample_rate(), 44100); + // TODO: CPPUNIT_ASSERT_EQUAL(163392U, f.audioProperties()->sampleFrames()); + assert_eq!(f.properties().version(), 1031); +} + +#[test_log::test] +#[ignore] +fn test_multi_channel_properties() { + // Marker test, this is not a valid file and TagLib does not handle it properly. + // + // A multichannel file should make use of the multichannel metadata sub block, which + // this file does not. Even FFMpeg thinks this is a mono file. +} + +#[test_log::test] +fn test_dsd_stereo_properties() { + let f = get_file::("tests/taglib/data/dsd_stereo.wv"); + assert_eq!(f.properties().duration().as_secs(), 0); + assert_eq!(f.properties().duration().as_millis(), 200); + assert_eq!(f.properties().audio_bitrate(), 2096); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().bit_depth(), 8); + assert!(f.properties().is_lossless()); + assert_eq!(f.properties().sample_rate(), 352_800); + // TODO: CPPUNIT_ASSERT_EQUAL(70560U, f.audioProperties()->sampleFrames()); + assert_eq!(f.properties().version(), 1040); +} + +#[test_log::test] +fn test_non_standard_rate_properties() { + let f = get_file::("tests/taglib/data/non_standard_rate.wv"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3675); + assert_eq!(f.properties().audio_bitrate(), 0); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().bit_depth(), 16); + assert!(f.properties().is_lossless()); + assert_eq!(f.properties().sample_rate(), 1000); + // TODO: CPPUNIT_ASSERT_EQUAL(3675U, f.audioProperties()->sampleFrames()); + assert_eq!(f.properties().version(), 1040); +} + +#[test_log::test] +fn test_tagged_properties() { + let f = get_file::("tests/taglib/data/tagged.wv"); + assert_eq!(f.properties().duration().as_secs(), 3); + assert_eq!(f.properties().duration().as_millis(), 3550); + assert_eq!(f.properties().audio_bitrate(), 172); + assert_eq!(f.properties().channels(), 2); + assert_eq!(f.properties().bit_depth(), 16); + assert!(!f.properties().is_lossless()); + assert_eq!(f.properties().sample_rate(), 44100); + // TODO: CPPUNIT_ASSERT_EQUAL(156556U, f.audioProperties()->sampleFrames()); + assert_eq!(f.properties().version(), 1031); +} + +#[test_log::test] +fn test_fuzzed_file() { + let mut f = File::open("tests/taglib/data/infloop.wv").unwrap(); + assert!(WavPackFile::read_from(&mut f, ParseOptions::new()).is_err()); +} + +#[test_log::test] +fn test_strip_and_properties() { + let mut file = temp_file!("tests/taglib/data/click.wv"); + + { + let mut f = WavPackFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let mut ape = ApeTag::default(); + ape.set_title(String::from("APE")); + f.set_ape(ape); + + let mut id3v1 = Id3v1Tag::default(); + id3v1.set_title(String::from("ID3v1")); + f.set_id3v1(id3v1); + + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + // NOTE: This is not the same as the TagLib test. + // Their test checks the first "TITLE" which changes when tags are stripped. + let mut f = WavPackFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert_eq!(f.ape().unwrap().title().as_deref(), Some("APE")); + f.remove_ape(); + assert_eq!(f.id3v1().unwrap().title.as_deref(), Some("ID3v1")); + f.remove_id3v1(); + assert!(!f.contains_tag()); + } +} + +#[test_log::test] +fn test_repeated_save() { + let mut file = temp_file!("tests/taglib/data/click.wv"); + + { + let mut f = WavPackFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + assert!(f.ape().is_none()); + assert!(f.id3v1().is_none()); + + let mut ape = ApeTag::default(); + ape.set_title(String::from("01234 56789 ABCDE FGHIJ")); + f.set_ape(ape); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + + f.ape_mut().unwrap().set_title(String::from("0")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + file.rewind().unwrap(); + + let mut id3v1 = Id3v1Tag::default(); + id3v1.set_title(String::from("01234 56789 ABCDE FGHIJ")); + f.set_id3v1(id3v1); + f.ape_mut().unwrap().set_title(String::from( + "01234 56789 ABCDE FGHIJ 01234 56789 ABCDE FGHIJ 01234 56789", + )); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = WavPackFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(f.ape().is_some()); + assert!(f.id3v1().is_some()); + } +} diff --git a/lofty/tests/taglib/test_xiphcomment.rs b/lofty/tests/taglib/test_xiphcomment.rs new file mode 100644 index 00000000..3aafd129 --- /dev/null +++ b/lofty/tests/taglib/test_xiphcomment.rs @@ -0,0 +1,168 @@ +use crate::temp_file; + +use std::io::Seek; + +use lofty::config::{ParseOptions, WriteOptions}; +use lofty::file::AudioFile; +use lofty::ogg::{OggPictureStorage, VorbisComments, VorbisFile}; +use lofty::picture::{MimeType, Picture, PictureInformation, PictureType}; +use lofty::tag::{Accessor, TagExt}; + +#[test_log::test] +fn test_year() { + let mut cmt = VorbisComments::default(); + assert_eq!(cmt.year(), None); + cmt.push(String::from("YEAR"), String::from("2009")); + assert_eq!(cmt.year(), Some(2009)); + + // NOTE: Lofty will *always* prioritize "YEAR" over "DATE". TagLib doesn't have the same ideas, + // so we have to remove "YEAR". + let _ = cmt.remove("YEAR"); + + cmt.push(String::from("DATE"), String::from("2008")); + assert_eq!(cmt.year(), Some(2008)); +} + +#[test_log::test] +fn test_set_year() { + let mut cmt = VorbisComments::default(); + cmt.push(String::from("YEAR"), String::from("2009")); + cmt.push(String::from("DATE"), String::from("2008")); + cmt.set_year(1995); + assert!(cmt.get("YEAR").is_none()); + assert_eq!(cmt.get("DATE"), Some("1995")); +} + +#[test_log::test] +fn test_track() { + let mut cmt = VorbisComments::default(); + assert_eq!(cmt.track(), None); + cmt.push(String::from("TRACKNUM"), String::from("7")); + assert_eq!(cmt.track(), Some(7)); + cmt.push(String::from("TRACKNUMBER"), String::from("8")); + assert_eq!(cmt.track(), Some(8)); +} + +#[test_log::test] +fn test_set_track() { + let mut cmt = VorbisComments::default(); + cmt.push(String::from("TRACKNUM"), String::from("7")); + cmt.push(String::from("TRACKNUMBER"), String::from("8")); + cmt.set_track(3); + assert!(cmt.get("TRACKNUM").is_none()); + assert_eq!(cmt.get("TRACKNUMBER"), Some("3")); +} + +#[test_log::test] +#[ignore] +fn test_invalid_keys1() { + // Marker test, Lofty does not replicate the properties API +} + +#[test_log::test] +fn test_invalid_keys2() { + let mut cmt = VorbisComments::default(); + cmt.push(String::new(), String::new()); + cmt.push(String::from("A=B"), String::new()); + cmt.push(String::from("A~B"), String::new()); + cmt.push(String::from("A\x7F"), String::new()); + cmt.push(String::from("A\u{3456}"), String::new()); + + assert!(cmt.is_empty()); +} + +#[test_log::test] +fn test_clear_comment() { + let mut file = temp_file!("tests/taglib/data/empty.ogg"); + + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + f.vorbis_comments_mut() + .push(String::from("COMMENT"), String::from("Comment1")); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + f.vorbis_comments_mut().remove_comment(); + assert_eq!(f.vorbis_comments().comment(), None); + } +} + +#[test_log::test] +#[ignore] +fn test_remove_fields() { + // Marker test, TagLib has some incredibly strange behavior in this test. + // + // When adding a field of the same key, TagLib will append each value to the same value. + // Meaning: + // + // tag.insert("title", "Title1", false); + // tag.insert("title, "Title2", false); + // assert_eq!(tag.title(), Some("Title1 Title2"); + // + // Lofty will never behave in this way. +} + +#[test_log::test] +fn test_picture() { + let mut file = temp_file!("tests/taglib/data/empty.ogg"); + + { + let mut f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + let picture = Picture::new_unchecked( + PictureType::CoverBack, + Some(MimeType::Jpeg), + Some(String::from("new image")), + b"JPEG data".to_vec(), + ); + let info = PictureInformation { + width: 5, + height: 6, + color_depth: 16, + num_colors: 7, + }; + + f.vorbis_comments_mut() + .insert_picture(picture, Some(info)) + .unwrap(); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + let pictures = f.vorbis_comments().pictures(); + assert_eq!(pictures.len(), 1); + assert_eq!(pictures[0].1.width, 5); + assert_eq!(pictures[0].1.height, 6); + assert_eq!(pictures[0].1.color_depth, 16); + assert_eq!(pictures[0].1.num_colors, 7); + assert_eq!(pictures[0].0.mime_type(), Some(&MimeType::Jpeg)); + assert_eq!(pictures[0].0.description(), Some("new image")); + assert_eq!(pictures[0].0.data(), b"JPEG data"); + } +} + +#[test_log::test] +fn test_lowercase_fields() { + let mut file = temp_file!("tests/taglib/data/lowercase-fields.ogg"); + + { + let f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + file.rewind().unwrap(); + + assert_eq!(f.vorbis_comments().title().as_deref(), Some("TEST TITLE")); + assert_eq!(f.vorbis_comments().artist().as_deref(), Some("TEST ARTIST")); + assert_eq!(f.vorbis_comments().pictures().len(), 1); + f.save_to(&mut file, WriteOptions::default()).unwrap(); + } + file.rewind().unwrap(); + { + let f = VorbisFile::read_from(&mut file, ParseOptions::new()).unwrap(); + assert!(!f.vorbis_comments().pictures().is_empty()); + } +} diff --git a/lofty/tests/taglib/util/mod.rs b/lofty/tests/taglib/util/mod.rs new file mode 100644 index 00000000..78201d10 --- /dev/null +++ b/lofty/tests/taglib/util/mod.rs @@ -0,0 +1,114 @@ +use lofty::config::ParseOptions; +use lofty::file::AudioFile; +use std::fs::File; + +pub fn get_file(path: &str) -> F { + let mut file = File::open(path).unwrap(); + F::read_from(&mut file, ParseOptions::new()).unwrap() +} + +#[macro_export] +macro_rules! assert_delta { + ($x:expr, $y:expr, $d:expr) => { + if $x > $y { + assert!($x - $y <= $d) + } else if $y > $x { + assert!($y - $x <= $d) + } + }; +} + +#[macro_export] +macro_rules! temp_file { + ($path:tt) => {{ + use std::io::{Seek, Write}; + let mut file = tempfile::tempfile().unwrap(); + file.write_all(&std::fs::read($path).unwrap()).unwrap(); + + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + + file + }}; +} + +#[macro_export] +macro_rules! verify_artist { + ($file:ident, $method:ident, $expected_value:literal, $item_count:expr) => {{ + println!("VERIFY: Expecting `{}` to have {} items, with an artist of \"{}\"", stringify!($method), $item_count, $expected_value); + + verify_artist!($file, $method(), $expected_value, $item_count) + }}; + ($file:ident, $method:ident, $arg:path, $expected_value:literal, $item_count:expr) => {{ + println!("VERIFY: Expecting `{}` to have {} items, with an artist of \"{}\"", stringify!($arg), $item_count, $expected_value); + + verify_artist!($file, $method($arg), $expected_value, $item_count) + }}; + ($file:ident, $method:ident($($arg:path)?), $expected_value:literal, $item_count:expr) => {{ + assert!($file.$method($(&$arg)?).is_some()); + + let tag = $file.$method($(&$arg)?).unwrap(); + + assert_eq!(tag.item_count(), $item_count); + + assert_eq!( + tag.get_item_ref(&ItemKey::TrackArtist), + Some(&TagItem::new( + ItemKey::TrackArtist, + ItemValue::Text(String::from($expected_value)) + )) + ); + + tag + }}; +} + +#[macro_export] +macro_rules! set_artist { + ($tagged_file:ident, $method:ident, $expected_value:literal, $item_count:expr => $file_write:ident, $new_value:literal) => { + let tag = verify_artist!($tagged_file, $method, $expected_value, $item_count); + println!( + "WRITE: Writing artist \"{}\" to {}\n", + $new_value, + stringify!($method) + ); + set_artist!($file_write, $new_value, tag) + }; + ($tagged_file:ident, $method:ident, $arg:path, $expected_value:literal, $item_count:expr => $file_write:ident, $new_value:literal) => { + let tag = verify_artist!($tagged_file, $method, $arg, $expected_value, $item_count); + println!( + "WRITE: Writing artist \"{}\" to {}\n", + $new_value, + stringify!($arg) + ); + set_artist!($file_write, $new_value, tag) + }; + ($file_write:ident, $new_value:literal, $tag:ident) => { + $tag.insert_item_unchecked(TagItem::new( + ItemKey::TrackArtist, + ItemValue::Text(String::from($new_value)), + )); + + $file_write.seek(std::io::SeekFrom::Start(0)).unwrap(); + + $tag.save_to(&mut $file_write).unwrap(); + }; +} + +#[macro_export] +macro_rules! remove_tag { + ($path:tt, $tag_type:path) => { + let mut file = temp_file!($path); + + let tagged_file = lofty::read_from(&mut file, false).unwrap(); + assert!(tagged_file.tag(&$tag_type).is_some()); + + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + + $tag_type.remove_from(&mut file).unwrap(); + + file.seek(std::io::SeekFrom::Start(0)).unwrap(); + + let tagged_file = lofty::read_from(&mut file, false).unwrap(); + assert!(tagged_file.tag(&$tag_type).is_none()); + }; +}