Skip to content

Make all file writes generic #290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>>` 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))
Expand Down
19 changes: 12 additions & 7 deletions lofty_attr/src/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,32 +48,32 @@ 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, {
lofty::id3::v2::tag::Id3v2TagRef {
flags: lofty::id3::v2::Id3v2TagFlags::default(),
frames: lofty::id3::v2::tag::tag_frames(tag),
}
.write_to(data, write_options)
.write_to(file, write_options)
});
}

insert!(map, RiffInfo, {
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, {
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down
7 changes: 6 additions & 1 deletion lofty_attr/src/lofty_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 18 additions & 14 deletions src/ape/tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
Expand All @@ -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),
Expand All @@ -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();
}
Expand Down Expand Up @@ -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)
}

Expand Down
48 changes: 25 additions & 23 deletions src/ape/tag/write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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 {
Expand All @@ -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(())
}
Expand Down
12 changes: 12 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"),
}
}
}
11 changes: 8 additions & 3 deletions src/file/audio_file.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand Down
17 changes: 14 additions & 3 deletions src/file/tagged_file.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down
Loading