/* This file is part of jellything (https://codeberg.org/metamuffin/jellything) which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2024 metamuffin */ use proc_macro::{token_stream, Delimiter, Span, TokenStream, TokenTree}; use quote::quote; use syn::{Fields, FieldsUnnamed, Ident, Variant}; struct Tag { id: u64, global: bool, path: Vec, name: Ident, r#type: Option, // None -> Master } #[proc_macro] pub fn define_ebml(ts: TokenStream) -> TokenStream { let mut ts = ts.into_iter(); let mut tags = vec![]; parse_kt(&mut tags, &mut ts, vec![]); let enum_variants = tags .iter() .map(|e| Variant { ident: e.name.clone(), attrs: vec![], fields: Fields::Unnamed( syn::parse2::(match e.r#type.clone() { None => quote!((Master)), Some(r#type) => match r#type.as_str() { "Int" => quote!((i64)), "Uint" => quote!((u64)), "Float" => quote!((f64)), "Utf8" => quote!((String)), "Binary" => quote!((Vec)), "Block" => quote!((Block)), _ => panic!("unsupported type {type}"), }, }) .expect("parse type"), ), discriminant: None, }) .collect::>(); let path_match = tags .iter() .map(|e| { let name = &e.name; let path = e.path.clone(); if e.global { quote! { Self::#name(_) => None } } else { quote! { Self::#name(_) => Some(&[#(#path),*]) } } }) .collect::>(); let id_match = tags .iter() .map(|Tag { id, name, .. }| { quote! { Self::#name(_) => #id } }) .collect::>(); let parse_match = tags .iter() .filter_map( |Tag { id, name, r#type, .. }| { if r#type.is_some() { Some(quote! { #id => Self::#name(crate::ReadValue::from_buf(data)?) }) } else { None } }, ) .collect::>(); let write_match = tags .iter() .map(|Tag { name, .. }| quote! { Self::#name(v) => v.write_to(w) }) .collect::>(); let size_match = tags .iter() .map(|Tag { name, .. }| quote! { Self::#name(v) => v.size() }) .collect::>(); let cons_master_match = tags .iter() .filter_map( |Tag { id, name, r#type, .. }| { if r#type.is_none() { Some(quote! { #id => Self::#name(kind) }) } else { None } }, ) .collect::>(); let is_master_match = tags .iter() .map(|Tag { id, r#type, .. }| match r#type { None => quote!(#id => true), Some(_) => quote!(#id => false), }) .collect::>(); quote! { use crate::Master; use crate::WriteValue; use crate::Block; #[derive(Debug, PartialEq, Clone)] pub enum MatroskaTag { #(#enum_variants),* } impl MatroskaTag { /// returns path in order top-to-immediate-parent order or None if global. pub fn path(&self) -> Option<&'static [u64]> { match self { #(#path_match),* } } pub fn id(&self) -> u64 { match self { #(#id_match),* } } pub fn is_master(id: u64) -> crate::Result { Ok(match id { #(#is_master_match),*, _ => return Err(crate::Error::UnknownID) }) } pub fn construct_master(id: u64, kind: Master) -> crate::Result { Ok(match id { #(#cons_master_match),*, _ => return Err(crate::Error::UnknownID) }) } pub fn parse(id: u64, data: &[u8]) -> crate::Result { Ok(match id { #(#parse_match),*, _ => return Err(crate::Error::UnknownID) }) } pub fn write(&self, w: &mut impl std::io::Write) -> crate::Result<()> { match self { #(#write_match),* } } pub fn size(&self) -> usize { match self { #(#size_match),* } } } } .into() } fn parse_kt(tags: &mut Vec, ts: &mut token_stream::IntoIter, path: Vec) { let mut next_glob = false; loop { let global = next_glob; next_glob = false; let name = if let Some(tt) = ts.next() { if let TokenTree::Ident(name) = tt { if &name.to_string() == "global" { next_glob = true; continue; } name.to_string() } else { panic!("expected ident") } } else { break; }; let id = if let Some(TokenTree::Group(gr)) = ts.next() { assert_eq!(gr.delimiter(), Delimiter::Bracket); let mut ts = gr.stream().into_iter(); if let TokenTree::Literal(lit) = ts.next().unwrap() { u64::from_str_radix(&lit.to_string()[2..], 16).unwrap() } else { panic!("literal expected") } } else { panic!("group expected") }; if let Some(TokenTree::Punct(p)) = ts.next() { assert_eq!(p.as_char(), ':') } else { panic!("colon expected") } match ts.next() { Some(TokenTree::Group(gr)) => { // eprintln!("entering group"); let mut ts = gr.stream().into_iter(); tags.push(Tag { global, id, name: Ident::new(&name, Span::call_site().into()), path: path.clone(), r#type: None, }); let mut path = path.clone(); path.push(id); parse_kt(tags, &mut ts, path); // eprintln!("leaving group"); } Some(TokenTree::Ident(r#type)) => { let r#type = r#type.to_string(); // eprintln!("global={global} id={id}, type={}", r#type); tags.push(Tag { id, name: Ident::new(&name, Span::call_site().into()), path: path.clone(), global, r#type: Some(r#type), }) } _ => panic!("group or ident expected"), } if let Some(TokenTree::Punct(p)) = ts.next() { assert_eq!(p.as_char(), ',') } else { panic!("colon expected") } } }