use crate::attribute::AttributeValue;
use crate::{internal, util};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::parse::Parse;
use syn::spanned::Spanned;
use syn::{Attribute, Data, DataStruct, DeriveInput, Field, Fields, Type};
#[derive(Default)]
pub struct InternalFileDetails {
pub(crate) has_internal_write_module: bool,
pub(crate) has_internal_file_type: bool,
pub(crate) id3v2_strippable: bool,
}
#[derive(Default)]
pub(crate) struct FileFields {
pub(crate) tags: Vec<FieldContents>,
pub(crate) properties: Option<Field>,
}
pub struct FileStructInfo {
pub(crate) name: Ident,
pub(crate) span: Span,
pub(crate) fields: FileFields,
}
pub(crate) struct AudioFileImplFields {
pub(crate) should_impl_audiofile: bool,
pub(crate) read_fn: Option<proc_macro2::TokenStream>,
pub(crate) write_fn: Option<proc_macro2::TokenStream>,
}
impl Default for AudioFileImplFields {
fn default() -> Self {
Self {
should_impl_audiofile: true,
read_fn: None,
write_fn: None,
}
}
}
pub struct LoftyFile {
pub(crate) struct_info: FileStructInfo,
pub(crate) audiofile_impl: AudioFileImplFields,
pub(crate) internal_details: InternalFileDetails,
pub(crate) file_type: proc_macro2::TokenStream,
pub(crate) should_impl_into_taggedfile: bool,
}
impl Parse for LoftyFile {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
let input: DeriveInput = input.parse()?;
let data_struct = match input.data {
Data::Struct(
ref data_struct @ DataStruct {
fields: Fields::Named(_),
..
},
) => data_struct,
_ => {
return Err(util::err(
input.ident.span(),
"This macro can only be used on structs with named fields",
))
},
};
let mut lofty_file = LoftyFile {
struct_info: FileStructInfo {
name: input.ident.clone(),
span: input.ident.span(),
fields: FileFields::default(),
},
audiofile_impl: AudioFileImplFields::default(),
should_impl_into_taggedfile: true,
file_type: proc_macro2::TokenStream::new(),
internal_details: InternalFileDetails::default(),
};
let mut errors = Vec::new();
let mut has_internal_write_module = false;
for attr in &input.attrs {
if let Some(lofty_attr) = AttributeValue::from_attribute("lofty", attr)? {
match lofty_attr {
AttributeValue::Path(value) => match &*value.to_string() {
"no_audiofile_impl" => {
lofty_file.audiofile_impl.should_impl_audiofile = false
},
"no_into_taggedfile_impl" => lofty_file.should_impl_into_taggedfile = false,
"internal_write_module_do_not_use_anywhere_else" => {
has_internal_write_module = true
},
_ => errors.push(util::err(attr.span(), "Unknown attribute")),
},
AttributeValue::NameValue(lhs, rhs) => match &*lhs.to_string() {
"read_fn" => lofty_file.audiofile_impl.read_fn = Some(rhs.parse()?),
"write_fn" => lofty_file.audiofile_impl.write_fn = Some(rhs.parse()?),
"file_type" => lofty_file.file_type = rhs.parse()?,
_ => errors.push(util::err(attr.span(), "Unknown attribute")),
},
_ => errors.push(util::err(attr.span(), "Unknown attribute")),
}
}
}
let struct_name = input.ident.clone();
let opt_file_type = internal::opt_internal_file_type(struct_name.to_string());
let has_internal_file_type = opt_file_type.is_some();
if !has_internal_file_type && has_internal_write_module {
return Err(crate::util::err(
input.ident.span(),
"Attempted to use an internal attribute externally",
));
}
lofty_file.internal_details.has_internal_write_module = has_internal_write_module;
lofty_file.internal_details.has_internal_file_type = has_internal_file_type;
if lofty_file.file_type.is_empty() && lofty_file.internal_details.has_internal_file_type {
let Some((ft, id3v2_strip)) = opt_file_type else {
return Err(util::err(
input.ident.span(),
"Unable to locate file type for internal file",
));
};
lofty_file.internal_details.id3v2_strippable = id3v2_strip;
lofty_file.file_type = ft;
}
let (tag_fields, properties_field) = match get_fields(&mut errors, data_struct)? {
Some(fields) => fields,
None => return Err(errors.remove(0)),
};
if tag_fields.is_empty() {
errors.push(util::err(input.ident.span(), "Struct has no tag fields"));
}
lofty_file.struct_info.fields.tags = tag_fields;
lofty_file.struct_info.fields.properties = properties_field.cloned();
Ok(lofty_file)
}
}
impl LoftyFile {
pub(crate) fn emit(self) -> syn::Result<TokenStream> {
let mut audiofile_impl = proc_macro2::TokenStream::new();
if self.audiofile_impl.should_impl_audiofile {
audiofile_impl = generate_audiofile_impl(&self)?;
}
let assert_tag_impl_into = self
.struct_info
.fields
.tags
.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::prelude::TagExt;
}
});
let mut from_taggedfile_impl = proc_macro2::TokenStream::new();
if self.should_impl_into_taggedfile {
from_taggedfile_impl = generate_from_taggedfile_impl(&self);
}
let getters = get_getters(&self.struct_info.fields.tags, &self.struct_info.name);
let mut ret = quote! {
#( #assert_tag_impl_into )*
#audiofile_impl
#from_taggedfile_impl
#( #getters )*
};
if self.internal_details.has_internal_write_module {
let lookup = internal::init_write_lookup(self.internal_details.id3v2_strippable);
let write_mod = internal::write_module(&self.struct_info.fields.tags, lookup);
ret = quote! {
#ret
use crate::_this_is_internal;
#write_mod
}
}
Ok(TokenStream::from(ret))
}
}
pub(crate) struct FieldContents {
name: Ident,
pub(crate) attrs: Vec<Attribute>,
needs_option: bool,
getter_name: Option<proc_macro2::TokenStream>,
ty: Type,
pub(crate) tag_type: proc_macro2::TokenStream,
}
impl FieldContents {
pub(crate) fn get_cfg_features(&self) -> impl Iterator<Item = &Attribute> {
self.attrs.iter().filter(|a| a.path().is_ident("cfg"))
}
}
fn get_fields<'a>(
errors: &mut Vec<syn::Error>,
data: &'a DataStruct,
) -> syn::Result<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 == "properties" {
properties_field = Some(field);
}
if !name.to_string().ends_with("_tag") {
continue;
}
let mut tag_type = None;
let mut getter_name = None;
for attr in &field.attrs {
if let Some(lofty_attr) = AttributeValue::from_attribute("lofty", attr)? {
match lofty_attr {
AttributeValue::NameValue(lhs, rhs) => match &*lhs.to_string() {
"tag_type" => tag_type = Some(rhs.parse::<proc_macro2::TokenStream>()?),
"getter" => getter_name = Some(rhs.parse::<proc_macro2::TokenStream>()?),
_ => errors.push(util::err(attr.span(), "Unknown attribute")),
},
_ => errors.push(util::err(attr.span(), "Unknown attribute")),
}
}
}
let Some(tag_type) = tag_type else {
errors.push(util::err(
field.ident.span(),
"Expected a `#[lofty(tag_type = \"...\")]` attribute",
));
return Ok(None);
};
let other_attrs = field
.attrs
.iter()
.filter(|a| !a.path().is_ident("lofty"))
.cloned()
.collect::<Vec<_>>();
let option_unwrapped = util::extract_type_from_option(&field.ty);
let needs_option = option_unwrapped.is_some();
let contents = FieldContents {
name,
attrs: other_attrs,
getter_name,
ty: option_unwrapped.unwrap_or_else(|| field.ty.clone()),
tag_type,
needs_option,
};
tag_fields.push(contents);
}
Ok(Some((tag_fields, properties_field)))
}
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 set_ident = Ident::new(&format!("set_{}", name), Span::call_site());
let setter = if f.needs_option {
quote! {
let ret = self.#field_name.take();
self.#field_name = Some(tag);
return ret;
}
} else {
quote! {
Some(::core::mem::replace(&mut self.#field_name, tag))
}
};
let remove_ident = Ident::new(&format!("remove_{}", name), Span::call_site());
let remover = if f.needs_option {
quote! { self.#field_name.take() }
} else {
let assert_field_ty_default = quote_spanned! {f.name.span()=>
struct _AssertDefault where #field_ty: core::default::Default;
};
quote! {
#assert_field_ty_default
::core::mem::take(&mut self.#field_name)
}
};
let cfg_features = f.get_cfg_features();
quote! {
#( #cfg_features )*
impl #struct_name {
pub fn #name(&self) -> #ty_prefix &#field_ty #ty_suffix {
#ref_access
}
pub fn #mut_ident(&mut self) -> #ty_prefix &mut #field_ty #ty_suffix {
#mut_access
}
pub fn #set_ident(&mut self, tag: #field_ty) -> Option<#field_ty> {
#setter
}
pub fn #remove_ident(&mut self) -> #ty_prefix #field_ty #ty_suffix {
#remover
}
}
}
})
}
fn generate_audiofile_impl(file: &LoftyFile) -> syn::Result<proc_macro2::TokenStream> {
fn tag_exists_iter(
tag_fields: &[FieldContents],
) -> impl Iterator<Item = proc_macro2::TokenStream> + '_ {
tag_fields.iter().map(|f| {
let name = &f.name;
if f.needs_option {
quote! { self.#name.is_some() }
} else {
quote! { true }
}
})
}
let Some(properties_field) = &file.struct_info.fields.properties else {
return Err(util::err(
file.struct_info.span,
"Struct has no `properties` field, required for `AudioFile` impl",
));
};
let Some(read_fn) = &file.audiofile_impl.read_fn else {
return Err(util::err(
file.struct_info.span,
"Expected a `#[read_fn]` attribute",
));
};
let tag_fields = &file.struct_info.fields.tags;
let save_to_body = get_save_to_body(file.audiofile_impl.write_fn.as_ref(), tag_fields);
let tag_exists = tag_exists_iter(tag_fields);
let tag_exists_2 = tag_exists_iter(tag_fields);
let tag_type = tag_fields.iter().map(|f| &f.tag_type);
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::properties::FileProperties>;
};
let struct_name = &file.struct_info.name;
let ret = quote! {
#assert_properties_impl
impl ::lofty::prelude::AudioFile for #struct_name {
type Properties = #properties_field_ty;
fn read_from<R>(reader: &mut R, parse_options: ::lofty::config::ParseOptions) -> ::lofty::error::Result<Self>
where
R: std::io::Read + std::io::Seek,
{
#read_fn(reader, parse_options)
}
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
}
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::tag::TagType) -> bool {
match tag_type {
#( ::lofty::tag::TagType::#tag_type => { #tag_exists_2 } ),*
_ => false
}
}
}
};
Ok(ret)
}
fn get_save_to_body(
write_fn: Option<&proc_macro2::TokenStream>,
tag_fields: &[FieldContents],
) -> proc_macro2::TokenStream {
if let Some(write_fn) = write_fn {
return quote! {
#write_fn(&self, file, write_options)
};
}
let tag_field_save = tag_fields.iter().map(|f| {
let name = &f.name;
if f.needs_option {
quote! {
if let Some(ref tag) = self.#name {
file.rewind()?;
tag.save_to(file, write_options)?;
}
}
} else {
quote! {
file.rewind()?;
self.#name.save_to(file, write_options)?;
}
}
});
quote! {
#(#tag_field_save)*
Ok(())
}
}
fn generate_from_taggedfile_impl(file: &LoftyFile) -> proc_macro2::TokenStream {
let tag_fields = &file.struct_info.fields.tags;
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 file_type = &file.file_type;
let file_type_variant = if file.internal_details.has_internal_file_type {
quote! { ::lofty::file::FileType::#file_type }
} else {
let file_ty_str = file_type.to_string();
quote! { ::lofty::file::FileType::Custom(#file_ty_str) }
};
let struct_name = &file.struct_info.name;
quote! {
impl ::std::convert::From<#struct_name> for ::lofty::file::TaggedFile {
fn from(input: #struct_name) -> Self {
use ::lofty::prelude::TaggedFileExt as _;
::lofty::file::TaggedFile::new(
#file_type_variant,
::lofty::properties::FileProperties::from(input.properties),
{
let mut tags: Vec<::lofty::tag::Tag> = Vec::new();
#( #conditions )*
tags
}
)
}
}
}
}