diff --git a/src/lib.rs b/src/lib.rs index 6975bc1b9..a3d407c0f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub use crate::error::{partial_tag_ok, Error, ErrorKind, Result}; pub use crate::frame::{Content, Frame, Timestamp}; +pub use crate::storage::StorageFile; pub use crate::stream::encoding::Encoding; pub use crate::stream::tag::Encoder; pub use crate::tag::{Tag, Version}; diff --git a/src/storage.rs b/src/storage.rs index de85af788..e9cd76563 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -43,7 +43,10 @@ where region: ops::Range, } -pub trait StorageFile: io::Read + io::Write + io::Seek { +/// This trait is the combination of the [`std::io`] stream traits with an additional method to resize the +/// file. +pub trait StorageFile: io::Read + io::Write + io::Seek + private::Sealed { + /// Performs the resize. Assumes the same behaviour as [`std::fs::File::set_len`]. fn set_len(&mut self, new_len: u64) -> io::Result<()>; } @@ -276,6 +279,15 @@ where } } +// https://rust-lang.github.io/api-guidelines/future-proofing.html#c-sealed +mod private { + pub trait Sealed {} + + impl<'a, T: Sealed> Sealed for &'a mut T {} + impl Sealed for std::fs::File {} + impl Sealed for std::io::Cursor> {} +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/stream/tag.rs b/src/stream/tag.rs index 6c31557dc..712f18216 100644 --- a/src/stream/tag.rs +++ b/src/stream/tag.rs @@ -1,4 +1,4 @@ -use crate::storage::{PlainStorage, Storage}; +use crate::storage::{PlainStorage, Storage, StorageFile}; use crate::stream::{frame, unsynch}; use crate::tag::{Tag, Version}; use crate::taglike::TagLike; @@ -420,7 +420,7 @@ impl Encoder { } /// Encodes a [`Tag`] and replaces any existing tag in the file. - pub fn encode_to_file(&self, tag: &Tag, mut file: &mut fs::File) -> crate::Result<()> { + pub fn write_to_file(&self, tag: &Tag, mut file: impl StorageFile) -> crate::Result<()> { #[allow(clippy::reversed_empty_ranges)] let location = locate_id3v2(&mut file)?.unwrap_or(0..0); // Create a new tag if none could be located. @@ -431,13 +431,25 @@ impl Encoder { Ok(()) } + /// Encodes a [`Tag`] and replaces any existing tag in the file. + #[deprecated(note = "Use write_to_file")] + pub fn encode_to_file(&self, tag: &Tag, file: &mut fs::File) -> crate::Result<()> { + self.write_to_file(tag, file) + } + /// Encodes a [`Tag`] and replaces any existing tag in the file pointed to by the specified path. - pub fn encode_to_path(&self, tag: &Tag, path: impl AsRef) -> crate::Result<()> { + pub fn write_to_path(&self, tag: &Tag, path: impl AsRef) -> crate::Result<()> { let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?; - self.encode_to_file(tag, &mut file)?; + self.write_to_file(tag, &mut file)?; file.flush()?; Ok(()) } + + /// Encodes a [`Tag`] and replaces any existing tag in the file pointed to by the specified path. + #[deprecated(note = "Use write_to_path")] + pub fn encode_to_path(&self, tag: &Tag, path: impl AsRef) -> crate::Result<()> { + self.write_to_path(tag, path) + } } impl Default for Encoder { diff --git a/src/tag.rs b/src/tag.rs index e9af8c80d..ae9beffe6 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -7,6 +7,7 @@ use crate::storage::{PlainStorage, Storage}; use crate::stream; use crate::taglike::TagLike; use crate::v1; +use crate::StorageFile; use std::fmt; use std::fs::{self, File}; use std::io::{self, BufReader, Write}; @@ -189,7 +190,7 @@ impl<'a> Tag { /// Attempts to write the ID3 tag to the writer using the specified version. /// /// Note that the plain tag is written, regardless of the original contents. To safely encode a - /// tag to an MP3 file, use `Tag::write_to_path`. + /// tag to an MP3 file, use `Tag::write_to_file`. pub fn write_to(&self, writer: impl io::Write, version: Version) -> crate::Result<()> { stream::tag::Encoder::new() .version(version) @@ -199,8 +200,7 @@ impl<'a> Tag { /// Attempts to write the ID3 tag from the file at the indicated path. If the specified path is /// the same path which the tag was read from, then the tag will be written to the padding if /// possible. - pub fn write_to_path(&self, path: impl AsRef, version: Version) -> crate::Result<()> { - let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?; + pub fn write_to_file(&self, mut file: impl StorageFile, version: Version) -> crate::Result<()> { #[allow(clippy::reversed_empty_ranges)] let location = stream::tag::locate_id3v2(&mut file)?.unwrap_or(0..0); // Create a new tag if none could be located. @@ -213,6 +213,12 @@ impl<'a> Tag { Ok(()) } + /// Conventience function for [`write_to_file`]. + pub fn write_to_path(&self, path: impl AsRef, version: Version) -> crate::Result<()> { + let file = fs::OpenOptions::new().read(true).write(true).open(path)?; + self.write_to_file(file, version) + } + /// Overwrite WAV file ID3 chunk in a file pub fn write_to_aiff_path( &self, diff --git a/src/v1.rs b/src/v1.rs index 1c5f6b64d..e020b9b1c 100644 --- a/src/v1.rs +++ b/src/v1.rs @@ -1,7 +1,7 @@ -use crate::{Error, ErrorKind}; +use crate::{Error, ErrorKind, StorageFile}; use std::cmp; use std::fs; -use std::io::{self, Read, Seek}; +use std::io; use std::ops; use std::path::Path; @@ -306,9 +306,19 @@ impl Tag { /// The file cursor position will be reset back to the previous position before returning. /// /// Returns true if the file initially contained a tag. + #[deprecated(note = "Use remove_from_file")] pub fn remove(file: &mut fs::File) -> crate::Result { + Self::remove_from_file(file) + } + + /// Removes an ID3v1 tag plus possible extended data if any. + /// + /// The file cursor position will be reset back to the previous position before returning. + /// + /// Returns true if the file initially contained a tag. + pub fn remove_from_file(mut file: impl StorageFile) -> crate::Result { let cur_pos = file.stream_position()?; - let file_len = file.metadata()?.len(); + let file_len = file.seek(io::SeekFrom::End(0))?; let has_ext_tag = if file_len >= XTAG_CHUNK.start.unsigned_abs() { file.seek(io::SeekFrom::End(XTAG_CHUNK.start))?; let mut b = [0; 4]; @@ -348,7 +358,7 @@ impl Tag { /// Returns true if the file initially contained a tag. pub fn remove_from_path(path: impl AsRef) -> crate::Result { let mut file = fs::OpenOptions::new().read(true).write(true).open(path)?; - Tag::remove(&mut file) + Tag::remove_from_file(&mut file) } /// Returns `genre_str`, falling back to translating `genre_id` to a string. @@ -366,6 +376,7 @@ impl Tag { mod tests { use super::*; use std::fs; + use std::io::Seek; use tempfile::tempdir; #[test] @@ -401,8 +412,8 @@ mod tests { .open(&tmp_name) .unwrap(); tag_file.seek(io::SeekFrom::Start(0)).unwrap(); - assert!(Tag::remove(&mut tag_file).unwrap()); + assert!(Tag::remove_from_file(&mut tag_file).unwrap()); tag_file.seek(io::SeekFrom::Start(0)).unwrap(); - assert!(!Tag::remove(&mut tag_file).unwrap()); + assert!(!Tag::remove_from_file(&mut tag_file).unwrap()); } } diff --git a/src/v1v2.rs b/src/v1v2.rs index c37b00b60..3182525ff 100644 --- a/src/v1v2.rs +++ b/src/v1v2.rs @@ -1,10 +1,11 @@ -use crate::{v1, Error, ErrorKind, Tag, Version}; +use crate::{v1, Error, ErrorKind, StorageFile, Tag, Version}; +use std::fs; use std::fs::File; +use std::io; use std::path::Path; /// Returns which tags are present in the specified file. -pub fn is_candidate_path(path: impl AsRef) -> crate::Result { - let mut file = File::open(path)?; +pub fn is_candidate(mut file: impl io::Read + io::Seek) -> crate::Result { let v2 = Tag::is_candidate(&mut file)?; let v1 = v1::Tag::is_candidate(&mut file)?; Ok(match (v1, v2) { @@ -15,11 +16,16 @@ pub fn is_candidate_path(path: impl AsRef) -> crate::Result }) } +/// Returns which tags are present in the specified file. +pub fn is_candidate_path(path: impl AsRef) -> crate::Result { + is_candidate(File::open(path)?) +} + /// Attempts to read an ID3v2 or ID3v1 tag, in that order. /// /// If neither version tag is found, an error with [`ErrorKind::NoTag`] is returned. -pub fn read_from_path(path: impl AsRef) -> crate::Result { - match Tag::read_from_path(&path) { +pub fn read_from(mut file: impl io::Read + io::Seek) -> crate::Result { + match Tag::read_from(&mut file) { Err(Error { kind: ErrorKind::NoTag, .. @@ -28,7 +34,7 @@ pub fn read_from_path(path: impl AsRef) -> crate::Result { Ok(tag) => return Ok(tag), } - match v1::Tag::read_from_path(path) { + match v1::Tag::read_from(file) { Err(Error { kind: ErrorKind::NoTag, .. @@ -43,17 +49,30 @@ pub fn read_from_path(path: impl AsRef) -> crate::Result { )) } +/// Attempts to read an ID3v2 or ID3v1 tag, in that order. +/// +/// If neither version tag is found, an error with [`ErrorKind::NoTag`] is returned. +pub fn read_from_path(path: impl AsRef) -> crate::Result { + read_from(File::open(path)?) +} + /// Writes the specified tag to a file. Any existing ID3v2 tag is replaced or added if it is not /// present. /// /// If any ID3v1 tag is present it will be REMOVED as it is not able to fully represent a ID3v2 /// tag. -pub fn write_to_path(path: impl AsRef, tag: &Tag, version: Version) -> crate::Result<()> { - tag.write_to_path(&path, version)?; - v1::Tag::remove_from_path(path)?; +pub fn write_to_file(mut file: impl StorageFile, tag: &Tag, version: Version) -> crate::Result<()> { + tag.write_to_file(&mut file, version)?; + v1::Tag::remove_from_file(&mut file)?; Ok(()) } +/// Conventience function for [`write_to_file`]. +pub fn write_to_path(path: impl AsRef, tag: &Tag, version: Version) -> crate::Result<()> { + let file = fs::OpenOptions::new().read(true).write(true).open(path)?; + write_to_file(file, tag, version) +} + /// Ensures that both ID3v1 and ID3v2 are not present in the specified file. /// /// Returns [`FormatVersion`] representing the previous state.