diff --git a/lofty_attr/src/internal.rs b/lofty_attr/src/internal.rs index 158a5d0a..f762efe8 100644 --- a/lofty_attr/src/internal.rs +++ b/lofty_attr/src/internal.rs @@ -1,6 +1,6 @@ // Items that only pertain to internal usage of lofty_attr -use crate::FieldContents; +use crate::lofty_file::FieldContents; use std::collections::HashMap; diff --git a/lofty_attr/src/lib.rs b/lofty_attr/src/lib.rs index 3b77e5f4..f57e976f 100644 --- a/lofty_attr/src/lib.rs +++ b/lofty_attr/src/lib.rs @@ -1,22 +1,52 @@ mod internal; +mod lofty_file; +mod lofty_tag; mod util; use proc_macro::TokenStream; -use proc_macro2::Span; -use quote::{format_ident, quote, quote_spanned, ToTokens}; -use syn::spanned::Spanned; -use syn::{parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Fields, Ident, Type}; +use quote::quote; +use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields}; /// Creates a file usable by Lofty /// /// See [here](https://github.com/Serial-ATA/lofty-rs/tree/main/examples/custom_resolver) for an example of how to use it. -// TODO: #[internal] #[proc_macro_derive(LoftyFile, attributes(lofty))] pub fn lofty_file(input: TokenStream) -> TokenStream { + act(input, lofty_file::parse) +} + +#[proc_macro_derive(LoftyTag, attributes(lofty))] +#[doc(hidden)] +pub fn lofty_tag(input: TokenStream) -> TokenStream { + act(input, lofty_tag::parse) +} + +fn act( + input: TokenStream, + func: impl Fn(&DeriveInput, &DataStruct, &mut Vec) -> proc_macro2::TokenStream, +) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); + let data_struct = match input.data { + Data::Struct( + ref data_struct @ DataStruct { + fields: Fields::Named(_), + .. + }, + ) => data_struct, + _ => { + return TokenStream::from( + util::err( + input.ident.span(), + "This macro can only be used on structs with named fields", + ) + .to_compile_error(), + ); + }, + }; + let mut errors = Vec::new(); - let ret = parse(input, &mut errors); + let ret = func(&input, data_struct, &mut errors); let compile_errors = errors.iter().map(syn::Error::to_compile_error); @@ -25,353 +55,3 @@ pub fn lofty_file(input: TokenStream) -> TokenStream { #ret }) } - -fn parse(input: DeriveInput, errors: &mut Vec) -> proc_macro2::TokenStream { - macro_rules! bail { - ($errors:ident, $span:expr, $msg:literal) => { - $errors.push(util::err($span, $msg)); - return proc_macro2::TokenStream::new(); - }; - } - - let data = match input.data { - Data::Struct( - ref data_struct @ DataStruct { - fields: Fields::Named(_), - .. - }, - ) => data_struct, - _ => { - bail!( - errors, - input.ident.span(), - "This macro can only be used on structs with named fields" - ); - }, - }; - - let impl_audiofile = should_impl_audiofile(&input.attrs); - - let read_fn = match util::get_attr("read_fn", &input.attrs) { - Some(rfn) => rfn, - _ if impl_audiofile => { - bail!( - errors, - input.ident.span(), - "Expected a #[read_fn] attribute" - ); - }, - _ => proc_macro2::TokenStream::new(), - }; - - let struct_name = input.ident.clone(); - - // TODO: This is not readable in the slightest - - let opt_file_type = internal::opt_internal_file_type(struct_name.to_string()); - - let has_internal_file_type = opt_file_type.is_some(); - let is_internal = input - .attrs - .iter() - .any(|attr| util::has_path_attr(attr, "internal_write_module_do_not_use_anywhere_else")); - - if opt_file_type.is_none() && is_internal { - // TODO: This is the best check we can do for now I think? - // Definitely needs some work when a better solution comes out. - bail!( - errors, - input.ident.span(), - "Attempted to use an internal attribute externally" - ); - } - - let mut id3v2_strippable = false; - let file_type = match opt_file_type { - Some((ft, id3v2_strip)) => { - id3v2_strippable = id3v2_strip; - ft - }, - _ => match util::get_attr("file_type", &input.attrs) { - Some(rfn) => rfn, - _ => { - bail!( - errors, - input.ident.span(), - "Expected a #[file_type] attribute" - ); - }, - }, - }; - - let (tag_fields, properties_field) = match get_fields(errors, data) { - Some(fields) => fields, - None => return proc_macro2::TokenStream::new(), - }; - - if tag_fields.is_empty() { - errors.push(util::err(input.ident.span(), "Struct has no tag fields")); - } - - let assert_tag_impl_into = tag_fields.iter().enumerate().map(|(i, f)| { - let name = format_ident!("_AssertTagExt{}", i); - let field_ty = &f.ty; - quote_spanned! {field_ty.span()=> - struct #name where #field_ty: lofty::TagExt; - } - }); - - let tag_exists = tag_fields.iter().map(|f| { - let name = &f.name; - if f.needs_option { - quote! { self.#name.is_some() } - } else { - quote! { true } - } - }); - let tag_exists_2 = tag_exists.clone(); - - let tag_type = tag_fields.iter().map(|f| &f.tag_type); - - let properties_field = if let Some(field) = properties_field { - field - } else { - bail!(errors, input.ident.span(), "Struct has no properties field"); - }; - - let properties_field_ty = &properties_field.ty; - let assert_properties_impl = quote_spanned! {properties_field_ty.span()=> - struct _AssertIntoFileProperties where #properties_field_ty: std::convert::Into; - }; - - let audiofile_impl = if impl_audiofile { - quote! { - impl lofty::AudioFile for #struct_name { - type Properties = #properties_field_ty; - - fn read_from(reader: &mut R, parse_options: lofty::ParseOptions) -> lofty::error::Result - where - R: std::io::Read + std::io::Seek, - { - #read_fn(reader, parse_options) - } - - fn properties(&self) -> &Self::Properties { - &self.properties - } - - #[allow(unreachable_code)] - fn contains_tag(&self) -> bool { - #( #tag_exists )||* - } - - #[allow(unreachable_code, unused_variables)] - fn contains_tag_type(&self, tag_type: lofty::TagType) -> bool { - match tag_type { - #( lofty::TagType::#tag_type => { #tag_exists_2 } ),* - _ => false - } - } - } - } - } else { - proc_macro2::TokenStream::new() - }; - - let conditions = tag_fields.iter().map(|f| { - let name = &f.name; - if f.needs_option { - quote! { if let Some(t) = input.#name { tags.push(t.into()); } } - } else { - quote! { tags.push(input.#name.into()); } - } - }); - - let getters = get_getters(&tag_fields, &struct_name); - - let file_type_variant = if has_internal_file_type { - quote! { lofty::FileType::#file_type } - } else { - let file_ty_str = file_type.to_string(); - quote! { lofty::FileType::Custom(#file_ty_str) } - }; - - let mut ret = quote! { - #assert_properties_impl - - #( #assert_tag_impl_into )* - - #audiofile_impl - - impl std::convert::From<#struct_name> for lofty::TaggedFile { - fn from(input: #struct_name) -> Self { - lofty::TaggedFile::new( - #file_type_variant, - lofty::FileProperties::from(input.properties), - { - let mut tags: Vec = Vec::new(); - #( #conditions )* - - tags - } - ) - } - } - - #( #getters )* - }; - - // Create `write` module if internal - if is_internal { - let lookup = internal::init_write_lookup(id3v2_strippable); - let write_mod = internal::write_module(&tag_fields, lookup); - - ret = quote! { - #ret - - #write_mod - } - } - - ret -} - -struct FieldContents { - name: Ident, - cfg_features: Vec, - needs_option: bool, - getter_name: Option, - ty: Type, - tag_type: proc_macro2::TokenStream, -} - -fn get_fields<'a>( - errors: &mut Vec, - data: &'a DataStruct, -) -> Option<(Vec, Option<&'a syn::Field>)> { - let mut tag_fields = Vec::new(); - let mut properties_field = None; - - for field in &data.fields { - let name = field.ident.clone().unwrap(); - if name.to_string().ends_with("_tag") { - let tag_type = match util::get_attr("tag_type", &field.attrs) { - Some(tt) => tt, - _ => { - errors.push(util::err(field.span(), "Field has no `tag_type` attribute")); - return None; - }, - }; - - let cfg = field - .attrs - .iter() - .cloned() - .filter_map(|a| util::get_attr_list("cfg", &a).map(|_| a)) - .collect::>(); - - let option_unwrapped = util::extract_type_from_option(&field.ty); - // `option_unwrapped` will be `Some` if the type was wrapped in an `Option` - let needs_option = option_unwrapped.is_some(); - - let contents = FieldContents { - name, - getter_name: util::get_attr("getter", &field.attrs), - ty: option_unwrapped.unwrap_or_else(|| field.ty.clone()), - tag_type, - needs_option, - cfg_features: cfg, - }; - tag_fields.push(contents); - continue; - } - - if name == "properties" { - properties_field = Some(field); - } - } - - Some((tag_fields, properties_field)) -} - -fn should_impl_audiofile(attrs: &[Attribute]) -> bool { - for attr in attrs { - if util::has_path_attr(attr, "no_audiofile_impl") { - return false; - } - } - - true -} - -fn get_getters<'a>( - tag_fields: &'a [FieldContents], - struct_name: &'a Ident, -) -> impl Iterator + 'a { - tag_fields.iter().map(move |f| { - let name = f.getter_name.clone().unwrap_or_else(|| { - let name = f.name.to_string().strip_suffix("_tag").unwrap().to_string(); - - Ident::new(&name, f.name.span()).into_token_stream() - }); - - let (ty_prefix, ty_suffix) = if f.needs_option { - (quote! {Option<}, quote! {>}) - } else { - (quote! {}, quote! {}) - }; - - let field_name = &f.name; - let field_ty = &f.ty; - - let ref_access = if f.needs_option { - quote! {self.#field_name.as_ref()} - } else { - quote! {&self.#field_name} - }; - - let mut_ident = Ident::new(&format!("{}_mut", name), Span::call_site()); - - let mut_access = if f.needs_option { - quote! {self.#field_name.as_mut()} - } else { - quote! {&mut self.#field_name} - }; - - let remove_ident = Ident::new(&format!("remove_{}", name), Span::call_site()); - - let remover = if f.needs_option { - quote! {self.#field_name = None;} - } else { - let assert_field_ty_default = quote_spanned! {f.name.span()=> - struct _AssertDefault where #field_ty: core::default::Default; - }; - - quote! { - #assert_field_ty_default - self.#field_name = <#field_ty>::default(); - } - }; - - let cfg = &f.cfg_features; - quote! { - #( #cfg )* - impl #struct_name { - /// Returns a reference to the tag - pub fn #name(&self) -> #ty_prefix &#field_ty #ty_suffix { - #ref_access - } - - /// Returns a mutable reference to the tag - pub fn #mut_ident(&mut self) -> #ty_prefix &mut #field_ty #ty_suffix { - #mut_access - } - - /// Removes the tag - pub fn #remove_ident(&mut self) { - #remover - } - } - } - }) -} diff --git a/lofty_attr/src/lofty_file.rs b/lofty_attr/src/lofty_file.rs new file mode 100644 index 00000000..a7b7c40a --- /dev/null +++ b/lofty_attr/src/lofty_file.rs @@ -0,0 +1,338 @@ +use crate::internal; +use crate::util::{self, bail}; + +use proc_macro2::{Ident, Span}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::spanned::Spanned; +use syn::{Attribute, DataStruct, DeriveInput, Type}; + +pub(crate) fn parse( + input: &DeriveInput, + data_struct: &DataStruct, + errors: &mut Vec, +) -> proc_macro2::TokenStream { + let impl_audiofile = should_impl_audiofile(&input.attrs); + + let read_fn = match util::get_attr("read_fn", &input.attrs) { + Some(rfn) => rfn, + _ if impl_audiofile => { + bail!( + errors, + input.ident.span(), + "Expected a #[read_fn] attribute" + ); + }, + _ => proc_macro2::TokenStream::new(), + }; + + let struct_name = input.ident.clone(); + + // TODO: This is not readable in the slightest + + let opt_file_type = internal::opt_internal_file_type(struct_name.to_string()); + + let has_internal_file_type = opt_file_type.is_some(); + let is_internal = input + .attrs + .iter() + .any(|attr| util::has_path_attr(attr, "internal_write_module_do_not_use_anywhere_else")); + + if opt_file_type.is_none() && is_internal { + // TODO: This is the best check we can do for now I think? + // Definitely needs some work when a better solution comes out. + bail!( + errors, + input.ident.span(), + "Attempted to use an internal attribute externally" + ); + } + + let mut id3v2_strippable = false; + let file_type = match opt_file_type { + Some((ft, id3v2_strip)) => { + id3v2_strippable = id3v2_strip; + ft + }, + _ => match util::get_attr("file_type", &input.attrs) { + Some(rfn) => rfn, + _ => { + bail!( + errors, + input.ident.span(), + "Expected a #[file_type] attribute" + ); + }, + }, + }; + + let (tag_fields, properties_field) = match get_fields(errors, data_struct) { + Some(fields) => fields, + None => return proc_macro2::TokenStream::new(), + }; + + if tag_fields.is_empty() { + errors.push(util::err(input.ident.span(), "Struct has no tag fields")); + } + + let assert_tag_impl_into = tag_fields.iter().enumerate().map(|(i, f)| { + let name = format_ident!("_AssertTagExt{}", i); + let field_ty = &f.ty; + quote_spanned! {field_ty.span()=> + struct #name where #field_ty: lofty::TagExt; + } + }); + + let tag_exists = tag_fields.iter().map(|f| { + let name = &f.name; + if f.needs_option { + quote! { self.#name.is_some() } + } else { + quote! { true } + } + }); + let tag_exists_2 = tag_exists.clone(); + + let tag_type = tag_fields.iter().map(|f| &f.tag_type); + + let properties_field = if let Some(field) = properties_field { + field + } else { + bail!(errors, input.ident.span(), "Struct has no properties field"); + }; + + let properties_field_ty = &properties_field.ty; + let assert_properties_impl = quote_spanned! {properties_field_ty.span()=> + struct _AssertIntoFileProperties where #properties_field_ty: std::convert::Into; + }; + + let audiofile_impl = if impl_audiofile { + quote! { + impl lofty::AudioFile for #struct_name { + type Properties = #properties_field_ty; + + fn read_from(reader: &mut R, parse_options: lofty::ParseOptions) -> lofty::error::Result + where + R: std::io::Read + std::io::Seek, + { + #read_fn(reader, parse_options) + } + + fn properties(&self) -> &Self::Properties { + &self.properties + } + + #[allow(unreachable_code)] + fn contains_tag(&self) -> bool { + #( #tag_exists )||* + } + + #[allow(unreachable_code, unused_variables)] + fn contains_tag_type(&self, tag_type: lofty::TagType) -> bool { + match tag_type { + #( lofty::TagType::#tag_type => { #tag_exists_2 } ),* + _ => false + } + } + } + } + } else { + proc_macro2::TokenStream::new() + }; + + let conditions = tag_fields.iter().map(|f| { + let name = &f.name; + if f.needs_option { + quote! { if let Some(t) = input.#name { tags.push(t.into()); } } + } else { + quote! { tags.push(input.#name.into()); } + } + }); + + let getters = get_getters(&tag_fields, &struct_name); + + let file_type_variant = if has_internal_file_type { + quote! { lofty::FileType::#file_type } + } else { + let file_ty_str = file_type.to_string(); + quote! { lofty::FileType::Custom(#file_ty_str) } + }; + + let mut ret = quote! { + #assert_properties_impl + + #( #assert_tag_impl_into )* + + #audiofile_impl + + impl std::convert::From<#struct_name> for lofty::TaggedFile { + fn from(input: #struct_name) -> Self { + lofty::TaggedFile::new( + #file_type_variant, + lofty::FileProperties::from(input.properties), + { + let mut tags: Vec = Vec::new(); + #( #conditions )* + + tags + } + ) + } + } + + #( #getters )* + }; + + // Create `write` module if internal + if is_internal { + let lookup = internal::init_write_lookup(id3v2_strippable); + let write_mod = internal::write_module(&tag_fields, lookup); + + ret = quote! { + #ret + + #write_mod + } + } + + ret +} + +pub(crate) struct FieldContents { + name: Ident, + pub(crate) cfg_features: Vec, + needs_option: bool, + getter_name: Option, + ty: Type, + pub(crate) tag_type: proc_macro2::TokenStream, +} + +fn get_fields<'a>( + errors: &mut Vec, + data: &'a DataStruct, +) -> Option<(Vec, Option<&'a syn::Field>)> { + let mut tag_fields = Vec::new(); + let mut properties_field = None; + + for field in &data.fields { + let name = field.ident.clone().unwrap(); + if name.to_string().ends_with("_tag") { + let tag_type = match util::get_attr("tag_type", &field.attrs) { + Some(tt) => tt, + _ => { + errors.push(util::err(field.span(), "Field has no `tag_type` attribute")); + return None; + }, + }; + + let cfg = field + .attrs + .iter() + .cloned() + .filter_map(|a| util::get_attr_list("cfg", &a).map(|_| a)) + .collect::>(); + + let option_unwrapped = util::extract_type_from_option(&field.ty); + // `option_unwrapped` will be `Some` if the type was wrapped in an `Option` + let needs_option = option_unwrapped.is_some(); + + let contents = FieldContents { + name, + getter_name: util::get_attr("getter", &field.attrs), + ty: option_unwrapped.unwrap_or_else(|| field.ty.clone()), + tag_type, + needs_option, + cfg_features: cfg, + }; + tag_fields.push(contents); + continue; + } + + if name == "properties" { + properties_field = Some(field); + } + } + + Some((tag_fields, properties_field)) +} + +fn should_impl_audiofile(attrs: &[Attribute]) -> bool { + for attr in attrs { + if util::has_path_attr(attr, "no_audiofile_impl") { + return false; + } + } + + true +} + +fn get_getters<'a>( + tag_fields: &'a [FieldContents], + struct_name: &'a Ident, +) -> impl Iterator + 'a { + tag_fields.iter().map(move |f| { + let name = f.getter_name.clone().unwrap_or_else(|| { + let name = f.name.to_string().strip_suffix("_tag").unwrap().to_string(); + + Ident::new(&name, f.name.span()).into_token_stream() + }); + + let (ty_prefix, ty_suffix) = if f.needs_option { + (quote! {Option<}, quote! {>}) + } else { + (quote! {}, quote! {}) + }; + + let field_name = &f.name; + let field_ty = &f.ty; + + let ref_access = if f.needs_option { + quote! {self.#field_name.as_ref()} + } else { + quote! {&self.#field_name} + }; + + let mut_ident = Ident::new(&format!("{}_mut", name), Span::call_site()); + + let mut_access = if f.needs_option { + quote! {self.#field_name.as_mut()} + } else { + quote! {&mut self.#field_name} + }; + + let remove_ident = Ident::new(&format!("remove_{}", name), Span::call_site()); + + let remover = if f.needs_option { + quote! {self.#field_name = None;} + } else { + let assert_field_ty_default = quote_spanned! {f.name.span()=> + struct _AssertDefault where #field_ty: core::default::Default; + }; + + quote! { + #assert_field_ty_default + self.#field_name = <#field_ty>::default(); + } + }; + + let cfg = &f.cfg_features; + quote! { + #( #cfg )* + impl #struct_name { + /// Returns a reference to the tag + pub fn #name(&self) -> #ty_prefix &#field_ty #ty_suffix { + #ref_access + } + + /// Returns a mutable reference to the tag + pub fn #mut_ident(&mut self) -> #ty_prefix &mut #field_ty #ty_suffix { + #mut_access + } + + /// Removes the tag + pub fn #remove_ident(&mut self) { + #remover + } + } + } + }) +} diff --git a/lofty_attr/src/lofty_tag.rs b/lofty_attr/src/lofty_tag.rs new file mode 100644 index 00000000..571e83fe --- /dev/null +++ b/lofty_attr/src/lofty_tag.rs @@ -0,0 +1,47 @@ +use crate::util::{self, bail}; + +use quote::quote; +use syn::{DataStruct, DeriveInput, Meta, NestedMeta}; + +pub(crate) fn parse( + input: &DeriveInput, + _data_struct: &DataStruct, + errors: &mut Vec, +) -> proc_macro2::TokenStream { + let mut supported_file_types_attr = None; + for attr in &input.attrs { + if let Some(list) = util::get_attr_list("lofty", attr) { + if let Some(NestedMeta::Meta(Meta::List(ml))) = list.nested.first() { + if ml + .path + .segments + .first() + .expect("path shouldn't be empty") + .ident == "supported_formats" + { + supported_file_types_attr = Some(ml.clone()); + } + } + } + } + + if supported_file_types_attr.is_none() { + bail!( + errors, + input.ident.span(), + "Tag has no #[lofty(supported_formats)] attribute" + ); + } + + let ident = &input.ident; + let supported_file_types_attr = supported_file_types_attr.unwrap(); + let file_types_iter = supported_file_types_attr.nested.iter(); + + quote! { + impl #ident { + pub(crate) const SUPPORTED_FORMATS: &'static [lofty::FileType] = &[ + #( lofty::FileType:: #file_types_iter ),* + ]; + } + } +} diff --git a/lofty_attr/src/util.rs b/lofty_attr/src/util.rs index 0bb97e2a..ac4e25fa 100644 --- a/lofty_attr/src/util.rs +++ b/lofty_attr/src/util.rs @@ -3,6 +3,15 @@ use std::fmt::Display; use proc_macro2::Span; use syn::{Attribute, Lit, Meta, MetaList, NestedMeta, Type}; +macro_rules! bail { + ($errors:ident, $span:expr, $msg:literal) => { + $errors.push(crate::util::err($span, $msg)); + return proc_macro2::TokenStream::new(); + }; +} + +pub(crate) use bail; + pub(crate) fn get_attr(name: &str, attrs: &[Attribute]) -> Option { for attr in attrs { if let Some(list) = get_attr_list("lofty", attr) { diff --git a/src/ape/tag/mod.rs b/src/ape/tag/mod.rs index 3ab8326e..592c3509 100644 --- a/src/ape/tag/mod.rs +++ b/src/ape/tag/mod.rs @@ -13,6 +13,8 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::Path; +use lofty_attr::LoftyTag; + macro_rules! impl_accessor { ($($name:ident => $($key:literal)|+;)+) => { paste::paste! { @@ -47,7 +49,6 @@ macro_rules! impl_accessor { } } -#[derive(Default, Debug, PartialEq, Eq, Clone)] /// An `APE` tag /// /// ## Supported file types @@ -71,6 +72,8 @@ macro_rules! impl_accessor { /// /// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded. /// For items, see [`ApeItem::new`]. +#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)] +#[lofty(supported_formats(APE, MPEG, WavPack))] pub struct ApeTag { /// Whether or not to mark the tag as read only pub read_only: bool, diff --git a/src/ape/tag/write.rs b/src/ape/tag/write.rs index c747bd8b..91ca2590 100644 --- a/src/ape/tag/write.rs +++ b/src/ape/tag/write.rs @@ -4,7 +4,6 @@ use super::ApeTagRef; use crate::ape::constants::APE_PREAMBLE; use crate::ape::header::read_ape_header; use crate::error::Result; -use crate::file::FileType; use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2}; use crate::macros::{decode_err, err}; use crate::probe::Probe; @@ -23,7 +22,7 @@ where let probe = Probe::new(data).guess_file_type()?; match probe.file_type() { - Some(FileType::APE | FileType::MPEG | FileType::WavPack) => {}, + Some(ft) if super::ApeTag::SUPPORTED_FORMATS.contains(&ft) => {}, _ => err!(UnsupportedTag), } diff --git a/src/id3/v1/tag.rs b/src/id3/v1/tag.rs index 8e6cb2f7..31d46411 100644 --- a/src/id3/v1/tag.rs +++ b/src/id3/v1/tag.rs @@ -8,6 +8,8 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::Path; +use lofty_attr::LoftyTag; + macro_rules! impl_accessor { ($($name:ident,)+) => { paste::paste! { @@ -52,7 +54,8 @@ macro_rules! impl_accessor { /// /// * [`GENRES`] contains the string /// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8` -#[derive(Default, Debug, PartialEq, Eq, Clone)] +#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)] +#[lofty(supported_formats(APE, MPEG, WavPack))] pub struct ID3v1Tag { /// Track title, 30 bytes max pub title: Option, diff --git a/src/id3/v1/write.rs b/src/id3/v1/write.rs index fc76cf2d..ff58c0db 100644 --- a/src/id3/v1/write.rs +++ b/src/id3/v1/write.rs @@ -1,6 +1,5 @@ use super::tag::Id3v1TagRef; use crate::error::Result; -use crate::file::FileType; use crate::id3::{find_id3v1, ID3FindResults}; use crate::macros::err; use crate::probe::Probe; @@ -15,7 +14,7 @@ pub(crate) fn write_id3v1(file: &mut File, tag: &Id3v1TagRef<'_>) -> Result<()> let probe = Probe::new(file).guess_file_type()?; match probe.file_type() { - Some(FileType::APE | FileType::MPEG | FileType::WavPack) => {}, + Some(ft) if super::ID3v1Tag::SUPPORTED_FORMATS.contains(&ft) => {}, _ => err!(UnsupportedTag), } diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index 4d568454..7bd8bc73 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -18,6 +18,8 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::Path; +use lofty_attr::LoftyTag; + macro_rules! impl_accessor { ($($name:ident => $id:literal;)+) => { paste::paste! { @@ -59,7 +61,6 @@ macro_rules! impl_accessor { } } -#[derive(PartialEq, Eq, Debug, Clone)] /// An `ID3v2` tag /// /// ## Supported file types @@ -97,6 +98,8 @@ macro_rules! impl_accessor { /// and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively, and converted back to binary with /// [`GeneralEncapsulatedObject::as_bytes`](crate::id3::v2::GeneralEncapsulatedObject::as_bytes) and /// [`SynchronizedText::as_bytes`](crate::id3::v2::SynchronizedText::as_bytes) for writing. +#[derive(LoftyTag, PartialEq, Eq, Debug, Clone)] +#[lofty(supported_formats(APE, AIFF, FLAC, MPEG, WAV))] pub struct ID3v2Tag { flags: ID3v2TagFlags, pub(super) original_version: ID3v2Version, diff --git a/src/id3/v2/write/mod.rs b/src/id3/v2/write/mod.rs index c302fdf3..e44f2d60 100644 --- a/src/id3/v2/write/mod.rs +++ b/src/id3/v2/write/mod.rs @@ -15,6 +15,7 @@ use std::fs::File; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::ops::Not; +use crate::id3::v2::ID3v2Tag; use byteorder::{BigEndian, LittleEndian, WriteBytesExt}; // In the very rare chance someone wants to write a CRC in their extended header @@ -41,8 +42,11 @@ pub(crate) fn write_id3v2<'a, I: Iterator> + 'a>( let data = probe.into_inner(); + if file_type.is_none() || !ID3v2Tag::SUPPORTED_FORMATS.contains(&(file_type.unwrap())) { + err!(UnsupportedTag); + } + match file_type { - Some(FileType::APE | FileType::MPEG | FileType::FLAC) => {}, // Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file Some(FileType::WAV) => { tag.flags.footer = false; @@ -52,7 +56,7 @@ pub(crate) fn write_id3v2<'a, I: Iterator> + 'a>( tag.flags.footer = false; return chunk_file::write_to_chunk_file::(data, &create_tag(tag)?); }, - _ => err!(UnsupportedTag), + _ => {}, } let id3v2 = create_tag(tag)?; diff --git a/src/iff/aiff/tag.rs b/src/iff/aiff/tag.rs index 916b0a50..e50a80de 100644 --- a/src/iff/aiff/tag.rs +++ b/src/iff/aiff/tag.rs @@ -11,6 +11,7 @@ use std::io::{Read, Seek, SeekFrom, Write}; use std::path::Path; use byteorder::BigEndian; +use lofty_attr::LoftyTag; /// Represents an AIFF `COMT` chunk /// @@ -60,7 +61,8 @@ pub struct Comment { /// * [`ItemKey::Comment`](crate::ItemKey::Comment) /// /// When converting [Comment]s, only the `text` field will be preserved. -#[derive(Default, Clone, Debug, PartialEq, Eq)] +#[derive(LoftyTag, Default, Clone, Debug, PartialEq, Eq)] +#[lofty(supported_formats(AIFF))] pub struct AIFFTextChunks { /// The name of the piece pub name: Option, diff --git a/src/iff/wav/tag/mod.rs b/src/iff/wav/tag/mod.rs index 99ced4b3..7e1e7ebc 100644 --- a/src/iff/wav/tag/mod.rs +++ b/src/iff/wav/tag/mod.rs @@ -10,6 +10,8 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::Path; +use lofty_attr::LoftyTag; + macro_rules! impl_accessor { ($($name:ident => $key:literal;)+) => { paste::paste! { @@ -30,7 +32,6 @@ macro_rules! impl_accessor { } } -#[derive(Default, Debug, PartialEq, Eq, Clone)] /// A RIFF INFO LIST /// /// ## Supported file types @@ -45,6 +46,8 @@ macro_rules! impl_accessor { /// /// * The [`TagItem`] has a value other than [`ItemValue::Binary`](crate::ItemValue::Binary) /// * It has a key that is 4 bytes in length and within the ASCII range +#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)] +#[lofty(supported_formats(WAV))] pub struct RIFFInfoList { /// A collection of chunk-value pairs pub(crate) items: Vec<(String, String)>, diff --git a/src/mp4/ilst/mod.rs b/src/mp4/ilst/mod.rs index 6e9f6254..686bb178 100644 --- a/src/mp4/ilst/mod.rs +++ b/src/mp4/ilst/mod.rs @@ -18,6 +18,8 @@ use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::Path; +use lofty_attr::LoftyTag; + const ARTIST: AtomIdent = AtomIdent::Fourcc(*b"\xa9ART"); const TITLE: AtomIdent = AtomIdent::Fourcc(*b"\xa9nam"); const ALBUM: AtomIdent = AtomIdent::Fourcc(*b"\xa9alb"); @@ -53,7 +55,6 @@ macro_rules! impl_accessor { } } -#[derive(Default, PartialEq, Debug, Clone)] /// An MP4 ilst atom /// /// ## Supported file types @@ -80,6 +81,8 @@ macro_rules! impl_accessor { /// well as pictures, will be preserved. /// /// An attempt will be made to create the `TrackNumber/TrackTotal` (trkn) and `DiscNumber/DiscTotal` (disk) pairs. +#[derive(LoftyTag, Default, PartialEq, Debug, Clone)] +#[lofty(supported_formats(MP4))] pub struct Ilst { pub(crate) atoms: Vec, } diff --git a/src/ogg/tag.rs b/src/ogg/tag.rs index 3d9ffb76..248bb07f 100644 --- a/src/ogg/tag.rs +++ b/src/ogg/tag.rs @@ -13,6 +13,8 @@ use std::fs::{File, OpenOptions}; use std::io::{Cursor, Write}; use std::path::Path; +use lofty_attr::LoftyTag; + macro_rules! impl_accessor { ($($name:ident => $key:literal;)+) => { paste::paste! { @@ -41,7 +43,8 @@ macro_rules! impl_accessor { /// * [`FileType::Opus`](crate::FileType::Opus) /// * [`FileType::Speex`](crate::FileType::Speex) /// * [`FileType::Vorbis`](crate::FileType::Vorbis) -#[derive(Default, PartialEq, Eq, Debug, Clone)] +#[derive(LoftyTag, Default, PartialEq, Eq, Debug, Clone)] +#[lofty(supported_formats(FLAC, Opus, Speex, Vorbis))] pub struct VorbisComments { /// An identifier for the encoding software pub(crate) vendor: String,