misc: Make all file writes generic

This commit is contained in:
Serial 2023-11-11 13:16:42 -05:00 committed by Alex
parent 3402a69f26
commit e919cd97a3
31 changed files with 721 additions and 276 deletions

View file

@ -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::<lofty::id3::v1::tag::Id3v1TagRef<'_>>::into(tag).write_to(data, write_options)
Into::<lofty::id3::v1::tag::Id3v1TagRef<'_>>::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<F>(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<<F as ::lofty::io::Truncate>::Error>,
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Length>::Error>,
{
match tag.tag_type() {
#( #applicable_formats )*
_ => crate::macros::err!(UnsupportedTag),

View file

@ -445,7 +445,12 @@ fn generate_audiofile_impl(file: &LoftyFile) -> syn::Result<proc_macro2::TokenSt
#read_fn(reader, parse_options)
}
fn save_to(&self, file: &mut ::std::fs::File, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()> {
fn save_to<F>(&self, file: &mut F, write_options: ::lofty::config::WriteOptions) -> ::lofty::error::Result<()>
where
F: ::lofty::io::FileLike,
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Truncate>::Error>,
::lofty::error::LoftyError: ::std::convert::From<<F as ::lofty::io::Length>::Error>,
{
use ::lofty::tag::TagExt as _;
use ::std::io::Seek as _;
#save_to_body

View file

@ -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<F>(
&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<<F as Truncate>::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<P: AsRef<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<Item = ApeItemRef<'a>>,
{
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
{
write::write_to(file, self, write_options)
}

View file

@ -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<Item = ApeItemRef<'a>>,
F: FileLike,
LoftyError: From<<F as Truncate>::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(())
}

View file

@ -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<std::collections::TryReserveError> for LoftyError {
}
}
impl From<std::convert::Infallible> 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"),
}
}
}

View file

@ -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<TaggedFile> {
/// tagged_file.save_to(&mut file, WriteOptions::default())?;
/// # Ok(()) }
/// ```
fn save_to(&self, file: &mut File, write_options: WriteOptions) -> Result<()>;
fn save_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>;
/// Returns a reference to the file's properties
fn properties(&self) -> &Self::Properties;

View file

@ -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<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
self.inner.save_to(file, write_options)
}

View file

@ -10,7 +10,7 @@ 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;
@ -18,13 +18,10 @@ use crate::ogg::{OggPictureStorage, VorbisComments};
use crate::picture::{Picture, PictureInformation};
use crate::tag::TagExt;
use std::fs::File;
use std::io::Seek;
use lofty_attr::LoftyFile;
// Exports
use crate::util::io::{FileLike, Length, Truncate};
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<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
if let Some(ref id3v2) = self.id3v2_tag {
id3v2.save_to(file, write_options)?;
file.rewind()?;

View file

@ -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 std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::io::{Cursor, Seek, SeekFrom, Write};
use crate::util::io::{FileLike, Length, Truncate};
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<F>(file: &mut F, tag: &Tag, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<<F as Truncate>::Error>,
II: Iterator<Item = (&'a str, &'a str)>,
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
{
@ -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(())

View file

@ -2,12 +2,11 @@ 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 std::borrow::Cow;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::util::io::{FileLike, Length, Truncate};
use lofty_attr::tag;
macro_rules! impl_accessor {
@ -209,6 +208,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 +246,16 @@ impl TagExt for Id3v1Tag {
&& self.genre.is_none()
}
fn save_to(
fn save_to<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
Into::<Id3v1TagRef<'_>>::into(self).write_to(file, write_options)
}
@ -267,7 +276,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<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
TagType::Id3v1.remove_from(file)
}
@ -420,7 +434,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<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
super::write::write_id3v1(file, self, write_options)
}

View file

@ -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 std::fs::File;
use std::io::{Cursor, Seek, Write};
use crate::util::io::{FileLike, Length, Truncate};
use byteorder::WriteBytesExt;
#[allow(clippy::shadow_unrelated)]
pub(crate) fn write_id3v1(
file: &mut File,
pub(crate) fn write_id3v1<F>(
file: &mut F,
tag: &Id3v1TagRef<'_>,
_write_options: WriteOptions,
) -> Result<()> {
) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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(());
}

View file

@ -24,11 +24,10 @@ use crate::tag::{
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 crate::util::io::{FileLike, Length, Truncate};
use lofty_attr::tag;
const USER_DEFINED_TEXT_FRAME_ID: &str = "TXXX";
@ -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<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<P: AsRef<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<Item = FrameRef<'_>> + Clon
}
impl<'a, I: Iterator<Item = FrameRef<'a>> + Clone + 'a> Id3v2TagRef<'a, I> {
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
super::write::write_id3v2(file, self, write_options)
}

View file

@ -1,45 +1,47 @@
use crate::config::WriteOptions;
use crate::error::Result;
use crate::error::{LoftyError, Result};
use crate::iff::chunk::Chunks;
use std::io::SeekFrom;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use crate::util::io::{FileLike, Length, Truncate};
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<B>(
data: &mut File,
pub(in crate::id3::v2) fn write_to_chunk_file<F, B>(
file: &mut F,
tag: &[u8],
write_options: WriteOptions,
) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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::<B>::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 +49,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::<B>(tag.len() as u32)?;
data.write_all(tag)?;
file.write_u32::<B>(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::<B>(total_size as u32)?;
file.write_u32::<B>(total_size as u32)?;
}
Ok(())

View file

@ -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;
@ -13,11 +13,11 @@ use crate::id3::{find_id3v2, FindId3v2Config};
use crate::macros::{err, try_vec};
use crate::probe::Probe;
use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::ops::Not;
use std::sync::OnceLock;
use crate::util::io::{FileLike, Length, Truncate};
use byteorder::{BigEndian, LittleEndian, WriteBytesExt};
// In the very rare chance someone wants to write a CRC in their extended header
@ -38,15 +38,21 @@ fn crc_32_table() -> &'static [u32; 256] {
}
#[allow(clippy::shadow_unrelated)]
pub(crate) fn write_id3v2<'a, I: Iterator<Item = FrameRef<'a>> + 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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
I: Iterator<Item = FrameRef<'a>> + 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<Item = FrameRef<'a>> + 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::<LittleEndian>(data, &id3v2, write_options);
return chunk_file::write_to_chunk_file::<F, LittleEndian>(file, &id3v2, write_options);
},
FileType::Aiff => {
tag.flags.footer = false;
return chunk_file::write_to_chunk_file::<BigEndian>(data, &id3v2, write_options);
return chunk_file::write_to_chunk_file::<F, BigEndian>(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(())
}

View file

@ -5,10 +5,10 @@ use crate::macros::err;
use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType};
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 crate::util::io::{FileLike, Length, Truncate};
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<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<P: AsRef<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<str>,
AI: IntoIterator<Item = T>,
{
pub(crate) fn write_to(self, file: &mut File, _write_options: WriteOptions) -> Result<()> {
pub(crate) fn write_to<F>(self, file: &mut F, _write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<F>(file: &mut F, mut tag: AiffTextChunksRef<'_, T, AI>) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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::<BigEndian>::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(())
}

View file

@ -8,10 +8,9 @@ use crate::tag::{
};
use std::borrow::Cow;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use crate::util::io::{FileLike, Length, Truncate};
use lofty_attr::tag;
macro_rules! impl_accessor {
@ -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<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<P: AsRef<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<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
write::write_riff_info(file, self, write_options)
}

View file

@ -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 std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::io::{Read, Seek, SeekFrom};
use crate::util::io::{FileLike, Length, Truncate};
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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
I: Iterator<Item = (&'a str, &'a str)>,
{
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::<LittleEndian>(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::<LittleEndian>(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(())
}

View file

@ -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.
//!

View file

@ -15,11 +15,10 @@ use crate::tag::{
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 crate::util::io::{FileLike, Length, Truncate};
use lofty_attr::tag;
const ARTIST: AtomIdent<'_> = AtomIdent::Fourcc(*b"\xa9ART");
@ -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<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<P: AsRef<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();
}

View file

@ -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 std::fs::File;
use crate::util::io::{FileLike, Length, Truncate};
use std::io::Write;
impl Ilst {
@ -25,7 +25,12 @@ impl<'a, I: 'a> IlstRef<'a, I>
where
I: IntoIterator<Item = &'a AtomData>,
{
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
super::write::write_to(file, self, write_options)
}

View file

@ -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,10 +9,9 @@ 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 std::fs::File;
use std::io::{Cursor, Seek, SeekFrom, Write};
use crate::util::io::{FileLike, Length, Truncate};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
// A "full" atom is a traditional length + identifier, followed by a version (1) and flags (3)
@ -20,25 +19,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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
I: IntoIterator<Item = &'a AtomData>,
{
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 +200,7 @@ where
drop(write_handle);
atom_writer.save_to(data)?;
atom_writer.save_to(file)?;
Ok(())
}

View file

@ -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<Self> {
pub(super) fn new_from_file<F>(file: &mut F, parse_mode: ParsingMode) -> Result<Self>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<F>(&mut self, file: &mut F) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
file.rewind()?;
file.set_len(0)?;
file.truncate(0)?;
file.write_all(self.contents.borrow().get_ref())?;
Ok(())

View file

@ -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<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<P: AsRef<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<Item = (&'a Picture, PictureInformation)>,
{
#[allow(clippy::shadow_unrelated)]
pub(crate) fn write_to(&mut self, file: &mut File, write_options: WriteOptions) -> Result<()> {
pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
let probe = Probe::new(file).guess_file_type()?;
let f_ty = probe.file_type();

View file

@ -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<F>(
file: &mut F,
tag: &Tag,
file_type: FileType,
write_options: WriteOptions,
) -> Result<()> {
) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
II: Iterator<Item = (&'a str, &'a str)>,
IP: Iterator<Item = (&'a Picture, PictureInformation)>,
{
@ -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;

View file

@ -14,11 +14,11 @@ use crate::picture::{Picture, PictureType};
use crate::probe::Probe;
use std::borrow::Cow;
use std::fs::File;
use std::io::Write;
use std::path::Path;
// Exports
use crate::util::io::{FileLike, Length, Truncate};
pub use accessor::Accessor;
pub use item::{ItemKey, ItemValue, TagItem};
pub use split_merge_tag::{MergeTag, SplitTag};
@ -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(
/// * Attempting to write a tag to a format that does not support it. See [`FileType::supports_tag_type`](crate::FileType::supports_tag_type)
fn save_to<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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`](crate::FileLike)
///
/// # Errors
///
/// See [`TagType::remove_from`]
fn remove_from(&self, file: &mut File) -> std::result::Result<(), Self::Err> {
fn remove_from<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
self.tag_type.remove_from(file)
}

View file

@ -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<Tag> + Sized {
/// The associated error which can be returned from IO operations
type Err: From<std::io::Error>;
type Err: From<std::io::Error> + From<LoftyError>;
/// 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.
@ -95,11 +99,15 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
///
/// * 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<F>(
&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<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>;
#[allow(clippy::missing_errors_doc)]
/// Dump the tag to a writer
@ -116,7 +124,9 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
/// # Errors
///
/// See [`TagExt::remove_from`]
fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err>;
fn remove_from_path<P: AsRef<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`]
///
@ -125,7 +135,14 @@ pub trait TagExt: Accessor + Into<Tag> + Sized {
/// * 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<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
self.tag_type().remove_from(file).map_err(Into::into)
}
/// Clear the tag, removing all items
///

View file

@ -4,7 +4,9 @@ use crate::file::FileType;
use crate::macros::err;
use crate::probe::Probe;
use std::fs::{File, OpenOptions};
use crate::error::LoftyError;
use crate::io::{FileLike, Length, Truncate};
use std::fs::OpenOptions;
use std::path::Path;
/// The tag's format
@ -46,7 +48,12 @@ impl TagType {
/// * 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<F>(&self, file: &mut F) -> crate::error::Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::Error>,
{
let probe = Probe::new(file).guess_file_type()?;
let Some(file_type) = probe.file_type() else {
err!(UnknownFormat);

View file

@ -1,5 +1,5 @@
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};
@ -14,16 +14,21 @@ use ape::tag::ApeTagRef;
use iff::aiff::tag::AiffTextChunksRef;
use iff::wav::tag::RIFFInfoListRef;
use std::fs::File;
use crate::util::io::{FileLike, Length, Truncate};
use std::io::Write;
#[allow(unreachable_patterns)]
pub(crate) fn write_tag(
pub(crate) fn write_tag<F>(
tag: &Tag,
file: &mut File,
file: &mut F,
file_type: FileType,
write_options: WriteOptions,
) -> Result<()> {
) -> Result<()>
where
F: FileLike,
LoftyError: From<<F as Truncate>::Error>,
LoftyError: From<<F as Length>::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),

0
src/traits.rs Normal file
View file

View file

@ -1,3 +1,10 @@
//! 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 {
fn stream_len_hack(&mut self) -> crate::error::Result<u64> {
@ -13,3 +20,245 @@ pub(crate) trait SeekStreamLen: std::io::Seek {
}
impl<T> SeekStreamLen for T where T: std::io::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::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<LoftyError>;
/// 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) -> std::result::Result<(), Self::Error>;
}
impl Truncate for File {
type Error = std::io::Error;
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.set_len(new_len)
}
}
impl Truncate for Vec<u8> {
type Error = std::convert::Infallible;
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.truncate(new_len as usize);
Ok(())
}
}
impl Truncate for VecDeque<u8> {
type Error = std::convert::Infallible;
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.truncate(new_len as usize);
Ok(())
}
}
impl<T> Truncate for Cursor<T>
where
T: Truncate,
{
type Error = <T as Truncate>::Error;
fn truncate(&mut self, new_len: u64) -> std::result::Result<(), Self::Error> {
self.get_mut().truncate(new_len)
}
}
impl<T> Truncate for Box<T>
where
T: Truncate,
{
type Error = <T as Truncate>::Error;
fn truncate(&mut self, new_len: u64) -> std::result::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::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<LoftyError>;
/// Get the length of a storage object
///
/// # Errors
///
/// Errors depend on the object being read, which may not always be fallible.
fn len(&self) -> std::result::Result<u64, Self::Error>;
}
impl Length for File {
type Error = std::io::Error;
fn len(&self) -> std::result::Result<u64, Self::Error> {
self.metadata().map(|m| m.len())
}
}
impl Length for Vec<u8> {
type Error = std::convert::Infallible;
fn len(&self) -> std::result::Result<u64, Self::Error> {
Ok(self.len() as u64)
}
}
impl Length for VecDeque<u8> {
type Error = std::convert::Infallible;
fn len(&self) -> std::result::Result<u64, Self::Error> {
Ok(self.len() as u64)
}
}
impl<T> Length for Cursor<T>
where
T: Length,
{
type Error = <T as Length>::Error;
fn len(&self) -> std::result::Result<u64, Self::Error> {
Length::len(self.get_ref())
}
}
impl<T> Length for Box<T>
where
T: Length,
{
type Error = <T as Length>::Error;
fn len(&self) -> std::result::Result<u64, Self::Error> {
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::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
<Self as Truncate>::Error: Into<LoftyError>,
<Self as Length>::Error: Into<LoftyError>,
{
}
impl<T> FileLike for T
where
T: Read + Write + Seek + Truncate + Length,
<T as Truncate>::Error: Into<LoftyError>,
<T as Length>::Error: Into<LoftyError>,
{
}
#[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<u8> {
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<Vec<u8>> 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());
}
}

View file

@ -1,4 +1,4 @@
pub(crate) mod alloc;
pub(crate) mod io;
pub mod io;
pub(crate) mod math;
pub(crate) mod text;