diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bbed1a6..72ce39c96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Lofty writes tags. These are best used as global user-configurable options, as most options will not apply to all files. The defaults are set to be as safe as possible, see [here](https://docs.rs/lofty/latest/lofty/struct.WriteOptions.html#impl-Default-for-WriteOptions). +- **Generic Writes** ([PR](https://github.com/Serial-ATA/lofty-rs/pull/290)): + - ⚠️ Important ⚠️: This update introduces `FileLike`, which is a combination of the `Truncate` + `Length` traits + that allows one to write to more than just `File`s. In short, `Cursor>` can now be written to. - **ChannelMask** - `BitAnd` and `BitOr` implementations ([PR](https://github.com/Serial-ATA/lofty-rs/pull/371)) - Associated constants for common channels, ex. `ChannelMask::FRONT_LEFT` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/371)) diff --git a/lofty_attr/src/internal.rs b/lofty_attr/src/internal.rs index 6123dde7d..1b8837186 100644 --- a/lofty_attr/src/internal.rs +++ b/lofty_attr/src/internal.rs @@ -48,16 +48,16 @@ pub(crate) fn init_write_lookup( read_only: false, items: lofty::ape::tag::tagitems_into_ape(tag), } - .write_to(data, write_options) + .write_to(file, write_options) }); insert!(map, Id3v1, { - Into::>::into(tag).write_to(data, write_options) + Into::>::into(tag).write_to(file, write_options) }); if id3v2_strippable { insert!(map, Id3v2, { - lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(data, write_options) + lofty::id3::v2::tag::Id3v2TagRef::empty().write_to(file, write_options) }); } else { insert!(map, Id3v2, { @@ -65,7 +65,7 @@ pub(crate) fn init_write_lookup( flags: lofty::id3::v2::Id3v2TagFlags::default(), frames: lofty::id3::v2::tag::tag_frames(tag), } - .write_to(data, write_options) + .write_to(file, write_options) }); } @@ -73,7 +73,7 @@ pub(crate) fn init_write_lookup( lofty::iff::wav::tag::RIFFInfoListRef::new(lofty::iff::wav::tag::tagitems_into_riff( tag.items(), )) - .write_to(data, write_options) + .write_to(file, write_options) }); insert!(map, AiffText, { @@ -84,7 +84,7 @@ pub(crate) fn init_write_lookup( annotations: Some(tag.get_strings(&lofty::prelude::ItemKey::Comment)), comments: None, } - .write_to(data, write_options) + .write_to(file, write_options) }); map @@ -112,7 +112,12 @@ pub(crate) fn write_module( quote! { pub(crate) mod write { #[allow(unused_variables)] - pub(crate) fn write_to(data: &mut ::std::fs::File, tag: &::lofty::tag::Tag, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> { + pub(crate) fn write_to(file: &mut F, tag: &::lofty::tag::Tag, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> + where + F: ::lofty::io::FileLike, + ::lofty::error::LoftyError: ::std::convert::From<::Error>, + ::lofty::error::LoftyError: ::std::convert::From<::Error>, + { match tag.tag_type() { #( #applicable_formats )* _ => crate::macros::err!(UnsupportedTag), diff --git a/lofty_attr/src/lofty_file.rs b/lofty_attr/src/lofty_file.rs index 250151a1c..72637e83a 100644 --- a/lofty_attr/src/lofty_file.rs +++ b/lofty_attr/src/lofty_file.rs @@ -445,7 +445,12 @@ fn generate_audiofile_impl(file: &LoftyFile) -> syn::Result ::lofty::error::Result<()> { + fn save_to(&self, file: &mut F, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> + where + F: ::lofty::io::FileLike, + ::lofty::error::LoftyError: ::std::convert::From<::Error>, + ::lofty::error::LoftyError: ::std::convert::From<::Error>, + { use ::lofty::tag::TagExt as _; use ::std::io::Seek as _; #save_to_body diff --git a/src/ape/tag/mod.rs b/src/ape/tag/mod.rs index 6be290c5e..934828d60 100644 --- a/src/ape/tag/mod.rs +++ b/src/ape/tag/mod.rs @@ -12,11 +12,10 @@ use crate::tag::{ }; use std::borrow::Cow; -use std::fs::File; use std::io::Write; use std::ops::Deref; -use std::path::Path; +use crate::util::io::{FileLike, Truncate}; use lofty_attr::tag; macro_rules! impl_accessor { @@ -304,6 +303,11 @@ impl TagExt for ApeTag { type Err = LoftyError; type RefKey<'a> = &'a str; + #[inline] + fn tag_type(&self) -> TagType { + TagType::Ape + } + fn len(&self) -> usize { self.items.len() } @@ -322,11 +326,15 @@ impl TagExt for ApeTag { /// /// * Attempting to write the tag to a format that does not support it /// * An existing tag has an invalid size - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + { ApeTagRef { read_only: self.read_only, items: self.items.iter().map(Into::into), @@ -351,14 +359,6 @@ impl TagExt for ApeTag { .dump_to(writer, write_options) } - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - TagType::Ape.remove_from_path(path) - } - - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - TagType::Ape.remove_from(file) - } - fn clear(&mut self) { self.items.clear(); } @@ -492,7 +492,11 @@ impl<'a, I> ApeTagRef<'a, I> where I: Iterator>, { - pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + { write::write_to(file, self, write_options) } diff --git a/src/ape/tag/write.rs b/src/ape/tag/write.rs index 7dec88e34..336fe9237 100644 --- a/src/ape/tag/write.rs +++ b/src/ape/tag/write.rs @@ -3,40 +3,42 @@ use super::ApeTagRef; use crate::ape::constants::APE_PREAMBLE; use crate::ape::tag::read; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2, FindId3v2Config}; use crate::macros::{decode_err, err}; use crate::probe::Probe; use crate::tag::item::ItemValueRef; +use crate::util::io::{FileLike, Truncate}; -use std::fs::File; -use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use std::io::{Cursor, Seek, SeekFrom, Write}; use byteorder::{LittleEndian, WriteBytesExt}; #[allow(clippy::shadow_unrelated)] -pub(crate) fn write_to<'a, I>( - data: &mut File, +pub(crate) fn write_to<'a, F, I>( + file: &mut F, tag_ref: &mut ApeTagRef<'a, I>, write_options: WriteOptions, ) -> Result<()> where I: Iterator>, + F: FileLike, + LoftyError: From<::Error>, { - let probe = Probe::new(data).guess_file_type()?; + let probe = Probe::new(file).guess_file_type()?; match probe.file_type() { Some(ft) if super::ApeTag::SUPPORTED_FORMATS.contains(&ft) => {}, _ => err!(UnsupportedTag), } - let data = probe.into_inner(); + let file = probe.into_inner(); // We don't actually need the ID3v2 tag, but reading it will seek to the end of it if it exists - find_id3v2(data, FindId3v2Config::NO_READ_TAG)?; + find_id3v2(file, FindId3v2Config::NO_READ_TAG)?; let mut ape_preamble = [0; 8]; - data.read_exact(&mut ape_preamble)?; + file.read_exact(&mut ape_preamble)?; // We have to check the APE tag for any read only items first let mut read_only = None; @@ -45,8 +47,8 @@ where // If one is found, it'll be removed and rewritten at the bottom, where it should be let mut header_ape_tag = (false, (0, 0)); - let start = data.stream_position()?; - match read::read_ape_tag(data, false)? { + let start = file.stream_position()?; + match read::read_ape_tag(file, false)? { Some((mut existing_tag, header)) => { if write_options.respect_read_only { // Only keep metadata around that's marked read only @@ -60,25 +62,25 @@ where header_ape_tag = (true, (start, start + u64::from(header.size))) }, None => { - data.seek(SeekFrom::Current(-8))?; + file.seek(SeekFrom::Current(-8))?; }, } // Skip over ID3v1 and Lyrics3v2 tags - find_id3v1(data, false)?; - find_lyrics3v2(data)?; + find_id3v1(file, false)?; + find_lyrics3v2(file)?; // In case there's no ape tag already, this is the spot it belongs - let ape_position = data.stream_position()?; + let ape_position = file.stream_position()?; // Now search for an APE tag at the end - data.seek(SeekFrom::Current(-32))?; + file.seek(SeekFrom::Current(-32))?; let mut ape_tag_location = None; // Also check this tag for any read only items - let start = data.stream_position()? as usize + 32; - if let Some((mut existing_tag, header)) = read::read_ape_tag(data, true)? { + let start = file.stream_position()? as usize + 32; + if let Some((mut existing_tag, header)) = read::read_ape_tag(file, true)? { if write_options.respect_read_only { existing_tag.items.retain(|i| i.read_only); @@ -114,10 +116,10 @@ where tag = create_ape_tag(tag_ref, std::iter::empty(), write_options)?; }; - data.rewind()?; + file.rewind()?; let mut file_bytes = Vec::new(); - data.read_to_end(&mut file_bytes)?; + file.read_to_end(&mut file_bytes)?; // Write the tag in the appropriate place if let Some(range) = ape_tag_location { @@ -131,9 +133,9 @@ where file_bytes.drain(header_ape_tag.1 .0 as usize..header_ape_tag.1 .1 as usize); } - data.rewind()?; - data.set_len(0)?; - data.write_all(&file_bytes)?; + file.rewind()?; + file.truncate(0)?; + file.write_all(&file_bytes)?; Ok(()) } diff --git a/src/error.rs b/src/error.rs index 9a6275bf0..50039534a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -68,6 +68,8 @@ pub enum ErrorKind { Io(std::io::Error), /// Failure to allocate enough memory Alloc(TryReserveError), + /// This should **never** be encountered + Infallible(std::convert::Infallible), } /// The types of errors that can occur while interacting with ID3v2 tags @@ -499,6 +501,14 @@ impl From for LoftyError { } } +impl From for LoftyError { + fn from(input: std::convert::Infallible) -> Self { + Self { + kind: ErrorKind::Infallible(input), + } + } +} + impl Display for LoftyError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.kind { @@ -540,6 +550,8 @@ impl Display for LoftyError { ), ErrorKind::FileDecoding(ref file_decode_err) => write!(f, "{file_decode_err}"), ErrorKind::FileEncoding(ref file_encode_err) => write!(f, "{file_encode_err}"), + + ErrorKind::Infallible(_) => write!(f, "A expected condition was not upheld"), } } } diff --git a/src/file/audio_file.rs b/src/file/audio_file.rs index 5b1b4bee7..4541d1c4d 100644 --- a/src/file/audio_file.rs +++ b/src/file/audio_file.rs @@ -1,9 +1,10 @@ use super::tagged_file::TaggedFile; use crate::config::{ParseOptions, WriteOptions}; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::tag::TagType; -use std::fs::{File, OpenOptions}; +use crate::util::io::{FileLike, Length, Truncate}; +use std::fs::OpenOptions; use std::io::{Read, Seek}; use std::path::Path; @@ -77,7 +78,11 @@ pub trait AudioFile: Into { /// tagged_file.save_to(&mut file, WriteOptions::default())?; /// # Ok(()) } /// ``` - fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()>; + fn save_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>; /// Returns a reference to the file's properties fn properties(&self) -> &Self::Properties; diff --git a/src/file/tagged_file.rs b/src/file/tagged_file.rs index c9185fefd..f2ae1b5a0 100644 --- a/src/file/tagged_file.rs +++ b/src/file/tagged_file.rs @@ -1,10 +1,11 @@ use super::audio_file::AudioFile; use super::file_type::FileType; use crate::config::{ParseOptions, WriteOptions}; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::properties::FileProperties; use crate::tag::{Tag, TagExt, TagType}; +use crate::util::io::{FileLike, Length, Truncate}; use std::fs::File; use std::io::{Read, Seek}; @@ -423,7 +424,12 @@ impl AudioFile for TaggedFile { .read() } - fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { + fn save_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { for tag in &self.tags { // TODO: This is a temporary solution. Ideally we should probe once and use // the format-specific writing to avoid these rewinds. @@ -631,7 +637,12 @@ impl AudioFile for BoundTaggedFile { ) } - fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { + fn save_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { self.inner.save_to(file, write_options) } diff --git a/src/flac/mod.rs b/src/flac/mod.rs index 01102dc00..72d1d0f73 100644 --- a/src/flac/mod.rs +++ b/src/flac/mod.rs @@ -10,21 +10,18 @@ mod read; pub(crate) mod write; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::file::{FileType, TaggedFile}; use crate::id3::v2::tag::Id3v2Tag; use crate::ogg::tag::VorbisCommentsRef; use crate::ogg::{OggPictureStorage, VorbisComments}; use crate::picture::{Picture, PictureInformation}; use crate::tag::TagExt; - -use std::fs::File; -use std::io::Seek; +use crate::util::io::{FileLike, Length, Truncate}; use lofty_attr::LoftyFile; // Exports - pub use properties::FlacProperties; /// A FLAC file @@ -56,7 +53,12 @@ pub struct FlacFile { impl FlacFile { // We need a special write fn to append our pictures into a `VorbisComments` tag - fn write_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { + fn write_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { if let Some(ref id3v2) = self.id3v2_tag { id3v2.save_to(file, write_options)?; file.rewind()?; diff --git a/src/flac/write.rs b/src/flac/write.rs index d043c53be..46aaac32c 100644 --- a/src/flac/write.rs +++ b/src/flac/write.rs @@ -1,22 +1,27 @@ use super::block::Block; use super::read::verify_flac; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::macros::{err, try_vec}; use crate::ogg::tag::VorbisCommentsRef; use crate::ogg::write::create_comments; use crate::picture::{Picture, PictureInformation}; use crate::tag::{Tag, TagType}; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; -use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use std::io::{Cursor, Seek, SeekFrom, Write}; use byteorder::{LittleEndian, WriteBytesExt}; const BLOCK_HEADER_SIZE: usize = 4; const MAX_BLOCK_SIZE: u32 = 16_777_215; -pub(crate) fn write_to(file: &mut File, tag: &Tag, write_options: WriteOptions) -> Result<()> { +pub(crate) fn write_to(file: &mut F, tag: &Tag, write_options: WriteOptions) -> Result<()> +where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, +{ match tag.tag_type() { TagType::VorbisComments => { let (vendor, items, pictures) = crate::ogg::tag::create_vorbis_comments_ref(tag); @@ -35,12 +40,14 @@ pub(crate) fn write_to(file: &mut File, tag: &Tag, write_options: WriteOptions) } } -pub(crate) fn write_to_inner<'a, II, IP>( - file: &mut File, +pub(crate) fn write_to_inner<'a, F, II, IP>( + file: &mut F, tag: &mut VorbisCommentsRef<'a, II, IP>, write_options: WriteOptions, ) -> Result<()> where + F: FileLike, + LoftyError: From<::Error>, II: Iterator, IP: Iterator, { @@ -131,7 +138,7 @@ where } file.seek(SeekFrom::Start(stream_info_end as u64))?; - file.set_len(stream_info_end as u64)?; + file.truncate(stream_info_end as u64)?; file.write_all(&file_bytes)?; Ok(()) diff --git a/src/id3/v1/tag.rs b/src/id3/v1/tag.rs index f49219da3..02806575c 100644 --- a/src/id3/v1/tag.rs +++ b/src/id3/v1/tag.rs @@ -2,9 +2,9 @@ use crate::config::WriteOptions; use crate::error::{LoftyError, Result}; use crate::id3::v1::constants::GENRES; use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType}; +use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; -use std::fs::File; use std::io::Write; use std::path::Path; @@ -209,6 +209,11 @@ impl TagExt for Id3v1Tag { type Err = LoftyError; type RefKey<'a> = &'a ItemKey; + #[inline] + fn tag_type(&self) -> TagType { + TagType::Id3v1 + } + fn len(&self) -> usize { usize::from(self.title.is_some()) + usize::from(self.artist.is_some()) @@ -242,11 +247,16 @@ impl TagExt for Id3v1Tag { && self.genre.is_none() } - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { Into::>::into(self).write_to(file, write_options) } @@ -267,7 +277,12 @@ impl TagExt for Id3v1Tag { TagType::Id3v1.remove_from_path(path) } - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn remove_from(&self, file: &mut F) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { TagType::Id3v1.remove_from(file) } @@ -420,7 +435,12 @@ impl<'a> Id3v1TagRef<'a> { && self.genre.is_none() } - pub(crate) fn write_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(&self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { super::write::write_id3v1(file, self, write_options) } diff --git a/src/id3/v1/write.rs b/src/id3/v1/write.rs index 188ea3b24..2d9582d97 100644 --- a/src/id3/v1/write.rs +++ b/src/id3/v1/write.rs @@ -1,21 +1,26 @@ use super::tag::Id3v1TagRef; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::id3::{find_id3v1, ID3FindResults}; use crate::macros::err; use crate::probe::Probe; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; use std::io::{Cursor, Seek, Write}; use byteorder::WriteBytesExt; #[allow(clippy::shadow_unrelated)] -pub(crate) fn write_id3v1( - file: &mut File, +pub(crate) fn write_id3v1( + file: &mut F, tag: &Id3v1TagRef<'_>, _write_options: WriteOptions, -) -> Result<()> { +) -> Result<()> +where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, +{ let probe = Probe::new(file).guess_file_type()?; match probe.file_type() { @@ -31,7 +36,8 @@ pub(crate) fn write_id3v1( if tag.is_empty() && header.is_some() { // An ID3v1 tag occupies the last 128 bytes of the file, so we can just // shrink it down. - file.set_len(file.metadata()?.len().saturating_sub(128))?; + let new_length = file.len()?.saturating_sub(128); + file.truncate(new_length)?; return Ok(()); } diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index a62962a27..47e5f7249 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -21,13 +21,12 @@ use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::{ try_parse_year, Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, }; +use crate::util::io::{FileLike, Length, Truncate}; use crate::util::text::{decode_text, TextDecodeOptions, TextEncoding}; use std::borrow::Cow; -use std::fs::File; use std::io::{Cursor, Write}; use std::ops::Deref; -use std::path::Path; use lofty_attr::tag; @@ -896,6 +895,11 @@ impl TagExt for Id3v2Tag { type Err = LoftyError; type RefKey<'a> = &'a FrameId<'a>; + #[inline] + fn tag_type(&self) -> TagType { + TagType::Id3v2 + } + fn len(&self) -> usize { self.frames.len() } @@ -915,11 +919,16 @@ impl TagExt for Id3v2Tag { /// * Attempting to write the tag to a format that does not support it /// * Attempting to write an encrypted frame without a valid method symbol or data length indicator /// * Attempting to write an invalid [`FrameId`]/[`FrameValue`] pairing - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { Id3v2TagRef { flags: self.flags, frames: self.frames.iter().filter_map(Frame::as_opt_ref), @@ -945,14 +954,6 @@ impl TagExt for Id3v2Tag { .dump_to(writer, write_options) } - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - TagType::Id3v2.remove_from_path(path) - } - - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - TagType::Id3v2.remove_from(file) - } - fn clear(&mut self) { self.frames.clear(); } @@ -1495,7 +1496,12 @@ pub(crate) fn tag_frames(tag: &Tag) -> impl Iterator> + Clon } impl<'a, I: Iterator> + Clone + 'a> Id3v2TagRef<'a, I> { - pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { super::write::write_id3v2(file, self, write_options) } diff --git a/src/id3/v2/write/chunk_file.rs b/src/id3/v2/write/chunk_file.rs index f0d383d66..d4ba3caf5 100644 --- a/src/id3/v2/write/chunk_file.rs +++ b/src/id3/v2/write/chunk_file.rs @@ -1,45 +1,48 @@ use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::iff::chunk::Chunks; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; -use std::io::{Read, Seek, SeekFrom, Write}; +use std::io::SeekFrom; use byteorder::{ByteOrder, WriteBytesExt}; const CHUNK_NAME_UPPER: [u8; 4] = [b'I', b'D', b'3', b' ']; const CHUNK_NAME_LOWER: [u8; 4] = [b'i', b'd', b'3', b' ']; -pub(in crate::id3::v2) fn write_to_chunk_file( - data: &mut File, +pub(in crate::id3::v2) fn write_to_chunk_file( + file: &mut F, tag: &[u8], write_options: WriteOptions, ) -> Result<()> where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, B: ByteOrder, { // RIFF....WAVE - data.seek(SeekFrom::Current(12))?; + file.seek(SeekFrom::Current(12))?; - let file_len = data.metadata()?.len().saturating_sub(12); + let file_len = file.len()?.saturating_sub(12); let mut id3v2_chunk = (None, None); let mut chunks = Chunks::::new(file_len); - while chunks.next(data).is_ok() { + while chunks.next(file).is_ok() { if chunks.fourcc == CHUNK_NAME_UPPER || chunks.fourcc == CHUNK_NAME_LOWER { - id3v2_chunk = (Some(data.stream_position()? - 8), Some(chunks.size)); + id3v2_chunk = (Some(file.stream_position()? - 8), Some(chunks.size)); break; } - data.seek(SeekFrom::Current(i64::from(chunks.size)))?; + file.seek(SeekFrom::Current(i64::from(chunks.size)))?; - chunks.correct_position(data)?; + chunks.correct_position(file)?; } if let (Some(chunk_start), Some(mut chunk_size)) = id3v2_chunk { - data.rewind()?; + file.rewind()?; // We need to remove the padding byte if it exists if chunk_size % 2 != 0 { @@ -47,41 +50,41 @@ where } let mut file_bytes = Vec::new(); - data.read_to_end(&mut file_bytes)?; + file.read_to_end(&mut file_bytes)?; file_bytes.splice( chunk_start as usize..(chunk_start + u64::from(chunk_size) + 8) as usize, [], ); - data.rewind()?; - data.set_len(0)?; - data.write_all(&file_bytes)?; + file.rewind()?; + file.truncate(0)?; + file.write_all(&file_bytes)?; } if !tag.is_empty() { - data.seek(SeekFrom::End(0))?; + file.seek(SeekFrom::End(0))?; if write_options.uppercase_id3v2_chunk { - data.write_all(&CHUNK_NAME_UPPER)?; + file.write_all(&CHUNK_NAME_UPPER)?; } else { - data.write_all(&CHUNK_NAME_LOWER)?; + file.write_all(&CHUNK_NAME_LOWER)?; } - data.write_u32::(tag.len() as u32)?; - data.write_all(tag)?; + file.write_u32::(tag.len() as u32)?; + file.write_all(tag)?; // It is required an odd length chunk be padded with a 0 // The 0 isn't included in the chunk size, however if tag.len() % 2 != 0 { - data.write_u8(0)?; + file.write_u8(0)?; } - let total_size = data.stream_position()? - 8; + let total_size = file.stream_position()? - 8; - data.seek(SeekFrom::Start(4))?; + file.seek(SeekFrom::Start(4))?; - data.write_u32::(total_size as u32)?; + file.write_u32::(total_size as u32)?; } Ok(()) diff --git a/src/id3/v2/write/mod.rs b/src/id3/v2/write/mod.rs index ef6a21b5a..99c89a905 100644 --- a/src/id3/v2/write/mod.rs +++ b/src/id3/v2/write/mod.rs @@ -3,7 +3,7 @@ mod frame; use super::Id3v2TagFlags; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::file::FileType; use crate::id3::v2::frame::FrameRef; use crate::id3::v2::tag::Id3v2TagRef; @@ -12,8 +12,8 @@ use crate::id3::v2::Id3v2Tag; use crate::id3::{find_id3v2, FindId3v2Config}; use crate::macros::{err, try_vec}; use crate::probe::Probe; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::ops::Not; use std::sync::OnceLock; @@ -38,15 +38,21 @@ fn crc_32_table() -> &'static [u32; 256] { } #[allow(clippy::shadow_unrelated)] -pub(crate) fn write_id3v2<'a, I: Iterator> + Clone + 'a>( - data: &mut File, +pub(crate) fn write_id3v2<'a, F, I>( + file: &mut F, tag: &mut Id3v2TagRef<'a, I>, write_options: WriteOptions, -) -> Result<()> { - let probe = Probe::new(data).guess_file_type()?; +) -> Result<()> +where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + I: Iterator> + Clone + 'a, +{ + let probe = Probe::new(file).guess_file_type()?; let file_type = probe.file_type(); - let data = probe.into_inner(); + let file = probe.into_inner(); // Unable to determine a format if file_type.is_none() { @@ -74,27 +80,27 @@ pub(crate) fn write_id3v2<'a, I: Iterator> + Clone + 'a>( // Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file FileType::Wav => { tag.flags.footer = false; - return chunk_file::write_to_chunk_file::(data, &id3v2, write_options); + return chunk_file::write_to_chunk_file::(file, &id3v2, write_options); }, FileType::Aiff => { tag.flags.footer = false; - return chunk_file::write_to_chunk_file::(data, &id3v2, write_options); + return chunk_file::write_to_chunk_file::(file, &id3v2, write_options); }, _ => {}, } // find_id3v2 will seek us to the end of the tag // TODO: Search through junk - find_id3v2(data, FindId3v2Config::NO_READ_TAG)?; + find_id3v2(file, FindId3v2Config::NO_READ_TAG)?; let mut file_bytes = Vec::new(); - data.read_to_end(&mut file_bytes)?; + file.read_to_end(&mut file_bytes)?; file_bytes.splice(0..0, id3v2); - data.rewind()?; - data.set_len(0)?; - data.write_all(&file_bytes)?; + file.rewind()?; + file.truncate(0)?; + file.write_all(&file_bytes)?; Ok(()) } diff --git a/src/iff/aiff/tag.rs b/src/iff/aiff/tag.rs index 186f03fa4..7c05add7e 100644 --- a/src/iff/aiff/tag.rs +++ b/src/iff/aiff/tag.rs @@ -3,11 +3,11 @@ use crate::error::{LoftyError, Result}; use crate::iff::chunk::Chunks; use crate::macros::err; use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType}; +use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; -use std::fs::File; -use std::io::{Read, Seek, SeekFrom, Write}; -use std::path::Path; +use std::convert::TryFrom; +use std::io::{SeekFrom, Write}; use byteorder::BigEndian; use lofty_attr::tag; @@ -156,6 +156,11 @@ impl TagExt for AIFFTextChunks { type Err = LoftyError; type RefKey<'a> = &'a ItemKey; + #[inline] + fn tag_type(&self) -> TagType { + TagType::AiffText + } + fn len(&self) -> usize { usize::from(self.name.is_some()) + usize::from(self.author.is_some()) @@ -187,11 +192,16 @@ impl TagExt for AIFFTextChunks { ) } - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { AiffTextChunksRef { name: self.name.as_deref(), author: self.author.as_deref(), @@ -217,14 +227,6 @@ impl TagExt for AIFFTextChunks { .dump_to(writer, write_options) } - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - TagType::AiffText.remove_from_path(path) - } - - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - TagType::AiffText.remove_from(file) - } - fn clear(&mut self) { *self = Self::default(); } @@ -315,7 +317,12 @@ where T: AsRef, AI: IntoIterator, { - pub(crate) fn write_to(self, file: &mut File, _write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(self, file: &mut F, _write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { AiffTextChunksRef::write_to_inner(file, self) } @@ -414,9 +421,14 @@ where Ok(text_chunks) } - fn write_to_inner(data: &mut File, mut tag: AiffTextChunksRef<'_, T, AI>) -> Result<()> { - super::read::verify_aiff(data)?; - let file_len = data.metadata()?.len().saturating_sub(12); + fn write_to_inner(file: &mut F, mut tag: AiffTextChunksRef<'_, T, AI>) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { + super::read::verify_aiff(file)?; + let file_len = file.len()?.saturating_sub(12); let text_chunks = Self::create_text_chunks(&mut tag)?; @@ -424,10 +436,10 @@ where let mut chunks = Chunks::::new(file_len); - while chunks.next(data).is_ok() { + while chunks.next(file).is_ok() { match &chunks.fourcc { b"NAME" | b"AUTH" | b"(c) " | b"ANNO" | b"COMT" => { - let start = (data.stream_position()? - 8) as usize; + let start = (file.stream_position()? - 8) as usize; let mut end = start + 8 + chunks.size as usize; if chunks.size % 2 != 0 { @@ -439,19 +451,19 @@ where _ => {}, } - chunks.skip(data)?; + chunks.skip(file)?; } - data.rewind()?; + file.rewind()?; let mut file_bytes = Vec::new(); - data.read_to_end(&mut file_bytes)?; + file.read_to_end(&mut file_bytes)?; if chunks_remove.is_empty() { - data.seek(SeekFrom::Start(16))?; + file.seek(SeekFrom::Start(16))?; let mut size = [0; 4]; - data.read_exact(&mut size)?; + file.read_exact(&mut size)?; let comm_end = (20 + u32::from_le_bytes(size)) as usize; file_bytes.splice(comm_end..comm_end, text_chunks); @@ -471,9 +483,9 @@ where let total_size = ((file_bytes.len() - 8) as u32).to_be_bytes(); file_bytes.splice(4..8, total_size.to_vec()); - data.rewind()?; - data.set_len(0)?; - data.write_all(&file_bytes)?; + file.rewind()?; + file.truncate(0)?; + file.write_all(&file_bytes)?; Ok(()) } diff --git a/src/iff/wav/tag/mod.rs b/src/iff/wav/tag/mod.rs index fd5c0f9e3..76487c9c8 100644 --- a/src/iff/wav/tag/mod.rs +++ b/src/iff/wav/tag/mod.rs @@ -6,11 +6,10 @@ use crate::error::{LoftyError, Result}; use crate::tag::{ try_parse_year, Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, }; +use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; -use std::fs::File; use std::io::Write; -use std::path::Path; use lofty_attr::tag; @@ -190,6 +189,11 @@ impl TagExt for RIFFInfoList { type Err = LoftyError; type RefKey<'a> = &'a str; + #[inline] + fn tag_type(&self) -> TagType { + TagType::RiffInfo + } + fn len(&self) -> usize { self.items.len() } @@ -204,11 +208,16 @@ impl TagExt for RIFFInfoList { self.items.is_empty() } - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { RIFFInfoListRef::new(self.items.iter().map(|(k, v)| (k.as_str(), v.as_str()))) .write_to(file, write_options) } @@ -222,14 +231,6 @@ impl TagExt for RIFFInfoList { .dump_to(writer, write_options) } - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - TagType::RiffInfo.remove_from_path(path) - } - - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - TagType::RiffInfo.remove_from(file) - } - fn clear(&mut self) { self.items.clear(); } @@ -311,7 +312,12 @@ where RIFFInfoListRef { items } } - pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { write::write_riff_info(file, self, write_options) } diff --git a/src/iff/wav/tag/write.rs b/src/iff/wav/tag/write.rs index 5fb38a63f..732c76d00 100644 --- a/src/iff/wav/tag/write.rs +++ b/src/iff/wav/tag/write.rs @@ -1,56 +1,64 @@ use super::RIFFInfoListRef; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::iff::chunk::Chunks; use crate::iff::wav::read::verify_wav; use crate::macros::err; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; -use std::io::{Read, Seek, SeekFrom, Write}; +use std::io::{Read, Seek, SeekFrom}; use byteorder::{LittleEndian, WriteBytesExt}; -pub(in crate::iff::wav) fn write_riff_info<'a, I>( - data: &mut File, +pub(in crate::iff::wav) fn write_riff_info<'a, F, I>( + file: &mut F, tag: &mut RIFFInfoListRef<'a, I>, _write_options: WriteOptions, ) -> Result<()> where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, I: Iterator, { - verify_wav(data)?; - let file_len = data.metadata()?.len().saturating_sub(12); + verify_wav(file)?; + let file_len = file.len()?.saturating_sub(12); let mut riff_info_bytes = Vec::new(); create_riff_info(&mut tag.items, &mut riff_info_bytes)?; - if let Some(info_list_size) = find_info_list(data, file_len)? { - let info_list_start = data.seek(SeekFrom::Current(-12))? as usize; - let info_list_end = info_list_start + 8 + info_list_size as usize; + let Some(info_list_size) = find_info_list(file, file_len)? else { + // Simply append the info list to the end of the file and update the file size + file.seek(SeekFrom::End(0))?; - data.rewind()?; + file.write_all(&riff_info_bytes)?; - let mut file_bytes = Vec::new(); - data.read_to_end(&mut file_bytes)?; + let len = (file.stream_position()? - 8) as u32; - let _ = file_bytes.splice(info_list_start..info_list_end, riff_info_bytes); + file.seek(SeekFrom::Start(4))?; + file.write_u32::(len)?; - let total_size = (file_bytes.len() - 8) as u32; - let _ = file_bytes.splice(4..8, total_size.to_le_bytes()); + return Ok(()); + }; - data.rewind()?; - data.set_len(0)?; - data.write_all(&file_bytes)?; - } else { - data.seek(SeekFrom::End(0))?; + // Replace the existing tag - data.write_all(&riff_info_bytes)?; + let info_list_start = file.seek(SeekFrom::Current(-12))? as usize; + let info_list_end = info_list_start + 8 + info_list_size as usize; - let len = (data.stream_position()? - 8) as u32; + file.rewind()?; - data.seek(SeekFrom::Start(4))?; - data.write_u32::(len)?; - } + let mut file_bytes = Vec::new(); + file.read_to_end(&mut file_bytes)?; + + let _ = file_bytes.splice(info_list_start..info_list_end, riff_info_bytes); + + let total_size = (file_bytes.len() - 8) as u32; + let _ = file_bytes.splice(4..8, total_size.to_le_bytes()); + + file.rewind()?; + file.truncate(0)?; + file.write_all(&file_bytes)?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index c08c238d5..8592d2ea8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -179,6 +179,8 @@ pub use util::text::TextEncoding; pub use lofty_attr::LoftyFile; +pub use util::io; + pub mod prelude { //! A prelude for commonly used items in the library. //! diff --git a/src/mp4/ilst/mod.rs b/src/mp4/ilst/mod.rs index 1a901924d..ff2c97bad 100644 --- a/src/mp4/ilst/mod.rs +++ b/src/mp4/ilst/mod.rs @@ -12,13 +12,12 @@ use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::{ try_parse_year, Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, }; +use crate::util::io::{FileLike, Length, Truncate}; use atom::{AdvisoryRating, Atom, AtomData}; use std::borrow::Cow; -use std::fs::File; use std::io::Write; use std::ops::Deref; -use std::path::Path; use lofty_attr::tag; @@ -518,6 +517,11 @@ impl TagExt for Ilst { type Err = LoftyError; type RefKey<'a> = &'a AtomIdent<'a>; + #[inline] + fn tag_type(&self) -> TagType { + TagType::Mp4Ilst + } + fn len(&self) -> usize { self.atoms.len() } @@ -530,11 +534,16 @@ impl TagExt for Ilst { self.atoms.is_empty() } - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { self.as_ref().write_to(file, write_options) } @@ -546,14 +555,6 @@ impl TagExt for Ilst { self.as_ref().dump_to(writer, write_options) } - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - TagType::Mp4Ilst.remove_from_path(path) - } - - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - TagType::Mp4Ilst.remove_from(file) - } - fn clear(&mut self) { self.atoms.clear(); } diff --git a/src/mp4/ilst/ref.rs b/src/mp4/ilst/ref.rs index 0e0c5505c..1fa6c0141 100644 --- a/src/mp4/ilst/ref.rs +++ b/src/mp4/ilst/ref.rs @@ -3,10 +3,10 @@ // ********************* use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::mp4::{Atom, AtomData, AtomIdent, Ilst}; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; use std::io::Write; impl Ilst { @@ -25,7 +25,12 @@ impl<'a, I: 'a> IlstRef<'a, I> where I: IntoIterator, { - pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { super::write::write_to(file, self, write_options) } diff --git a/src/mp4/ilst/write.rs b/src/mp4/ilst/write.rs index a49215263..df5aeab1a 100644 --- a/src/mp4/ilst/write.rs +++ b/src/mp4/ilst/write.rs @@ -1,6 +1,6 @@ use super::r#ref::IlstRef; use crate::config::{ParseOptions, WriteOptions}; -use crate::error::{FileEncodingError, Result}; +use crate::error::{FileEncodingError, LoftyError, Result}; use crate::file::FileType; use crate::macros::{decode_err, err, try_vec}; use crate::mp4::atom_info::{AtomIdent, AtomInfo, ATOM_HEADER_LEN, FOURCC_LEN}; @@ -9,8 +9,8 @@ use crate::mp4::read::{atom_tree, meta_is_full, nested_atom, verify_mp4, AtomRea use crate::mp4::write::{AtomWriter, AtomWriterCompanion, ContextualAtom}; use crate::mp4::AtomData; use crate::picture::{MimeType, Picture}; +use crate::util::io::{FileLike, Length, Truncate}; -use std::fs::File; use std::io::{Cursor, Seek, SeekFrom, Write}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; @@ -20,25 +20,28 @@ const FULL_ATOM_SIZE: u64 = ATOM_HEADER_LEN + 4; const HDLR_SIZE: u64 = ATOM_HEADER_LEN + 25; // TODO: We are forcing the use of ParseOptions::DEFAULT_PARSING_MODE. This is not good. It should be caller-specified. -pub(crate) fn write_to<'a, I: 'a>( - data: &mut File, +pub(crate) fn write_to<'a, F, I: 'a>( + file: &mut F, tag: &mut IlstRef<'a, I>, write_options: WriteOptions, ) -> Result<()> where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, I: IntoIterator, { log::debug!("Attempting to write `ilst` tag to file"); // Create a temporary `AtomReader`, just to verify that this is a valid MP4 file - let mut reader = AtomReader::new(data, ParseOptions::DEFAULT_PARSING_MODE)?; + let mut reader = AtomReader::new(file, ParseOptions::DEFAULT_PARSING_MODE)?; verify_mp4(&mut reader)?; // Now we can just read the entire file into memory - let data = reader.into_inner(); - data.rewind()?; + let file = reader.into_inner(); + file.rewind()?; - let mut atom_writer = AtomWriter::new_from_file(data, ParseOptions::DEFAULT_PARSING_MODE)?; + let mut atom_writer = AtomWriter::new_from_file(file, ParseOptions::DEFAULT_PARSING_MODE)?; let Some(moov) = atom_writer.find_contextual_atom(*b"moov") else { return Err(FileEncodingError::new( @@ -198,7 +201,7 @@ where drop(write_handle); - atom_writer.save_to(data)?; + atom_writer.save_to(file)?; Ok(()) } diff --git a/src/mp4/write.rs b/src/mp4/write.rs index e5bd7de43..b37b752d4 100644 --- a/src/mp4/write.rs +++ b/src/mp4/write.rs @@ -1,13 +1,13 @@ use crate::config::ParsingMode; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::mp4::atom_info::{AtomIdent, AtomInfo, IDENTIFIER_LEN}; use crate::mp4::read::skip_unneeded; use std::cell::{RefCell, RefMut}; -use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::ops::RangeBounds; +use crate::io::{FileLike, Length, Truncate}; use byteorder::{BigEndian, WriteBytesExt}; /// A wrapper around [`AtomInfo`] that allows us to track all of the children of containers we deem important @@ -111,7 +111,12 @@ impl AtomWriter { /// Create a new [`AtomWriter`] /// /// This will read the entire file into memory, and parse its atoms. - pub(super) fn new_from_file(file: &mut File, parse_mode: ParsingMode) -> Result { + pub(super) fn new_from_file(file: &mut F, parse_mode: ParsingMode) -> Result + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { let mut contents = Cursor::new(Vec::new()); file.read_to_end(contents.get_mut())?; @@ -145,9 +150,14 @@ impl AtomWriter { } } - pub(super) fn save_to(&mut self, file: &mut File) -> Result<()> { + pub(super) fn save_to(&mut self, file: &mut F) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { file.rewind()?; - file.set_len(0)?; + file.truncate(0)?; file.write_all(self.contents.borrow().get_ref())?; Ok(()) diff --git a/src/ogg/tag.rs b/src/ogg/tag.rs index f4e2ce069..de27f7e9b 100644 --- a/src/ogg/tag.rs +++ b/src/ogg/tag.rs @@ -11,11 +11,10 @@ use crate::tag::{ }; use std::borrow::Cow; -use std::fs::File; use std::io::Write; use std::ops::Deref; -use std::path::Path; +use crate::util::io::{FileLike, Length, Truncate}; use lofty_attr::tag; macro_rules! impl_accessor { @@ -433,6 +432,11 @@ impl TagExt for VorbisComments { type Err = LoftyError; type RefKey<'a> = &'a str; + #[inline] + fn tag_type(&self) -> TagType { + TagType::VorbisComments + } + fn len(&self) -> usize { self.items.len() + self.pictures.len() } @@ -455,11 +459,16 @@ impl TagExt for VorbisComments { /// * The file does not contain valid packets /// * [`PictureInformation::from_picture`] /// * [`std::io::Error`] - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { VorbisCommentsRef { vendor: self.vendor.as_str(), items: self.items.iter().map(|(k, v)| (k.as_str(), v.as_str())), @@ -490,14 +499,6 @@ impl TagExt for VorbisComments { .dump_to(writer, write_options) } - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { - TagType::VorbisComments.remove_from_path(path) - } - - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { - TagType::VorbisComments.remove_from(file) - } - fn clear(&mut self) { self.items.clear(); self.pictures.clear(); @@ -634,7 +635,12 @@ where IP: Iterator, { #[allow(clippy::shadow_unrelated)] - pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> { + pub(crate) fn write_to(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { let probe = Probe::new(file).guess_file_type()?; let f_ty = probe.file_type(); diff --git a/src/ogg/write.rs b/src/ogg/write.rs index 194c7886e..43471a102 100644 --- a/src/ogg/write.rs +++ b/src/ogg/write.rs @@ -1,6 +1,6 @@ use super::verify_signature; use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::file::FileType; use crate::macros::{decode_err, err, try_vec}; use crate::ogg::constants::{OPUSTAGS, VORBIS_COMMENT_HEAD}; @@ -8,9 +8,9 @@ use crate::ogg::tag::{create_vorbis_comments_ref, VorbisCommentsRef}; use crate::picture::{Picture, PictureInformation}; use crate::tag::{Tag, TagType}; -use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use crate::util::io::{FileLike, Length, Truncate}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use ogg_pager::{Packets, Page, PageHeader, CONTAINS_FIRST_PAGE_OF_BITSTREAM}; @@ -40,12 +40,17 @@ impl OGGFormat { } } -pub(crate) fn write_to( - file: &mut File, +pub(crate) fn write_to( + file: &mut F, tag: &Tag, file_type: FileType, write_options: WriteOptions, -) -> Result<()> { +) -> Result<()> +where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, +{ if tag.tag_type() != TagType::VorbisComments { err!(UnsupportedTag); } @@ -69,14 +74,17 @@ pub(crate) fn write_to( ) } -pub(super) fn write<'a, II, IP>( - file: &mut File, +pub(super) fn write<'a, F, II, IP>( + file: &mut F, tag: &mut VorbisCommentsRef<'a, II, IP>, format: OGGFormat, header_packet_count: isize, _write_options: WriteOptions, ) -> Result<()> where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, II: Iterator, IP: Iterator, { @@ -120,7 +128,7 @@ where packets.set(1, new_metadata_packet); file.rewind()?; - file.set_len(0)?; + file.truncate(0)?; let pages_written = packets.write_to(file, stream_serial, 0, CONTAINS_FIRST_PAGE_OF_BITSTREAM)? as u32; diff --git a/src/tag/mod.rs b/src/tag/mod.rs index 9caaddb6f..a4507dada 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -12,9 +12,9 @@ use crate::error::{LoftyError, Result}; use crate::macros::err; use crate::picture::{Picture, PictureType}; use crate::probe::Probe; +use crate::util::io::{FileLike, Length, Truncate}; use std::borrow::Cow; -use std::fs::File; use std::io::Write; use std::path::Path; @@ -543,6 +543,11 @@ impl TagExt for Tag { type Err = LoftyError; type RefKey<'a> = &'a ItemKey; + #[inline] + fn tag_type(&self) -> TagType { + self.tag_type + } + fn len(&self) -> usize { self.items.len() + self.pictures.len() } @@ -555,17 +560,22 @@ impl TagExt for Tag { self.items.is_empty() && self.pictures.is_empty() } - /// Save the `Tag` to a [`File`](std::fs::File) + /// Save the `Tag` to a [`FileLike`] /// /// # Errors /// /// * A [`FileType`](crate::file::FileType) couldn't be determined from the File /// * Attempting to write a tag to a format that does not support it. See [`FileType::supports_tag_type`](crate::file::FileType::supports_tag_type) - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err> { + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { let probe = Probe::new(file).guess_file_type()?; match probe.file_type() { @@ -593,12 +603,17 @@ impl TagExt for Tag { self.tag_type.remove_from_path(path) } - /// Remove a tag from a [`File`] + /// Remove a tag from a [`FileLike`] /// /// # Errors /// /// See [`TagType::remove_from`] - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> { + fn remove_from(&self, file: &mut F) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { self.tag_type.remove_from(file) } diff --git a/src/tag/tag_ext.rs b/src/tag/tag_ext.rs index 0ebb55c04..bed35529e 100644 --- a/src/tag/tag_ext.rs +++ b/src/tag/tag_ext.rs @@ -1,7 +1,8 @@ use crate::config::WriteOptions; -use crate::tag::{Accessor, Tag}; +use crate::error::LoftyError; +use crate::io::{FileLike, Length, Truncate}; +use crate::tag::{Accessor, Tag, TagType}; -use std::fs::File; use std::path::Path; /// A set of common methods between tags @@ -12,12 +13,15 @@ use std::path::Path; /// This can be implemented downstream to provide a familiar interface for custom tags. pub trait TagExt: Accessor + Into + Sized { /// The associated error which can be returned from IO operations - type Err: From; + type Err: From + From; /// The type of key used in the tag for non-mutating functions type RefKey<'a> where Self: 'a; + #[doc(hidden)] + fn tag_type(&self) -> TagType; + /// Returns the number of items in the tag /// /// This will also include any extras, such as pictures. @@ -89,17 +93,21 @@ pub trait TagExt: Accessor + Into + Sized { ) } - /// Save the tag to a [`File`] + /// Save the tag to a [`FileLike`] /// /// # Errors /// /// * The file format could not be determined /// * Attempting to write a tag to a format that does not support it. - fn save_to( + fn save_to( &self, - file: &mut File, + file: &mut F, write_options: WriteOptions, - ) -> std::result::Result<(), Self::Err>; + ) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>; #[allow(clippy::missing_errors_doc)] /// Dump the tag to a writer @@ -116,16 +124,25 @@ pub trait TagExt: Accessor + Into + Sized { /// # Errors /// /// See [`TagExt::remove_from`] - fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err>; + fn remove_from_path>(&self, path: P) -> std::result::Result<(), Self::Err> { + self.tag_type().remove_from_path(path).map_err(Into::into) + } - /// Remove a tag from a [`File`] + /// Remove a tag from a [`FileLike`] /// /// # Errors /// /// * It is unable to guess the file format /// * The format doesn't support the tag /// * It is unable to write to the file - fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err>; + fn remove_from(&self, file: &mut F) -> std::result::Result<(), Self::Err> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { + self.tag_type().remove_from(file).map_err(Into::into) + } /// Clear the tag, removing all items /// diff --git a/src/tag/tag_type.rs b/src/tag/tag_type.rs index 628779a82..f995a968d 100644 --- a/src/tag/tag_type.rs +++ b/src/tag/tag_type.rs @@ -1,10 +1,12 @@ use super::{utils, Tag}; use crate::config::WriteOptions; +use crate::error::LoftyError; use crate::file::FileType; +use crate::io::{FileLike, Length, Truncate}; use crate::macros::err; use crate::probe::Probe; -use std::fs::{File, OpenOptions}; +use std::fs::OpenOptions; use std::path::Path; /// The tag's format @@ -39,14 +41,19 @@ impl TagType { } #[allow(clippy::shadow_unrelated)] - /// Remove a tag from a [`File`] + /// Remove a tag from a [`FileLike`] /// /// # Errors /// /// * It is unable to guess the file format /// * The format doesn't support the tag /// * It is unable to write to the file - pub fn remove_from(&self, file: &mut File) -> crate::error::Result<()> { + pub fn remove_from(&self, file: &mut F) -> crate::error::Result<()> + where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, + { let probe = Probe::new(file).guess_file_type()?; let Some(file_type) = probe.file_type() else { err!(UnknownFormat); diff --git a/src/tag/utils.rs b/src/tag/utils.rs index 2683764e0..40cd0fcff 100644 --- a/src/tag/utils.rs +++ b/src/tag/utils.rs @@ -1,8 +1,9 @@ use crate::config::WriteOptions; -use crate::error::Result; +use crate::error::{LoftyError, Result}; use crate::file::FileType; use crate::macros::err; use crate::tag::{Tag, TagType}; +use crate::util::io::{FileLike, Length, Truncate}; use crate::{aac, ape, flac, iff, mpeg, musepack, wavpack}; use crate::id3::v1::tag::Id3v1TagRef; @@ -14,16 +15,20 @@ use ape::tag::ApeTagRef; use iff::aiff::tag::AiffTextChunksRef; use iff::wav::tag::RIFFInfoListRef; -use std::fs::File; use std::io::Write; #[allow(unreachable_patterns)] -pub(crate) fn write_tag( +pub(crate) fn write_tag( tag: &Tag, - file: &mut File, + file: &mut F, file_type: FileType, write_options: WriteOptions, -) -> Result<()> { +) -> Result<()> +where + F: FileLike, + LoftyError: From<::Error>, + LoftyError: From<::Error>, +{ match file_type { FileType::Aac => aac::write::write_to(file, tag, write_options), FileType::Aiff => iff::aiff::write::write_to(file, tag, write_options), diff --git a/src/util/io.rs b/src/util/io.rs index 3a2946c6d..7b9f1f0e9 100644 --- a/src/util/io.rs +++ b/src/util/io.rs @@ -1,5 +1,13 @@ +//! Various traits for reading and writing to file-like objects + +use crate::error::LoftyError; + +use std::collections::VecDeque; +use std::fs::File; +use std::io::{Cursor, Read, Seek, Write}; + // TODO: https://github.com/rust-lang/rust/issues/59359 -pub(crate) trait SeekStreamLen: std::io::Seek { +pub(crate) trait SeekStreamLen: Seek { fn stream_len_hack(&mut self) -> crate::error::Result { use std::io::SeekFrom; @@ -12,4 +20,246 @@ pub(crate) trait SeekStreamLen: std::io::Seek { } } -impl SeekStreamLen for T where T: std::io::Seek {} +impl SeekStreamLen for T where T: Seek {} + +/// Provides a method to truncate an object to the specified length +/// +/// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any +/// file saving methods such as [`crate::file::AudioFile::save_to`]. +/// +/// Take great care in implementing this for downstream types, as Lofty will assume that the +/// container has the new length specified. If this assumption were to be broken, files **will** become corrupted. +pub trait Truncate { + /// The error type of the truncation operation + type Error: Into; + + /// Truncate a storage object to the specified length + /// + /// # Errors + /// + /// Errors depend on the object being truncated, which may not always be fallible. + fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error>; +} + +impl Truncate for File { + type Error = std::io::Error; + + fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> { + self.set_len(new_len) + } +} + +impl Truncate for Vec { + type Error = std::convert::Infallible; + + fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> { + self.truncate(new_len as usize); + Ok(()) + } +} + +impl Truncate for VecDeque { + type Error = std::convert::Infallible; + + fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> { + self.truncate(new_len as usize); + Ok(()) + } +} + +impl Truncate for Cursor +where + T: Truncate, +{ + type Error = ::Error; + + fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> { + self.get_mut().truncate(new_len) + } +} + +impl Truncate for Box +where + T: Truncate, +{ + type Error = ::Error; + + fn truncate(&mut self, new_len: u64) -> Result<(), Self::Error> { + self.as_mut().truncate(new_len) + } +} + +/// Provides a method to get the length of a storage object +/// +/// This is one component of the [`FileLike`] trait, which is used to provide implementors access to any +/// file saving methods such as [`crate::file::AudioFile::save_to`]. +/// +/// Take great care in implementing this for downstream types, as Lofty will assume that the +/// container has the exact length specified. If this assumption were to be broken, files **may** become corrupted. +pub trait Length { + /// The error type of the length operation + type Error: Into; + + /// Get the length of a storage object + /// + /// # Errors + /// + /// Errors depend on the object being read, which may not always be fallible. + fn len(&self) -> Result; +} + +impl Length for File { + type Error = std::io::Error; + + fn len(&self) -> Result { + self.metadata().map(|m| m.len()) + } +} + +impl Length for Vec { + type Error = std::convert::Infallible; + + fn len(&self) -> Result { + Ok(self.len() as u64) + } +} + +impl Length for VecDeque { + type Error = std::convert::Infallible; + + fn len(&self) -> Result { + Ok(self.len() as u64) + } +} + +impl Length for Cursor +where + T: Length, +{ + type Error = ::Error; + + fn len(&self) -> Result { + Length::len(self.get_ref()) + } +} + +impl Length for Box +where + T: Length, +{ + type Error = ::Error; + + fn len(&self) -> Result { + Length::len(self.as_ref()) + } +} + +/// Provides a set of methods to read and write to a file-like object +/// +/// This is a combination of the [`Read`], [`Write`], [`Seek`], [`Truncate`], and [`Length`] traits. +/// It is used to provide implementors access to any file saving methods such as [`crate::file::AudioFile::save_to`]. +/// +/// Take great care in implementing this for downstream types, as Lofty will assume that the +/// trait implementations are correct. If this assumption were to be broken, files **may** become corrupted. +pub trait FileLike: Read + Write + Seek + Truncate + Length +where + ::Error: Into, + ::Error: Into, +{ +} + +impl FileLike for T +where + T: Read + Write + Seek + Truncate + Length, + ::Error: Into, + ::Error: Into, +{ +} + +#[cfg(test)] +mod tests { + use crate::config::{ParseOptions, WriteOptions}; + use crate::file::AudioFile; + use crate::mpeg::MpegFile; + use crate::tag::Accessor; + + use std::io::{Cursor, Read, Seek, Write}; + + const TEST_ASSET: &str = "tests/files/assets/minimal/full_test.mp3"; + + fn test_asset_contents() -> Vec { + std::fs::read(TEST_ASSET).unwrap() + } + + fn file() -> MpegFile { + let file_contents = test_asset_contents(); + let mut reader = Cursor::new(file_contents); + MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap() + } + + fn alter_tag(file: &mut MpegFile) { + let tag = file.id3v2_mut().unwrap(); + tag.set_artist(String::from("Bar artist")); + } + + fn revert_tag(file: &mut MpegFile) { + let tag = file.id3v2_mut().unwrap(); + tag.set_artist(String::from("Foo artist")); + } + + #[test] + fn io_save_to_file() { + // Read the file and change the artist + let mut file = file(); + alter_tag(&mut file); + + let mut temp_file = tempfile::tempfile().unwrap(); + let file_content = std::fs::read(TEST_ASSET).unwrap(); + temp_file.write_all(&file_content).unwrap(); + temp_file.rewind().unwrap(); + + // Save the new artist + file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to file"); + + // Read the file again and change the artist back + temp_file.rewind().unwrap(); + let mut file = MpegFile::read_from(&mut temp_file, ParseOptions::new()).unwrap(); + revert_tag(&mut file); + + temp_file.rewind().unwrap(); + file.save_to(&mut temp_file, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to file"); + + // The contents should be the same as the original file + temp_file.rewind().unwrap(); + let mut current_file_contents = Vec::new(); + temp_file.read_to_end(&mut current_file_contents).unwrap(); + + assert_eq!(current_file_contents, test_asset_contents()); + } + + #[test] + fn io_save_to_vec() { + // Same test as above, but using a Cursor> instead of a file + let mut file = file(); + alter_tag(&mut file); + + let file_content = std::fs::read(TEST_ASSET).unwrap(); + + let mut reader = Cursor::new(file_content); + file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to vec"); + + reader.rewind().unwrap(); + let mut file = MpegFile::read_from(&mut reader, ParseOptions::new()).unwrap(); + revert_tag(&mut file); + + reader.rewind().unwrap(); + file.save_to(&mut reader, WriteOptions::new().preferred_padding(0)) + .expect("Failed to save to vec"); + + let current_file_contents = reader.into_inner(); + assert_eq!(current_file_contents, test_asset_contents()); + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 009c84209..351aa415b 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,4 +1,4 @@ pub(crate) mod alloc; -pub(crate) mod io; +pub mod io; pub(crate) mod math; pub(crate) mod text;