mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-11 21:22:39 +00:00
lofty_attr: Add supported_formats attribute for tags
This commit is contained in:
parent
0b6ad1599b
commit
3a4b4f243e
15 changed files with 466 additions and 370 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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<syn::Error>) -> 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<syn::Error>) -> 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<lofty::FileProperties>;
|
||||
};
|
||||
|
||||
let audiofile_impl = if impl_audiofile {
|
||||
quote! {
|
||||
impl lofty::AudioFile for #struct_name {
|
||||
type Properties = #properties_field_ty;
|
||||
|
||||
fn read_from<R>(reader: &mut R, parse_options: lofty::ParseOptions) -> lofty::error::Result<Self>
|
||||
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<lofty::Tag> = 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<Attribute>,
|
||||
needs_option: bool,
|
||||
getter_name: Option<proc_macro2::TokenStream>,
|
||||
ty: Type,
|
||||
tag_type: proc_macro2::TokenStream,
|
||||
}
|
||||
|
||||
fn get_fields<'a>(
|
||||
errors: &mut Vec<syn::Error>,
|
||||
data: &'a DataStruct,
|
||||
) -> Option<(Vec<FieldContents>, 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::<Vec<_>>();
|
||||
|
||||
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<Item = proc_macro2::TokenStream> + '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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
338
lofty_attr/src/lofty_file.rs
Normal file
338
lofty_attr/src/lofty_file.rs
Normal file
|
@ -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<syn::Error>,
|
||||
) -> 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<lofty::FileProperties>;
|
||||
};
|
||||
|
||||
let audiofile_impl = if impl_audiofile {
|
||||
quote! {
|
||||
impl lofty::AudioFile for #struct_name {
|
||||
type Properties = #properties_field_ty;
|
||||
|
||||
fn read_from<R>(reader: &mut R, parse_options: lofty::ParseOptions) -> lofty::error::Result<Self>
|
||||
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<lofty::Tag> = 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<Attribute>,
|
||||
needs_option: bool,
|
||||
getter_name: Option<proc_macro2::TokenStream>,
|
||||
ty: Type,
|
||||
pub(crate) tag_type: proc_macro2::TokenStream,
|
||||
}
|
||||
|
||||
fn get_fields<'a>(
|
||||
errors: &mut Vec<syn::Error>,
|
||||
data: &'a DataStruct,
|
||||
) -> Option<(Vec<FieldContents>, 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::<Vec<_>>();
|
||||
|
||||
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<Item = proc_macro2::TokenStream> + '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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
47
lofty_attr/src/lofty_tag.rs
Normal file
47
lofty_attr/src/lofty_tag.rs
Normal file
|
@ -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<syn::Error>,
|
||||
) -> 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 ),*
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<proc_macro2::TokenStream> {
|
||||
for attr in attrs {
|
||||
if let Some(list) = get_attr_list("lofty", attr) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String>,
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<Item = FrameRef<'a>> + '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<Item = FrameRef<'a>> + 'a>(
|
|||
tag.flags.footer = false;
|
||||
return chunk_file::write_to_chunk_file::<BigEndian>(data, &create_tag(tag)?);
|
||||
},
|
||||
_ => err!(UnsupportedTag),
|
||||
_ => {},
|
||||
}
|
||||
|
||||
let id3v2 = create_tag(tag)?;
|
||||
|
|
|
@ -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<String>,
|
||||
|
|
|
@ -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)>,
|
||||
|
|
|
@ -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<Atom>,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue