use proc_macro2::{Ident, Span, TokenStream};
use quote::{quote, ToTokens as _};
use syn::{spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, Error, Field};
use crate::{
derive::project::{
derive_enum, derive_projection_struct_union, generate_tag_consts, struct_union_variant_id,
tag_ident,
},
repr::EnumRepr,
util::{
enum_size_from_repr, generate_tag_enum, Client, Ctx, DataExt, FieldBounds,
ImplBlockBuilder, Trait, TraitBound,
},
};
fn candidate_ident(ctx: &Ctx) -> Ident {
let fields = ctx.ast.data.fields();
let mut name = "candidate".to_owned();
while fields.iter().any(|(_, field, _)| field.to_string().trim_start_matches("r#") == name) {
name.push('_');
}
Ident::new(&name, Span::mixed_site())
}
fn derive_variant_is_safe<'a>(
ctx: &Ctx,
variant_id: &TokenStream,
fields: impl IntoIterator<Item = &'a Field>,
) -> Result<TokenStream, Error> {
let fields = fields.into_iter().collect::<Vec<_>>();
if fields.is_empty() {
return Ok(quote!(true));
}
let zerocopy_crate = &ctx.zerocopy_crate;
let core = ctx.core_path();
let candidate = candidate_ident(ctx);
let field_names = fields.iter().enumerate().map(|(idx, field)| {
field
.ident
.as_ref()
.map(|name| name.to_token_stream())
.unwrap_or_else(|| syn::Index::from(idx).to_token_stream())
});
let field_checks = field_names
.map(|name| {
quote! {
#zerocopy_crate::into_inner!(
#candidate.project::<
#zerocopy_crate::project_clients::TryFromBytesDerive,
_,
{ #variant_id },
{ #zerocopy_crate::ident_id!(#name) },
>()
).try_into_safe::<_, #zerocopy_crate::BecauseImmutable>()
}
})
.collect::<Vec<_>>();
if ctx.invariant_span.is_none() {
return Ok(quote! { true #(&& #field_checks.is_ok())* });
}
let field_bindings = fields
.iter()
.enumerate()
.map(|(idx, _)| ctx.fresh_ident(&format!("field_{}", idx)))
.collect::<Vec<_>>();
let bound_fields =
fields.iter().copied().zip(field_bindings.iter().cloned()).collect::<Vec<_>>();
let field_validations = fields
.iter()
.enumerate()
.map(|(idx, field)| {
let invariants = crate::invariant::parse(&field.attrs)?;
let invariants = invariants
.iter()
.map(|expression| crate::invariant::bind_fields(&bound_fields[..=idx], expression));
Ok(quote! {
#(
if !{ #[inline(always)] || -> #core::primitive::bool { #invariants } }() {
return false;
}
)*
})
})
.collect::<Result<Vec<_>, Error>>()?;
Ok(quote! {
{
#(
#[allow(unused_variables)]
let #field_bindings = match #field_checks {
#core::result::Result::Ok(#field_bindings) => #field_bindings,
#core::result::Result::Err(_) => return false,
};
#field_validations
)*
true
}
})
}
pub(crate) fn derive_is_safe(
ctx: &Ctx,
data: &DataEnum,
repr: &EnumRepr,
) -> Result<TokenStream, Error> {
if !(repr.is_c() || repr.is_primitive()) {
return Err(Error::new(
ctx.ast.span(),
"must have #[repr(C)] or #[repr(Int)] attribute in order to guarantee this type's memory layout",
));
}
let zerocopy_crate = &ctx.zerocopy_crate;
let core = ctx.core_path();
let candidate = candidate_ident(ctx);
let alignment = ctx.fresh_ident("___ZcAlignment");
let tag = Ident::new("tag", Span::mixed_site());
let projections = if data.fields().is_empty() {
let tag_enum = generate_tag_enum(ctx, repr, data);
let tag_consts = generate_tag_consts(data);
quote! {
#tag_enum
type ___ZerocopyTagPrimitive = #zerocopy_crate::util::macro_util::SizeToTag<
{ #core::mem::size_of::<___ZerocopyTag>() },
>;
#tag_consts
}
} else {
derive_enum(ctx, data, Client::TryFromBytesDerive)?
};
let match_arms = data
.variants
.iter()
.enumerate()
.map(|(idx, variant)| {
let name = &variant.ident;
let variant_id = quote! { #zerocopy_crate::ident_id!(#name) };
let fields_is_safe = derive_variant_is_safe(ctx, &variant_id, &variant.fields)?;
let pattern = if ctx.invariant_span.is_some() {
quote! { #core::option::Option::Some(#idx) }
} else {
tag_ident(name).to_token_stream()
};
Ok(quote! {
#pattern => { #fields_is_safe }
})
})
.collect::<Result<Vec<_>, Error>>()?;
let read_tag = quote! {
let #tag = #candidate
.reborrow()
.cast::<
___ZerocopyTagPrimitive,
#zerocopy_crate::pointer::cast::CastSized,
(#zerocopy_crate::pointer::BecauseRead, _),
>()
.recall_validity::<_, (_, (_, _))>()
.read::<#zerocopy_crate::BecauseImmutable>();
};
let tag_init = if ctx.invariant_span.is_some() {
let allow = crate::util::allow_generated_code();
let tag_arms = data.variants.iter().enumerate().map(|(idx, variant)| {
let tag = tag_ident(&variant.ident);
quote! { #tag => #core::option::Option::Some(#idx) }
});
quote! {
#allow
let #tag = {
#projections
#read_tag
match #tag {
#(#tag_arms,)*
_ => #core::option::Option::None,
}
};
}
} else {
quote! { #projections #read_tag }
};
Ok(quote! {
#[inline]
fn is_safe<#alignment>(
mut #candidate: #zerocopy_crate::Maybe<'_, Self, #alignment>,
) -> #core::primitive::bool
where
#alignment: #zerocopy_crate::invariant::Alignment,
{
#tag_init
match #tag {
#(#match_arms,)*
_ => false,
}
}
})
}
pub(crate) fn derive_try_from_bytes(ctx: &Ctx, top_level: Trait) -> Result<TokenStream, Error> {
match &ctx.ast.data {
Data::Struct(strct) => derive_try_from_bytes_struct(ctx, strct, top_level),
Data::Enum(enm) => derive_try_from_bytes_enum(ctx, enm, top_level),
Data::Union(unn) => derive_try_from_bytes_union(ctx, unn, top_level),
}
}
fn derive_try_from_bytes_struct(
ctx: &Ctx,
strct: &DataStruct,
top_level: Trait,
) -> Result<TokenStream, Error> {
let extras = if let Some(extras) = try_gen_trivial_is_safe(ctx, top_level) {
extras
} else {
let zerocopy_crate = &ctx.zerocopy_crate;
let variant_id = quote! { #zerocopy_crate::STRUCT_VARIANT_ID };
let fields_is_safe = derive_variant_is_safe(ctx, &variant_id, &strct.fields)?;
let core = ctx.core_path();
let candidate = candidate_ident(ctx);
let alignment = ctx.fresh_ident("___ZcAlignment");
quote!(
#[inline]
fn is_safe<#alignment>(
mut #candidate: #zerocopy_crate::Maybe<'_, Self, #alignment>,
) -> #core::primitive::bool
where
#alignment: #zerocopy_crate::invariant::Alignment,
{
#fields_is_safe
}
)
};
Ok(ImplBlockBuilder::new(ctx, strct, Trait::TryFromBytes, FieldBounds::ALL_SELF)
.inner_extras(extras)
.outer_extras(derive_projection_struct_union(ctx, strct, Client::TryFromBytesDerive))
.build())
}
fn derive_try_from_bytes_union(
ctx: &Ctx,
unn: &DataUnion,
top_level: Trait,
) -> Result<TokenStream, Error> {
let field_type_trait_bounds = FieldBounds::All(&[TraitBound::Slf]);
let zerocopy_crate = &ctx.zerocopy_crate;
let union_variant_id = struct_union_variant_id(ctx).to_token_stream();
let extras = if let Some(extras) = try_gen_trivial_is_safe(ctx, top_level) {
extras
} else {
let fields_is_safe = unn
.fields
.named
.iter()
.map(|field| derive_variant_is_safe(ctx, &union_variant_id, [field]))
.collect::<Result<Vec<_>, _>>()?;
let core = ctx.core_path();
let candidate = candidate_ident(ctx);
let alignment = ctx.fresh_ident("___ZcAlignment");
quote!(
#[inline]
fn is_safe<#alignment>(
mut #candidate: #zerocopy_crate::Maybe<'_, Self, #alignment>,
) -> #core::primitive::bool
where
#alignment: #zerocopy_crate::invariant::Alignment,
{
false #(|| { #[inline(always)] || { #fields_is_safe } }())*
}
)
};
Ok(ImplBlockBuilder::new(ctx, unn, Trait::TryFromBytes, field_type_trait_bounds)
.inner_extras(extras)
.outer_extras(derive_projection_struct_union(ctx, unn, Client::TryFromBytesDerive))
.build())
}
fn derive_try_from_bytes_enum(
ctx: &Ctx,
enm: &DataEnum,
top_level: Trait,
) -> Result<TokenStream, Error> {
let repr = EnumRepr::from_attrs(&ctx.ast.attrs)?;
let could_be_from_bytes = enum_size_from_repr(&repr)
.map(|size| enm.fields().is_empty() && enm.variants.len() == 1usize << size)
.unwrap_or(false);
let trivial_is_safe = try_gen_trivial_is_safe(ctx, top_level);
let extra = match (trivial_is_safe, could_be_from_bytes) {
(Some(is_safe), _) => is_safe,
(None, true) => unsafe { gen_trivial_is_safe_unchecked(ctx) },
(None, false) => match derive_is_safe(ctx, enm, &repr) {
Ok(extra) => extra,
Err(e) => return ctx.error_or_skip(e),
},
};
Ok(ImplBlockBuilder::new(ctx, enm, Trait::TryFromBytes, FieldBounds::ALL_SELF)
.inner_extras(extra)
.build())
}
fn try_gen_trivial_is_safe(ctx: &Ctx, top_level: Trait) -> Option<proc_macro2::TokenStream> {
if matches!(top_level, Trait::FromBytes)
&& ctx.ast.generics.params.is_empty()
&& !ctx.skip_on_error
{
let zerocopy_crate = &ctx.zerocopy_crate;
let core = ctx.core_path();
Some(quote!(
#[inline(always)]
fn is_safe<___ZcAlignment>(
_candidate: #zerocopy_crate::Maybe<'_, Self, ___ZcAlignment>,
) -> #core::primitive::bool
where
___ZcAlignment: #zerocopy_crate::invariant::Alignment,
{
if false {
fn assert_is_from_bytes<T>()
where
T: #zerocopy_crate::FromBytes,
T: ?#core::marker::Sized,
{
}
assert_is_from_bytes::<Self>();
}
true
}
))
} else {
None
}
}
unsafe fn gen_trivial_is_safe_unchecked(ctx: &Ctx) -> proc_macro2::TokenStream {
let zerocopy_crate = &ctx.zerocopy_crate;
let core = ctx.core_path();
quote!(
#[inline(always)]
fn is_safe<___ZcAlignment>(
_candidate: #zerocopy_crate::Maybe<'_, Self, ___ZcAlignment>,
) -> #core::primitive::bool
where
___ZcAlignment: #zerocopy_crate::invariant::Alignment,
{
true
}
)
}
#[cfg(test)]
mod tests {
use syn::{parse_quote, DeriveInput};
use super::*;
fn derive_error(input: DeriveInput) -> String {
let ctx = Ctx::try_from_derive_input(input).unwrap();
match derive_try_from_bytes(&ctx, Trait::TryFromBytes) {
Ok(_) => panic!("expected derive to reject conflicting representations"),
Err(error) => error.to_string(),
}
}
#[test]
fn raw_repr_conflict_matches_ordinary_repr() {
let ordinary = derive_error(parse_quote! {
#[repr(C, align(8))]
#[repr(u8)]
enum Packet {
Value(core::num::NonZeroU8),
End,
}
});
let raw = derive_error(parse_quote! {
#[repr(C, align(8))]
#[r#repr(r#u8)]
enum Packet {
Value(core::num::NonZeroU8),
End,
}
});
assert_eq!(ordinary, "this conflicts with another representation hint");
assert_eq!(ordinary, raw);
}
}