#![deny(missing_docs)]
#![forbid(unsafe_code)]
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;
use syn::{
Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericParam, Generics,
Ident, Index, Lifetime, LifetimeParam, parse_macro_input, parse_quote,
};
#[proc_macro_derive(Serialize, attributes(pack_io))]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_serialize(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(Deserialize, attributes(pack_io))]
pub fn derive_deserialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_deserialize(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_derive(DeserializeView, attributes(pack_io))]
pub fn derive_deserialize_view(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand_deserialize_view(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn expand_serialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let generics = add_trait_bound(&input.generics, parse_quote!(::pack_io::Serialize));
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let type_version = parse_type_attrs(&input.attrs)?;
let body = match &input.data {
Data::Struct(data) => serialize_struct_body(name, data, type_version)?,
Data::Enum(data) => {
if type_version.is_some() {
return Err(syn::Error::new_spanned(
name,
"pack-io: `#[pack_io(version = N)]` is not supported on enums in this \
release; wrap the enum in a versioned newtype struct instead",
));
}
serialize_enum_body(name, data)?
}
Data::Union(u) => {
return Err(syn::Error::new(
u.union_token.span(),
"pack-io: `union` is not supported (no defined wire format)",
));
}
};
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::pack_io::Serialize for #name #ty_generics #where_clause {
#[inline]
fn serialize<__E: ::pack_io::Encode + ?Sized>(
&self,
__encoder: &mut __E,
) -> ::pack_io::Result<()> {
#body
::core::result::Result::Ok(())
}
}
})
}
fn serialize_struct_body(
name: &Ident,
data: &DataStruct,
type_version: Option<u32>,
) -> syn::Result<TokenStream2> {
let fields_meta = collect_field_meta(&data.fields, quote!(self))?;
if type_version.is_none() {
for fm in &fields_meta {
if fm.attrs.since.is_some() || fm.attrs.deprecated.is_some() {
return Err(syn::Error::new_spanned(
name,
"pack-io: `#[pack_io(since = N)]` / `#[pack_io(deprecated = N)]` requires \
the struct itself to carry `#[pack_io(version = N)]`",
));
}
}
}
match type_version {
None => {
let writes = fields_meta.iter().map(|fm| {
let acc = &fm.accessor;
quote! { ::pack_io::Serialize::serialize(&#acc, __encoder)?; }
});
Ok(quote! { #(#writes)* })
}
Some(version) => {
let writes = fields_meta
.iter()
.filter(|fm| fm.attrs.live_at(version))
.map(|fm| {
let acc = &fm.accessor;
quote! { ::pack_io::Serialize::serialize(&#acc, &mut __body)?; }
});
Ok(quote! {
__encoder.write_varint_u64(#version as u64)?;
let mut __body = ::pack_io::Encoder::new();
#(#writes)*
let __body_bytes = ::pack_io::Encoder::into_inner(__body);
__encoder.write_varint_u64(__body_bytes.len() as u64)?;
__encoder.write_bytes(&__body_bytes)?;
})
}
}
}
fn serialize_enum_body(name: &Ident, data: &DataEnum) -> syn::Result<TokenStream2> {
if data.variants.is_empty() {
return Err(syn::Error::new_spanned(
name,
"pack-io: empty enums cannot be serialised (no value to encode)",
));
}
let arms: Vec<TokenStream2> = data
.variants
.iter()
.enumerate()
.map(|(index, variant)| {
let index = u32::try_from(index).expect("u32 enum variants");
let var_name = &variant.ident;
let bindings = variant_bindings(&variant.fields);
let pattern = match &variant.fields {
Fields::Named(_) => quote!(Self::#var_name { #(#bindings),* }),
Fields::Unnamed(_) => quote!(Self::#var_name(#(#bindings),*)),
Fields::Unit => quote!(Self::#var_name),
};
let writes = bindings.iter().map(|b| {
quote! { ::pack_io::Serialize::serialize(#b, __encoder)?; }
});
quote! {
#pattern => {
__encoder.write_varint_u64(#index as u64)?;
#(#writes)*
}
}
})
.collect();
Ok(quote! {
match self {
#(#arms)*
}
})
}
fn expand_deserialize(input: &DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let generics = add_trait_bound(&input.generics, parse_quote!(::pack_io::Deserialize));
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let type_version = parse_type_attrs(&input.attrs)?;
let body = match &input.data {
Data::Struct(data) => deserialize_struct_body(name, &data.fields, type_version)?,
Data::Enum(data) => {
if type_version.is_some() {
return Err(syn::Error::new_spanned(
name,
"pack-io: `#[pack_io(version = N)]` is not supported on enums in this \
release; wrap the enum in a versioned newtype struct instead",
));
}
deserialize_enum_body(name, data)?
}
Data::Union(u) => {
return Err(syn::Error::new(
u.union_token.span(),
"pack-io: `union` is not supported",
));
}
};
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::pack_io::Deserialize for #name #ty_generics #where_clause {
#[inline]
fn deserialize<__D: ::pack_io::Decode + ?Sized>(
__decoder: &mut __D,
) -> ::pack_io::Result<Self> {
#body
}
}
})
}
fn deserialize_struct_body(
name: &Ident,
fields: &Fields,
type_version: Option<u32>,
) -> syn::Result<TokenStream2> {
let fields_meta = collect_field_meta(fields, quote!(self))?;
if type_version.is_none() {
for fm in &fields_meta {
if fm.attrs.since.is_some() || fm.attrs.deprecated.is_some() {
return Err(syn::Error::new_spanned(
name,
"pack-io: `#[pack_io(since = N)]` / `#[pack_io(deprecated = N)]` requires \
the struct itself to carry `#[pack_io(version = N)]`",
));
}
}
}
match type_version {
None => {
let constructor = construct_from_fields(quote!(#name), fields, |ty| {
quote_spanned! { ty.span() =>
<#ty as ::pack_io::Deserialize>::deserialize(__decoder)?
}
});
Ok(quote! { ::core::result::Result::Ok(#constructor) })
}
Some(_) => {
let field_inits: Vec<TokenStream2> = fields_meta
.iter()
.enumerate()
.map(|(i, fm)| {
let var = field_local_ident(fm, i);
let ty = &fm.ty;
let always_live =
fm.attrs.since.unwrap_or(1) == 1 && fm.attrs.deprecated.is_none();
if always_live {
quote! {
let #var: #ty =
<#ty as ::pack_io::Deserialize>::deserialize(&mut __body_dec)?;
}
} else {
let since = fm.attrs.since.unwrap_or(1);
let deprecated_check = match fm.attrs.deprecated {
Some(d) => quote! { __version < (#d as u32) },
None => quote! { true },
};
quote! {
let #var: #ty = if (#since as u32) <= __version
&& #deprecated_check
{
<#ty as ::pack_io::Deserialize>::deserialize(&mut __body_dec)?
} else {
::core::default::Default::default()
};
}
}
})
.collect();
let constructor = match fields {
Fields::Named(_) => {
let pairs = fields_meta.iter().enumerate().map(|(i, fm)| {
let name = fm.field_ident.as_ref().expect("named");
let var = field_local_ident(fm, i);
quote! { #name: #var }
});
quote! { #name { #(#pairs),* } }
}
Fields::Unnamed(_) => {
let positions = fields_meta
.iter()
.enumerate()
.map(|(i, fm)| field_local_ident(fm, i));
quote! { #name ( #(#positions),* ) }
}
Fields::Unit => quote! { #name },
};
Ok(quote! {
let __version_u64 = __decoder.read_varint_u64()?;
let __version = u32::try_from(__version_u64)
.map_err(|_| ::pack_io::SerialError::IntegerOutOfRange)?;
let __body_len_u64 = __decoder.read_varint_u64()?;
let __max = ::pack_io::Decode::max_alloc(__decoder) as u64;
if __body_len_u64 > __max {
return ::core::result::Result::Err(::pack_io::SerialError::InvalidLength {
declared: __body_len_u64,
remaining: 0,
});
}
let __body_len = __body_len_u64 as usize;
let mut __body_buf = ::std::vec![0u8; __body_len];
__decoder.read_into(&mut __body_buf)?;
let mut __body_dec = ::pack_io::Decoder::new(&__body_buf);
#(#field_inits)*
::core::result::Result::Ok(#constructor)
})
}
}
}
fn field_local_ident(fm: &FieldMeta<'_>, index: usize) -> Ident {
match &fm.field_ident {
Some(id) => Ident::new(&format!("__f_{}", id), id.span()),
None => Ident::new(&format!("__f_{}", index), fm.ty.span()),
}
}
fn deserialize_enum_body(name: &Ident, data: &DataEnum) -> syn::Result<TokenStream2> {
if data.variants.is_empty() {
return Err(syn::Error::new_spanned(
name,
"pack-io: empty enums cannot be deserialised",
));
}
let arms: Vec<TokenStream2> = data
.variants
.iter()
.enumerate()
.map(|(index, variant)| {
let index = u32::try_from(index).expect("u32 enum variants");
let var_name = &variant.ident;
let constructor =
construct_from_fields(quote!(#name :: #var_name), &variant.fields, |ty| {
quote_spanned! { ty.span() =>
<#ty as ::pack_io::Deserialize>::deserialize(__decoder)?
}
});
quote! { #index => ::core::result::Result::Ok(#constructor), }
})
.collect();
let enum_name = name.to_string();
Ok(quote! {
let __tag = __decoder.read_varint_u64()?;
let __idx = u32::try_from(__tag)
.map_err(|_| ::pack_io::SerialError::UnknownVariant {
kind: #enum_name,
index: u64::MAX,
})?;
match __idx {
#(#arms)*
other => ::core::result::Result::Err(::pack_io::SerialError::UnknownVariant {
kind: #enum_name,
index: u64::from(other),
}),
}
})
}
fn expand_deserialize_view(input: &DeriveInput) -> syn::Result<TokenStream2> {
let name = &input.ident;
let lifetime = extract_single_lifetime(&input.generics).ok_or_else(|| {
syn::Error::new_spanned(
&input.generics,
"pack-io: `DeserializeView` requires the type to have exactly one lifetime parameter \
(used as the borrow of the input buffer)",
)
})?;
let generics = add_trait_bound_with_lifetime(
&input.generics,
parse_quote!(::pack_io::DeserializeView<#lifetime>),
&lifetime,
);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let body = match &input.data {
Data::Struct(data) => {
let constructor = construct_from_fields(quote!(#name), &data.fields, |ty| {
quote_spanned! { ty.span() =>
<#ty as ::pack_io::DeserializeView<#lifetime>>::deserialize_view(__decoder)?
}
});
quote! { ::core::result::Result::Ok(#constructor) }
}
Data::Enum(e) => {
return Err(syn::Error::new_spanned(
e.enum_token,
"pack-io: `DeserializeView` on enums is not yet supported; track \
https://github.com/jamesgober/pack-io/issues for status",
));
}
Data::Union(u) => {
return Err(syn::Error::new(
u.union_token.span(),
"pack-io: `union` is not supported",
));
}
};
Ok(quote! {
#[automatically_derived]
impl #impl_generics ::pack_io::DeserializeView<#lifetime> for #name #ty_generics #where_clause {
#[inline]
fn deserialize_view(
__decoder: &mut ::pack_io::Decoder<#lifetime>,
) -> ::pack_io::Result<Self> {
#body
}
}
})
}
#[derive(Default, Clone, Copy)]
struct SchemaAttrs {
since: Option<u32>,
deprecated: Option<u32>,
}
impl SchemaAttrs {
fn live_at(self, version: u32) -> bool {
let since = self.since.unwrap_or(1);
if version < since {
return false;
}
match self.deprecated {
Some(d) => version < d,
None => true,
}
}
}
struct FieldMeta<'a> {
accessor: TokenStream2,
field_ident: Option<&'a Ident>,
ty: &'a syn::Type,
attrs: SchemaAttrs,
}
fn parse_type_attrs(attrs: &[Attribute]) -> syn::Result<Option<u32>> {
let mut version: Option<u32> = None;
for attr in attrs {
if !attr.path().is_ident("pack_io") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("version") {
let lit: syn::LitInt = meta.value()?.parse()?;
let v: u32 = lit.base10_parse()?;
if v == 0 {
return Err(meta.error("pack-io: schema versions start at 1, not 0"));
}
version = Some(v);
Ok(())
} else if meta.path.is_ident("since") || meta.path.is_ident("deprecated") {
Err(meta.error(
"pack-io: `since` / `deprecated` are field-level attributes; only \
`version` is allowed on the type",
))
} else {
Err(meta.error("pack-io: unknown attribute (expected `version`)"))
}
})?;
}
Ok(version)
}
fn parse_field_attrs(attrs: &[Attribute]) -> syn::Result<SchemaAttrs> {
let mut out = SchemaAttrs::default();
for attr in attrs {
if !attr.path().is_ident("pack_io") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("since") {
let lit: syn::LitInt = meta.value()?.parse()?;
let v: u32 = lit.base10_parse()?;
if v == 0 {
return Err(meta.error("pack-io: schema versions start at 1, not 0"));
}
out.since = Some(v);
Ok(())
} else if meta.path.is_ident("deprecated") {
let lit: syn::LitInt = meta.value()?.parse()?;
let v: u32 = lit.base10_parse()?;
if v == 0 {
return Err(meta.error("pack-io: schema versions start at 1, not 0"));
}
out.deprecated = Some(v);
Ok(())
} else if meta.path.is_ident("version") {
Err(meta.error(
"pack-io: `version` is a type-level attribute; use `since` / `deprecated` \
on individual fields",
))
} else {
Err(meta.error("pack-io: unknown attribute (expected `since` or `deprecated`)"))
}
})?;
}
if let (Some(since), Some(dep)) = (out.since, out.deprecated) {
if dep <= since {
return Err(syn::Error::new(
attrs[0].span(),
format!(
"pack-io: `deprecated = {dep}` must be strictly greater than \
`since = {since}` — a field cannot be removed before it is introduced",
),
));
}
}
Ok(out)
}
fn collect_field_meta(fields: &Fields, owner: TokenStream2) -> syn::Result<Vec<FieldMeta<'_>>> {
let mut out = Vec::new();
match fields {
Fields::Named(named) => {
for f in &named.named {
let ident = f.ident.as_ref().expect("named fields have idents");
out.push(FieldMeta {
accessor: quote!(#owner.#ident),
field_ident: Some(ident),
ty: &f.ty,
attrs: parse_field_attrs(&f.attrs)?,
});
}
}
Fields::Unnamed(unnamed) => {
for (i, f) in unnamed.unnamed.iter().enumerate() {
let idx = Index::from(i);
out.push(FieldMeta {
accessor: quote!(#owner.#idx),
field_ident: None,
ty: &f.ty,
attrs: parse_field_attrs(&f.attrs)?,
});
}
}
Fields::Unit => {}
}
Ok(out)
}
fn variant_bindings(fields: &Fields) -> Vec<TokenStream2> {
match fields {
Fields::Named(named) => named
.named
.iter()
.map(|f| {
let name = f.ident.as_ref().expect("named fields have idents");
quote!(#name)
})
.collect(),
Fields::Unnamed(unnamed) => unnamed
.unnamed
.iter()
.enumerate()
.map(|(i, _)| {
let id = Ident::new(&format!("__f{i}"), unnamed.unnamed[i].span());
quote!(#id)
})
.collect(),
Fields::Unit => Vec::new(),
}
}
fn construct_from_fields<F>(path: TokenStream2, fields: &Fields, mut gen_expr: F) -> TokenStream2
where
F: FnMut(&syn::Type) -> TokenStream2,
{
match fields {
Fields::Named(named) => {
let pieces = named.named.iter().map(|f: &Field| {
let name = f.ident.as_ref().expect("named fields have idents");
let expr = gen_expr(&f.ty);
quote! { #name: #expr }
});
quote! { #path { #(#pieces),* } }
}
Fields::Unnamed(unnamed) => {
let pieces = unnamed.unnamed.iter().map(|f| gen_expr(&f.ty));
quote! { #path ( #(#pieces),* ) }
}
Fields::Unit => quote! { #path },
}
}
fn add_trait_bound(generics: &Generics, bound: syn::TypeParamBound) -> Generics {
let mut generics = generics.clone();
for param in &mut generics.params {
if let GenericParam::Type(t) = param {
t.bounds.push(bound.clone());
}
}
generics
}
fn add_trait_bound_with_lifetime(
generics: &Generics,
bound: syn::TypeParamBound,
_lifetime: &Lifetime,
) -> Generics {
let mut generics = generics.clone();
for param in &mut generics.params {
if let GenericParam::Type(t) = param {
t.bounds.push(bound.clone());
}
}
generics
}
fn extract_single_lifetime(generics: &Generics) -> Option<Lifetime> {
let lifetimes: Vec<&LifetimeParam> = generics
.params
.iter()
.filter_map(|p| {
if let GenericParam::Lifetime(l) = p {
Some(l)
} else {
None
}
})
.collect();
if lifetimes.len() == 1 {
Some(lifetimes[0].lifetime.clone())
} else {
None
}
}