diff --git a/tools/cargo-zerocopy/src/main.rs b/tools/cargo-zerocopy/src/main.rs index 7ca066aa8a..2c91454c5a 100644 --- a/tools/cargo-zerocopy/src/main.rs +++ b/tools/cargo-zerocopy/src/main.rs @@ -314,7 +314,7 @@ fn set_ui_test_feature_args(command: &mut Command, args: &[String]) { fn get_rustflags(name: &str) -> String { // See #1792 for context on zerocopy_derive_union_into_bytes. let mut flags = - "--cfg zerocopy_unstable_linux --cfg zerocopy_derive_union_into_bytes --cfg __ZEROCOPY_INTERNAL_USE_ONLY_DEV_MODE" + "--cfg zerocopy_unstable_linux --cfg zerocopy_unstable_ptr --cfg zerocopy_derive_union_into_bytes --cfg __ZEROCOPY_INTERNAL_USE_ONLY_DEV_MODE" .to_string(); flags += &format!(" --cfg __ZEROCOPY_INTERNAL_USE_ONLY_TOOLCHAIN=\"{name}\""); diff --git a/zerocopy/src/lib.rs b/zerocopy/src/lib.rs index 40b835610e..3ac6db76fd 100644 --- a/zerocopy/src/lib.rs +++ b/zerocopy/src/lib.rs @@ -1708,6 +1708,62 @@ pub unsafe trait Immutable { /// } /// ``` /// +#[cfg_attr( + zerocopy_unstable_ptr, + doc = r#" +# Field invariants + +This experimental feature requires `--cfg zerocopy_unstable_ptr`. + +Named fields of structs, enum variants, and unions can specify additional +runtime checks using `#[zerocopy(invariant(expression))]`: + +``` +# use zerocopy_derive::TryFromBytes; +#[derive(TryFromBytes)] +struct Foo { + a: u8, + #[zerocopy(invariant((**a.unaligned_as_ref() % 2) == (**b.unaligned_as_ref() as u8)))] + b: bool, + #[zerocopy(invariant(**c.unaligned_as_ref() > 0))] + c: i8, +} +``` + +Each expression must return a `bool`. It has access to validated, read-only +[`Ptr`]s to the current field and all preceding fields of the struct or +variant, using their field names. A union's invariants have access only to +the current field. The expression can use the existing [`Ptr`] APIs to +inspect those fields. In this example, all fields are accessed by reference. + +Rust's usual restrictions on local bindings apply; for example, a field name +cannot shadow an in-scope constant. + +Fields are checked in declaration order. Each field's bit validity is +checked before its invariants run. Multiple invariants on a field run in +attribute order. For structs and enums, validation stops at the first invalid +field or invariant that returns `false`. For unions, a failed bit-validity +check or invariant causes validation to try the next field; validation +succeeds as soon as one field and all its invariants pass. Each expression +runs in its own closure; `return` returns from that expression, and panics +propagate to the caller. Only the selected enum variant's fields and +invariants are checked. Expressions may have arbitrary side effects, even +when the conversion fails or its result is discarded. + +These predicates are additional acceptance checks beyond Rust's bit validity; +bit-valid bytes may still be rejected. See [What is a "valid instance"?] for +the distinction. + +[What is a "valid instance"?]: trait@TryFromBytes#what-is-a-valid-instance + +Invariants are not supported on tuple fields. Types with invariants cannot +derive [`FromZeros`] or [`FromBytes`], whose conversions do not perform runtime +validation. These checks apply to conversions through [`TryFromBytes`]; they +do not restrict ordinary construction or mutation of Rust values. + +"# +)] +/// /// # Portability /// /// To ensure consistent endianness for enums with multi-byte representations, @@ -1802,6 +1858,14 @@ pub use zerocopy_derive::TryFromBytes; /// If you are negatively affected by lack of support for a particular type, /// we encourage you to let us know by [filing an issue][github-repo]. /// +/// In this trait's conversion methods, a "valid instance" must also pass any +/// configured field invariants, including those on nested fields. Rust bit +/// validity alone does not guarantee that a conversion succeeds: an invariant +/// may reject otherwise bit-valid bytes. Such predicates check acceptance at +/// conversion time; they do not constrain subsequent mutation or ordinary Rust +/// construction. See the [derive's field invariants][derive] documentation for +/// the experimental attribute's syntax, evaluation order, and side effects. +/// /// # `TryFromBytes` is not symmetrical with [`IntoBytes`] /// /// There are some types which implement both `TryFromBytes` and [`IntoBytes`], @@ -1966,7 +2030,7 @@ pub unsafe trait TryFromBytes { @variant "dynamic_padding" ] )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_ref_from_bytes(source: &[u8]) -> Result<&Self, TryCastError<&[u8], Self>> @@ -2089,7 +2153,7 @@ pub unsafe trait TryFromBytes { @variant "dynamic_padding" ] )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_ref_from_prefix(source: &[u8]) -> Result<(&Self, &[u8]), TryCastError<&[u8], Self>> @@ -2199,7 +2263,7 @@ pub unsafe trait TryFromBytes { @variant "dynamic_padding" ] )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_ref_from_suffix(source: &[u8]) -> Result<(&[u8], &Self), TryCastError<&[u8], Self>> @@ -2293,7 +2357,7 @@ pub unsafe trait TryFromBytes { #[doc = codegen_header!("h5", "try_mut_from_bytes")] /// /// See [`TryFromBytes::try_ref_from_bytes`](#method.try_ref_from_bytes.codegen). - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_mut_from_bytes(bytes: &mut [u8]) -> Result<&mut Self, TryCastError<&mut [u8], Self>> @@ -2402,7 +2466,7 @@ pub unsafe trait TryFromBytes { #[doc = codegen_header!("h5", "try_mut_from_prefix")] /// /// See [`TryFromBytes::try_ref_from_prefix`](#method.try_ref_from_prefix.codegen). - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_mut_from_prefix( @@ -2502,7 +2566,7 @@ pub unsafe trait TryFromBytes { #[doc = codegen_header!("h5", "try_mut_from_suffix")] /// /// See [`TryFromBytes::try_ref_from_suffix`](#method.try_ref_from_suffix.codegen). - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_mut_from_suffix( @@ -2607,7 +2671,7 @@ pub unsafe trait TryFromBytes { @variant "dynamic_padding" ] )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_ref_from_bytes_with_elems( @@ -2728,7 +2792,7 @@ pub unsafe trait TryFromBytes { @variant "dynamic_padding" ] )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_ref_from_prefix_with_elems( @@ -2836,7 +2900,7 @@ pub unsafe trait TryFromBytes { @variant "dynamic_padding" ] )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_ref_from_suffix_with_elems( @@ -2932,7 +2996,7 @@ pub unsafe trait TryFromBytes { #[doc = codegen_header!("h5", "try_mut_from_bytes_with_elems")] /// /// See [`TryFromBytes::try_ref_from_bytes_with_elems`](#method.try_ref_from_bytes_with_elems.codegen). - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_mut_from_bytes_with_elems( @@ -3043,7 +3107,7 @@ pub unsafe trait TryFromBytes { #[doc = codegen_header!("h5", "try_mut_from_prefix_with_elems")] /// /// See [`TryFromBytes::try_ref_from_prefix_with_elems`](#method.try_ref_from_prefix_with_elems.codegen). - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_mut_from_prefix_with_elems( @@ -3143,7 +3207,7 @@ pub unsafe trait TryFromBytes { #[doc = codegen_header!("h5", "try_mut_from_suffix_with_elems")] /// /// See [`TryFromBytes::try_ref_from_suffix_with_elems`](#method.try_ref_from_suffix_with_elems.codegen). - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_mut_from_suffix_with_elems( @@ -3210,7 +3274,7 @@ pub unsafe trait TryFromBytes { bench = "try_read_from_bytes", format = "coco_static_size", )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_read_from_bytes(source: &[u8]) -> Result> @@ -3288,7 +3352,7 @@ pub unsafe trait TryFromBytes { bench = "try_read_from_prefix", format = "coco_static_size", )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_read_from_prefix(source: &[u8]) -> Result<(Self, &[u8]), TryReadError<&[u8], Self>> @@ -3367,7 +3431,7 @@ pub unsafe trait TryFromBytes { bench = "try_read_from_suffix", format = "coco_static_size", )] - #[must_use = "has no side effects"] + #[must_use = "the conversion result must be checked"] #[cfg_attr(zerocopy_inline_always, inline(always))] #[cfg_attr(not(zerocopy_inline_always), inline)] fn try_read_from_suffix(source: &[u8]) -> Result<(&[u8], Self), TryReadError<&[u8], Self>> diff --git a/zerocopy/src/pointer/ptr.rs b/zerocopy/src/pointer/ptr.rs index 74f71b7733..dcf47b29dd 100644 --- a/zerocopy/src/pointer/ptr.rs +++ b/zerocopy/src/pointer/ptr.rs @@ -831,7 +831,7 @@ mod _transitions { } /// Casts of the referent type. -#[cfg_attr(not(zerocopy_unstable_ptr), allow(unreachable_pub))] +#[allow(unreachable_pub)] // False positive on MSRV pub use _casts::TryWithError; mod _casts { use core::cell::UnsafeCell; diff --git a/zerocopy/testutil/src/lib.rs b/zerocopy/testutil/src/lib.rs index af5cec72df..d3f18af374 100644 --- a/zerocopy/testutil/src/lib.rs +++ b/zerocopy/testutil/src/lib.rs @@ -164,14 +164,16 @@ impl UiTestRunner { let mut command = Command::new("cargo"); command.current_dir(workspace_root.clone()); - // We strip `--cfg zerocopy_derive_union_into_bytes` and `--cfg - // zerocopy_unstable_linux` from `RUSTFLAGS` so that the + // We strip experimental feature cfgs from `RUSTFLAGS` so that the // `zerocopy-derive` proc macro is built without them. This ensures it // generates the feature-gate checks into the UI tests, which we can // then explicitly enable or disable via `rustc_args`. let mut rustflags = env::var("RUSTFLAGS").unwrap_or_default(); - let cfgs_to_strip = - ["--cfg zerocopy_derive_union_into_bytes", "--cfg zerocopy_unstable_linux"]; + let cfgs_to_strip = [ + "--cfg zerocopy_derive_union_into_bytes", + "--cfg zerocopy_unstable_linux", + "--cfg zerocopy_unstable_ptr", + ]; for &cfg in &cfgs_to_strip { rustflags = rustflags.replace(cfg, ""); } diff --git a/zerocopy/zerocopy-derive/Cargo.toml b/zerocopy/zerocopy-derive/Cargo.toml index beb86320cb..ef508ec7e7 100644 --- a/zerocopy/zerocopy-derive/Cargo.toml +++ b/zerocopy/zerocopy-derive/Cargo.toml @@ -29,6 +29,7 @@ unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage_nightly)', 'cfg(zerocopy_derive_union_into_bytes)', 'cfg(zerocopy_unstable_linux)', + 'cfg(zerocopy_unstable_ptr)', ] } [lib] diff --git a/zerocopy/zerocopy-derive/src/derive/from_bytes.rs b/zerocopy/zerocopy-derive/src/derive/from_bytes.rs index 20ebd57965..b94274a00e 100644 --- a/zerocopy/zerocopy-derive/src/derive/from_bytes.rs +++ b/zerocopy/zerocopy-derive/src/derive/from_bytes.rs @@ -89,6 +89,12 @@ pub(crate) fn find_zero_variant(enm: &DataEnum) -> Result { Err(has_unknown_discriminants) } pub(crate) fn derive_from_zeros(ctx: &Ctx, top_level: Trait) -> Result { + if let Some(span) = ctx.invariant_span { + return ctx.error_or_skip(Error::new( + span, + "cannot derive `FromZeros` for a type with invariants", + )); + } let try_from_bytes = derive_try_from_bytes(ctx, top_level)?; let from_zeros = match &ctx.ast.data { Data::Struct(strct) => derive_from_zeros_struct(ctx, strct), @@ -98,6 +104,12 @@ pub(crate) fn derive_from_zeros(ctx: &Ctx, top_level: Trait) -> Result Result { + if let Some(span) = ctx.invariant_span { + return ctx.error_or_skip(Error::new( + span, + "cannot derive `FromBytes` for a type with invariants", + )); + } let from_zeros = derive_from_zeros(ctx, top_level)?; let from_bytes = match &ctx.ast.data { Data::Struct(strct) => derive_from_bytes_struct(ctx, strct), diff --git a/zerocopy/zerocopy-derive/src/derive/try_from_bytes.rs b/zerocopy/zerocopy-derive/src/derive/try_from_bytes.rs index 924a63fc75..5be26ba9fc 100644 --- a/zerocopy/zerocopy-derive/src/derive/try_from_bytes.rs +++ b/zerocopy/zerocopy-derive/src/derive/try_from_bytes.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: BSD-2-Clause OR Apache-2.0 OR MIT // -use proc_macro2::TokenStream; -use quote::quote; -use syn::{spanned::Spanned as _, Data, DataEnum, DataStruct, DataUnion, Error, Type, Visibility}; +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::{ @@ -16,29 +16,99 @@ use crate::{ }, }; -/// Generates validation of every field of a struct or enum variant. -fn derive_variant_is_safe( +fn candidate_ident(ctx: &Ctx) -> Ident { + // Choose a source-pointer name distinct from the field names. Mixed-site + // hygiene keeps it out of the user's invariant expressions. + 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()) +} + +/// Generates validation of every field of a struct or enum variant, leaving +/// the validated pointers in scope for subsequent field invariants. +/// A union is checked by invoking this once per field. +fn derive_variant_is_safe<'a>( ctx: &Ctx, variant_id: &TokenStream, - fields: &[(&Visibility, TokenStream, &Type)], -) -> TokenStream { + fields: impl IntoIterator, +) -> Result { + let fields = fields.into_iter().collect::>(); + if fields.is_empty() { + return Ok(quote!(true)); + } + let zerocopy_crate = &ctx.zerocopy_crate; - let trait_path = Trait::TryFromBytes.crate_path(ctx); - let field_names = fields.iter().map(|(_, name, _)| name); - let field_tys = fields.iter().map(|(_, _, ty)| ty); - quote! { - true #(&& { - let field_candidate = #zerocopy_crate::into_inner!( - candidate.reborrow().project::< - #zerocopy_crate::project_clients::TryFromBytesDerive, - _, - { #variant_id }, - { #zerocopy_crate::ident_id!(#field_names) }, - >() - ); - <#field_tys as #trait_path>::is_safe(field_candidate) - })* + 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::>(); + // Without invariants, no expression needs the validated pointers. Avoid + // introducing bindings that could collide with caller-defined items. + 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::>(); + let bound_fields = + fields.iter().copied().zip(field_bindings.iter().cloned()).collect::>(); + 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! { + #( + // Keep `return` in an invariant from bypassing later fields. + if !{ #[inline(always)] || -> #core::primitive::bool { #invariants } }() { + return false; + } + )* + }) + }) + .collect::, Error>>()?; + Ok(quote! { + { + #( + // Retain each shared pointer for subsequent field invariants. + #[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 + } + }) } /// Generates an implementation of `is_safe` for an arbitrary enum. @@ -62,6 +132,9 @@ pub(crate) fn derive_is_safe( 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); @@ -78,15 +151,58 @@ pub(crate) fn derive_is_safe( derive_enum(ctx, data, Client::TryFromBytesDerive)? }; - let match_arms = data.variants().into_iter().map(|(variant, fields)| { - let variant = &variant.unwrap().ident; - let tag = tag_ident(variant); - let variant_id = quote! { #zerocopy_crate::ident_id!(#variant) }; - let fields_is_safe = derive_variant_is_safe(ctx, &variant_id, &fields); + 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::, 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! { - #tag => #fields_is_safe + // Keep implementation-only items and their lint allowances out + // of the scopes containing caller-authored invariant expressions. + #allow + let #tag = { + #projections + #read_tag + match #tag { + #(#tag_arms,)* + _ => #core::option::Option::None, + } + }; } - }); + } else { + quote! { #projections #read_tag } + }; Ok(quote! { // SAFETY: We use `is_safe` to validate that the bit pattern of the @@ -94,25 +210,15 @@ pub(crate) fn derive_is_safe( // check the bit validity of each field of the corresponding variant. // Thus, this is a sound implementation of `is_safe`. #[inline] - fn is_safe<___ZcAlignment>( - mut candidate: #zerocopy_crate::Maybe<'_, Self, ___ZcAlignment>, + fn is_safe<#alignment>( + mut #candidate: #zerocopy_crate::Maybe<'_, Self, #alignment>, ) -> #core::primitive::bool where - ___ZcAlignment: #zerocopy_crate::invariant::Alignment, + #alignment: #zerocopy_crate::invariant::Alignment, { - #projections - - let tag = candidate - .reborrow() - .cast::< - ___ZerocopyTagPrimitive, - #zerocopy_crate::pointer::cast::CastSized, - (#zerocopy_crate::pointer::BecauseRead, _), - >() - .recall_validity::<_, (_, (_, _))>() - .read::<#zerocopy_crate::BecauseImmutable>(); + #tag_init - match tag { + match #tag { #(#match_arms,)* _ => false, } @@ -123,7 +229,7 @@ pub(crate) fn derive_try_from_bytes(ctx: &Ctx, top_level: Trait) -> Result derive_try_from_bytes_struct(ctx, strct, top_level), Data::Enum(enm) => derive_try_from_bytes_enum(ctx, enm, top_level), - Data::Union(unn) => Ok(derive_try_from_bytes_union(ctx, unn, top_level)), + Data::Union(unn) => derive_try_from_bytes_union(ctx, unn, top_level), } } fn derive_try_from_bytes_struct( @@ -131,74 +237,80 @@ fn derive_try_from_bytes_struct( strct: &DataStruct, top_level: Trait, ) -> Result { - let extras = try_gen_trivial_is_safe(ctx, top_level).unwrap_or_else(|| { + 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 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!( // SAFETY: We use `is_safe` to validate that each field is bit-valid, // and only return `true` if all of them are. The bit validity of a // struct is just the composition of the bit validities of its // fields, so this is a sound implementation of `is_safe`. #[inline] - fn is_safe<___ZcAlignment>( - mut candidate: #zerocopy_crate::Maybe<'_, Self, ___ZcAlignment>, + fn is_safe<#alignment>( + mut #candidate: #zerocopy_crate::Maybe<'_, Self, #alignment>, ) -> #core::primitive::bool where - ___ZcAlignment: #zerocopy_crate::invariant::Alignment, + #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) -> TokenStream { +fn derive_try_from_bytes_union( + ctx: &Ctx, + unn: &DataUnion, + top_level: Trait, +) -> Result { let field_type_trait_bounds = FieldBounds::All(&[TraitBound::Slf]); let zerocopy_crate = &ctx.zerocopy_crate; - let union_variant_id = struct_union_variant_id(ctx); - let extras = try_gen_trivial_is_safe(ctx, top_level).unwrap_or_else(|| { - let fields = unn.fields(); - let field_names = fields.iter().map(|(_vis, name, _ty)| name); - let field_tys = fields.iter().map(|(_vis, _name, ty)| ty); + 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::, _>>()?; let core = ctx.core_path(); + let candidate = candidate_ident(ctx); + let alignment = ctx.fresh_ident("___ZcAlignment"); quote!( // SAFETY: We use `is_safe` to validate that any field is bit-valid; - // we only return `true` if at least one of them is. The bit validity - // of a union is not yet well defined in Rust, but it is guaranteed - // to be no more strict than this definition. See #696 for a more - // in-depth discussion. + // we only return `true` if at least one of them is and its + // invariants also hold. The bit validity of a union is not yet + // well defined in Rust, but it is guaranteed to be no more strict + // than this definition. See #696 for a more in-depth discussion. #[inline] - fn is_safe<___ZcAlignment>( - mut candidate: #zerocopy_crate::Maybe<'_, Self, ___ZcAlignment>, + fn is_safe<#alignment>( + mut #candidate: #zerocopy_crate::Maybe<'_, Self, #alignment>, ) -> #core::primitive::bool where - ___ZcAlignment: #zerocopy_crate::invariant::Alignment, + #alignment: #zerocopy_crate::invariant::Alignment, { - false #(|| { - let field_candidate = #zerocopy_crate::into_inner!( - candidate.reborrow().project::< - #zerocopy_crate::project_clients::TryFromBytesDerive, - _, - { #union_variant_id }, - { #zerocopy_crate::ident_id!(#field_names) }, - >() - ); - - <#field_tys as #zerocopy_crate::TryFromBytes>::is_safe(field_candidate) - })* + // Keep each field's bindings and early returns local to that + // field, so a failure lets validation try the next field. + false #(|| { #[inline(always)] || { #fields_is_safe } }())* } ) - }); - ImplBlockBuilder::new(ctx, unn, Trait::TryFromBytes, field_type_trait_bounds) + }; + 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() + .build()) } fn derive_try_from_bytes_enum( ctx: &Ctx, @@ -224,8 +336,7 @@ fn derive_try_from_bytes_enum( (None, true) => unsafe { gen_trivial_is_safe_unchecked(ctx) }, (None, false) => match derive_is_safe(ctx, enm, &repr) { Ok(extra) => extra, - Err(_) if ctx.skip_on_error => return Ok(TokenStream::new()), - Err(e) => return Err(e), + Err(e) => return ctx.error_or_skip(e), }, }; diff --git a/zerocopy/zerocopy-derive/src/invariant.rs b/zerocopy/zerocopy-derive/src/invariant.rs new file mode 100644 index 0000000000..a078e47ada --- /dev/null +++ b/zerocopy/zerocopy-derive/src/invariant.rs @@ -0,0 +1,114 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +use proc_macro2::{Ident, Span, TokenStream, TokenTree}; +use quote::{quote, ToTokens as _}; +use syn::{ + punctuated::Punctuated, spanned::Spanned as _, Attribute, Data, DeriveInput, Error, Expr, + Field, MacroDelimiter, Meta, Token, +}; + +use crate::util::path_is_ident; + +/// Parses field invariants in attribute order. +pub(crate) fn parse(attrs: &[Attribute]) -> Result, Error> { + let mut invariants = Vec::new(); + for attr in attrs.iter().filter(|attr| path_is_ident(attr.path(), "zerocopy")) { + let options = attr.parse_args_with(Punctuated::::parse_terminated)?; + for option in options { + if !path_is_ident(option.path(), "invariant") { + return Err(Error::new_spanned(option, "expected `invariant(...)`")); + } + match option { + Meta::List(list) if matches!(list.delimiter, MacroDelimiter::Paren(_)) => { + invariants.push(list.parse_args::()?) + } + other => return Err(Error::new_spanned(other, "expected `invariant(...)`")), + } + } + } + Ok(invariants) +} + +/// Validates invariant syntax and placement, returning the first invariant's +/// span so derives which cannot honor invariants can reject them. +pub(crate) fn validate(ast: &DeriveInput) -> Result, Error> { + let fields: Vec<&Field> = match &ast.data { + Data::Struct(data) => data.fields.iter().collect(), + Data::Enum(data) => { + for variant in &data.variants { + if let Some(invariant) = parse(&variant.attrs)?.first() { + return Err(Error::new( + invariant.span(), + "invariants are only supported on named fields", + )); + } + } + data.variants.iter().flat_map(|variant| &variant.fields).collect() + } + Data::Union(data) => data.fields.named.iter().collect(), + }; + let mut first = None; + for field in fields { + if let Some(invariant) = parse(&field.attrs)?.first() { + if field.ident.is_none() { + return Err(Error::new( + invariant.span(), + "invariants are only supported on named fields", + )); + } + first.get_or_insert(invariant.span()); + } + } + Ok(first) +} + +/// Visits identifiers, including those inside macro arguments. +pub(crate) fn idents(tokens: TokenStream) -> Vec { + tokens + .into_iter() + .flat_map(|token| match token { + TokenTree::Ident(ident) => vec![ident], + TokenTree::Group(group) => idents(group.stream()), + _ => Vec::new(), + }) + .collect() +} + +/// Binds semantic field names in every syntax context used by the expression. +/// Preserve the expression's tokens so its other names retain their hygiene. +pub(crate) fn bind_fields(fields: &[(&Field, Ident)], expression: &Expr) -> TokenStream { + let mut body = expression.to_token_stream(); + let references = idents(body.clone()); + for (field, binding) in fields { + let name = match &field.ident { + Some(name) => name, + None => continue, + }; + let spelling = name.to_string(); + let spelling = spelling.trim_start_matches("r#"); + // Also support references produced by a macro invoked by the expression. + let mut call_site = name.clone(); + call_site.set_span(Span::call_site()); + for alias in [name.clone(), call_site].into_iter().chain( + references + .iter() + .filter(|ident| ident.to_string().trim_start_matches("r#") == spelling) + .cloned(), + ) { + body = quote! {{ + // Require a binding; reject names Rust would resolve as constant + // patterns. Item aliases could capture helper calls in macros. + #[allow(unused_variables, non_snake_case, clippy::redundant_pattern)] + let #alias @ _ = #binding; + #body + }}; + } + } + body +} diff --git a/zerocopy/zerocopy-derive/src/lib.rs b/zerocopy/zerocopy-derive/src/lib.rs index 68e8469134..0bff5ca7fa 100644 --- a/zerocopy/zerocopy-derive/src/lib.rs +++ b/zerocopy/zerocopy-derive/src/lib.rs @@ -50,6 +50,7 @@ macro_rules! ident { } mod derive; +mod invariant; #[cfg(test)] mod output_tests; mod repr; @@ -93,10 +94,13 @@ macro_rules! derive { Err(e) => return e.into_compile_error().into(), }; let ts = $inner(&ctx, Trait::$trait).into_ts(); - // We wrap in `const_block` as a backstop in case any derive fails - // to wrap its output in `const_block` (and thus fails to annotate) - // with the full set of `#[allow(...)]` attributes). - let ts = const_block([Some(ts)]); + // Apply generated-code lint allowances as a backstop, except + // around caller-authored invariant expressions. + let ts = if matches!(Trait::$trait, Trait::TryFromBytes) { + ctx.const_block([Some(ts)]) + } else { + const_block([Some(ts)]) + }; #[cfg(test)] crate::util::testutil::check_hygiene(ts.clone()); ts.into() @@ -176,6 +180,48 @@ pub fn __test_hygienically_mixed_into_bytes( ) } +/// Constructs an invariant whose field declaration and reference have different +/// syntax contexts, while preserving all other caller tokens. +#[doc(hidden)] +#[proc_macro] +pub fn __test_hygienically_mixed_invariant( + input: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + // Cross-compilation does not necessarily pass the nightly test cfg to + // host proc macros. Omit the fixture and its test together in that case. + #[cfg(not(__ZEROCOPY_INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS))] + { + let _ = input; + proc_macro::TokenStream::new() + } + #[cfg(__ZEROCOPY_INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS)] + { + use proc_macro::{Group, Ident, Span, TokenStream, TokenTree}; + + fn rewrite(input: TokenStream) -> TokenStream { + input + .into_iter() + .map(|token| match token { + TokenTree::Ident(ident) if ident.to_string() == "DefField" => { + TokenTree::Ident(Ident::new("field", Span::def_site())) + } + TokenTree::Ident(ident) if ident.to_string() == "CallField" => { + TokenTree::Ident(Ident::new("field", Span::call_site())) + } + TokenTree::Group(group) => { + let mut rewritten = Group::new(group.delimiter(), rewrite(group.stream())); + rewritten.set_span(group.span()); + TokenTree::Group(rewritten) + } + token => token, + }) + .collect() + } + + rewrite(input) + } +} + #[cfg_attr(not(zerocopy_unstable_linux), doc(hidden))] #[proc_macro_derive(most_traits, attributes(zerocopy))] pub fn most_traits(ts: proc_macro::TokenStream) -> proc_macro::TokenStream { @@ -200,11 +246,18 @@ pub fn most_traits(ts: proc_macro::TokenStream) -> proc_macro::TokenStream { for (derive, t) in derives { tokens.extend(derive(&ctx, t)) } + // Invariants prevent `FromBytes` from generating its usual supertrait + // impls, but still permit checked conversions through `TryFromBytes`. + if ctx.invariant_span.is_some() { + tokens.extend(crate::derive::try_from_bytes::derive_try_from_bytes( + &ctx, + Trait::TryFromBytes, + )); + } - // We wrap in `const_block` as a backstop in case any derive fails - // to wrap its output in `const_block` (and thus fails to annotate) - // with the full set of `#[allow(...)]` attributes). - let ts = const_block([Some(tokens)]); + // Apply generated-code lint allowances as a backstop, except around + // caller-authored invariant expressions. + let ts = ctx.const_block([Some(tokens)]); #[cfg(test)] crate::util::testutil::check_hygiene(ts.clone()); ts.into() diff --git a/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_1.expected.rs b/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_1.expected.rs index a948a149a3..ac3a51fd23 100644 --- a/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_1.expected.rs +++ b/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_1.expected.rs @@ -4801,90 +4801,73 @@ const _: () = { ___ZEROCOPY_TAG_UnitLike => true, ___ZEROCOPY_TAG_StructLike => { true - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(a) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(b) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(c) }, > () - ); - ::is_safe( - field_candidate, - ) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(d) }, > () - ); - ::is_safe( - field_candidate, - ) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(e) }, > () - ); - <[( - X, - Y, - ); N] as ::zerocopy::TryFromBytes>::is_safe(field_candidate) - } + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(a) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(b) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(c) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(d) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(e) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() } ___ZEROCOPY_TAG_TupleLike => { true - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(0) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(1) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(2) }, > () - ); - as ::zerocopy::TryFromBytes>::is_safe(field_candidate) - } + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(0) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(1) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(2) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() } _ => false, } diff --git a/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_2.expected.rs b/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_2.expected.rs index 57cc16862f..60ea279c38 100644 --- a/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_2.expected.rs +++ b/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_2.expected.rs @@ -4801,90 +4801,73 @@ const _: () = { ___ZEROCOPY_TAG_UnitLike => true, ___ZEROCOPY_TAG_StructLike => { true - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(a) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(b) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(c) }, > () - ); - ::is_safe( - field_candidate, - ) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(d) }, > () - ); - ::is_safe( - field_candidate, - ) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(e) }, > () - ); - <[( - X, - Y, - ); N] as ::zerocopy::TryFromBytes>::is_safe(field_candidate) - } + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(a) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(b) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(c) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(d) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(e) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() } ___ZEROCOPY_TAG_TupleLike => { true - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(0) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(1) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(2) }, > () - ); - as ::zerocopy::TryFromBytes>::is_safe(field_candidate) - } + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(0) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(1) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(2) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() } _ => false, } diff --git a/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_3.expected.rs b/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_3.expected.rs index 62559c7323..72676a6db4 100644 --- a/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_3.expected.rs +++ b/zerocopy/zerocopy-derive/src/output_tests/expected/try_from_bytes_enum_3.expected.rs @@ -4801,90 +4801,73 @@ const _: () = { ___ZEROCOPY_TAG_UnitLike => true, ___ZEROCOPY_TAG_StructLike => { true - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(a) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(b) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(c) }, > () - ); - ::is_safe( - field_candidate, - ) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(d) }, > () - ); - ::is_safe( - field_candidate, - ) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(StructLike) }, { - ::zerocopy::ident_id!(e) }, > () - ); - <[( - X, - Y, - ); N] as ::zerocopy::TryFromBytes>::is_safe(field_candidate) - } + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(a) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(b) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(c) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(d) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(StructLike) }, { + ::zerocopy::ident_id!(e) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() } ___ZEROCOPY_TAG_TupleLike => { true - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(0) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(1) }, > () - ); - ::is_safe(field_candidate) - } - && { - let field_candidate = ::zerocopy::into_inner!( - candidate.reborrow().project:: < - ::zerocopy::project_clients::TryFromBytesDerive, _, { - ::zerocopy::ident_id!(TupleLike) }, { - ::zerocopy::ident_id!(2) }, > () - ); - as ::zerocopy::TryFromBytes>::is_safe(field_candidate) - } + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(0) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(1) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() + && ::zerocopy::into_inner!( + candidate.project:: < + ::zerocopy::project_clients::TryFromBytesDerive, _, { + ::zerocopy::ident_id!(TupleLike) }, { + ::zerocopy::ident_id!(2) }, > () + ) + .try_into_safe::<_, ::zerocopy::BecauseImmutable>() + .is_ok() } _ => false, } diff --git a/zerocopy/zerocopy-derive/src/util.rs b/zerocopy/zerocopy-derive/src/util.rs index 23456f7086..10995386b4 100644 --- a/zerocopy/zerocopy-derive/src/util.rs +++ b/zerocopy/zerocopy-derive/src/util.rs @@ -30,6 +30,9 @@ pub(crate) struct Ctx { // The span of the last `#[zerocopy(on_error = ...)]` attribute, if any. pub(crate) on_error_span: Option, + + /// The first field invariant on the source type, if any. + pub(crate) invariant_span: Option, } #[derive(Eq, PartialEq)] @@ -152,7 +155,8 @@ impl Ctx { } } - Ok(Self { ast, zerocopy_crate: path, skip_on_error, on_error_span }) + let invariant_span = crate::invariant::validate(&ast)?; + Ok(Self { ast, zerocopy_crate: path, skip_on_error, on_error_span, invariant_span }) } pub(crate) fn with_input(&self, input: &DeriveInput) -> Self { @@ -161,6 +165,7 @@ impl Ctx { zerocopy_crate: self.zerocopy_crate.clone(), skip_on_error: self.skip_on_error, on_error_span: self.on_error_span, + invariant_span: self.invariant_span, } } @@ -174,15 +179,41 @@ impl Ctx { quote!(#zerocopy_crate::util::macro_util::core_reexport) } + /// Choose an implementation name absent from the caller's tokens. This is + /// best-effort; names introduced by nested macros are not considered. + /// Unlike local variables, type parameters are not hidden by mixed-site + /// hygiene. + pub(crate) fn fresh_ident(&self, name: &str) -> Ident { + let idents = crate::invariant::idents(self.ast.to_token_stream()); + let mut name = name.to_owned(); + while idents.iter().any(|ident| ident.to_string().trim_start_matches("r#") == name) { + name.push('_'); + } + Ident::new(&name, Span::mixed_site()) + } + + /// Caller-authored invariant expressions must inherit the caller's lint + /// policy, rather than the blanket allowances for generated glue. + pub(crate) fn const_block( + &self, + items: impl IntoIterator>, + ) -> TokenStream { + if self.invariant_span.is_none() { + const_block(items) + } else { + let items = items.into_iter().flatten(); + quote! { const _: () = { #(#items)* }; } + } + } + pub(crate) fn cfg_compile_error(&self) -> TokenStream { // By checking both during the compilation of the proc macro *and* in - // the generated code, we ensure that `--cfg - // zerocopy_unstable_linux` need only be passed *either* when + // the generated code, we ensure that each cfg need only be passed when // compiling this crate *or* when compiling the user's crate. The former // is preferable, but in some situations (such as when cross-compiling // using `cargo build --target`), it doesn't get propagated to this // crate's build by default. - if cfg!(zerocopy_unstable_linux) { + let on_error = if cfg!(zerocopy_unstable_linux) { quote!() } else if let Some(span) = self.on_error_span { let core = self.core_path(); @@ -197,7 +228,24 @@ impl Ctx { } } else { quote!() - } + }; + let invariant = if cfg!(zerocopy_unstable_ptr) { + quote!() + } else if let Some(span) = self.invariant_span { + let core = self.core_path(); + let error_message = + "`invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable"; + quote::quote_spanned! {span=> + #[allow(unused_attributes, unexpected_cfgs)] + const _: () = { + #[cfg(not(zerocopy_unstable_ptr))] + #core::compile_error!(#error_message); + }; + } + } else { + quote!() + }; + quote!(#on_error #invariant) } pub(crate) fn error_or_skip(&self, error: E) -> Result { @@ -832,7 +880,12 @@ impl<'a> ImplBlockBuilder<'a> { let outer_extras = self.outer_extras.filter(|e| !e.is_empty()); let cfg_compile_error = self.ctx.cfg_compile_error(); - const_block([Some(cfg_compile_error), Some(impl_tokens), outer_extras]) + let items = [Some(cfg_compile_error), Some(impl_tokens), outer_extras]; + if matches!(self.trt, Trait::TryFromBytes) { + self.ctx.const_block(items) + } else { + const_block(items) + } } } @@ -858,8 +911,7 @@ impl BoolExt for bool { } } -pub(crate) fn const_block(items: impl IntoIterator>) -> TokenStream { - let items = items.into_iter().flatten(); +pub(crate) fn allow_generated_code() -> TokenStream { quote! { #[allow( // FIXME(#553): Add a test that generates a warning when @@ -876,6 +928,14 @@ pub(crate) fn const_block(items: impl IntoIterator>) non_ascii_idents, clippy::missing_inline_in_public_items, )] + } +} + +pub(crate) fn const_block(items: impl IntoIterator>) -> TokenStream { + let items = items.into_iter().flatten(); + let allow = allow_generated_code(); + quote! { + #allow #[deny(ambiguous_associated_items)] // While there are not currently any warnings that this suppresses // (that we're aware of), it's good future-proofing hygiene. diff --git a/zerocopy/zerocopy-derive/tests/enum_try_from_bytes.rs b/zerocopy/zerocopy-derive/tests/enum_try_from_bytes.rs index 297da54186..1fe8d8f048 100644 --- a/zerocopy/zerocopy-derive/tests/enum_try_from_bytes.rs +++ b/zerocopy/zerocopy-derive/tests/enum_try_from_bytes.rs @@ -12,6 +12,28 @@ include!("include.rs"); +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum FieldBindings { + Named { candidate: bool, candidate_: bool, field: bool, r#type: bool }, + Tuple(bool, bool, bool, bool), +} + +#[test] +fn field_bindings() { + for tag in 0..2 { + util::test_is_safe::([tag, 0u8, 0, 0, 0], true); + util::test_is_safe::([tag, 1u8, 1, 1, 1], true); + for idx in 1..5 { + let mut bytes = [tag, 0u8, 0, 0, 0]; + bytes[idx] = 2; + util::test_is_safe::(bytes, false); + } + } + util::test_is_safe::([2u8, 0, 0, 0, 0], false); +} + #[derive(Eq, PartialEq, Debug, imp::Immutable, imp::KnownLayout, imp::TryFromBytes)] #[zerocopy(crate = "zerocopy_renamed")] #[repr(u8)] diff --git a/zerocopy/zerocopy-derive/tests/hygiene.rs b/zerocopy/zerocopy-derive/tests/hygiene.rs index 5d9dcea1de..4bbd63c113 100644 --- a/zerocopy/zerocopy-derive/tests/hygiene.rs +++ b/zerocopy/zerocopy-derive/tests/hygiene.rs @@ -132,3 +132,52 @@ mod issue_3621 { #[repr(C)] struct Outer(Inner); } + +// Field names that match constants, unit structs, or const parameters in the +// surrounding scope must not interfere with validation. +mod field_bindings { + use super::{imp, util}; + + const CONST: () = (); + const candidate_: () = (); + struct UNIT; + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Struct { + CONST: bool, + UNIT: bool, + N: bool, + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(u8)] + enum Enum { + Named { CONST: bool, UNIT: bool, N: bool }, + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + union Union { + CONST: bool, + UNIT: bool, + N: bool, + } + + #[test] + fn test_field_bindings() { + util::test_is_safe::, _>([0u8; 3], true); + util::test_is_safe::, _>([0u8; 4], true); + for idx in 0..3 { + let mut fields = [0u8; 3]; + fields[idx] = 2; + util::test_is_safe::, _>(fields, false); + util::test_is_safe::, _>([0u8, fields[0], fields[1], fields[2]], false); + } + util::test_is_safe::, _>([0u8], true); + util::test_is_safe::, _>([1u8], true); + util::test_is_safe::, _>([2u8], false); + } +} diff --git a/zerocopy/zerocopy-derive/tests/invariant.rs b/zerocopy/zerocopy-derive/tests/invariant.rs new file mode 100644 index 0000000000..e4a63ca24d --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/invariant.rs @@ -0,0 +1,691 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +// See comment in `include.rs` for why we disable the prelude. +#![no_implicit_prelude] +#![allow(dead_code)] +// The test wrapper must enable this feature; fail instead of running no tests. +#[cfg(not(zerocopy_unstable_ptr))] +compile_error!("invariant tests require --cfg zerocopy_unstable_ptr"); + +include!("include.rs"); + +use imp::Iterator as _; + +// Read through the existing pointer APIs so these tests focus on which field +// bindings are in scope, independently of conveniences for reading them. +fn read( + field: imp::Ptr<'_, imp::ReadOnly, (imp::invariant::Shared, A, imp::invariant::Safe)>, +) -> T { + field.transmute::().read() +} + +#[derive(imp::TryFromBytes, imp::KnownLayout, imp::Immutable)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C, packed)] +struct Foo { + a: u8, + #[zerocopy(invariant((read(a) % 2) == (read(b) as u8)))] + b: bool, + #[zerocopy(invariant(read(c) > 0))] + c: i16, +} + +#[test] +fn current_and_previous_fields() { + use imp::TryFromBytes as _; + + for (a, b, c, valid) in [ + (2, 0, 1i16, true), + (3, 1, 1, true), + (2, 1, 1, false), + (3, 0, 1, false), + (2, 0, 0, false), + (2, 0, -1, false), + (2, 2, 1, false), + ] { + let c = c.to_ne_bytes(); + let bytes = [a, b, c[0], c[1]]; + imp::assert_eq!(Foo::try_ref_from_bytes(&bytes).is_ok(), valid); + } +} + +mod order { + use super::{imp, read, util}; + + static CALLS: imp::core::sync::atomic::AtomicUsize = + imp::core::sync::atomic::AtomicUsize::new(0); + + fn check(expected: usize, result: bool) -> bool { + imp::assert_eq!(CALLS.fetch_add(1, imp::core::sync::atomic::Ordering::SeqCst), expected); + result + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Ordered { + #[zerocopy(invariant(check(0, read(a))))] + #[zerocopy(invariant(check(1, true)), invariant(check(2, true)))] + a: bool, + #[zerocopy(invariant(check(3, read(a) == read(b))))] + b: bool, + } + + #[test] + fn bit_validity_and_short_circuiting() { + for (bytes, valid, calls) in [ + ([1u8, 1], true, 4), + ([2, 1], false, 0), + ([0, 1], false, 1), + ([1, 2], false, 3), + ([1, 0], false, 4), + ] { + CALLS.store(0, imp::core::sync::atomic::Ordering::SeqCst); + util::test_is_safe::(bytes, valid); + imp::assert_eq!(CALLS.load(imp::core::sync::atomic::Ordering::SeqCst), calls); + } + } +} + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct EarlyReturn { + #[zerocopy(invariant(return read(a)))] + a: bool, + b: bool, +} + +#[test] +fn return_does_not_bypass_later_fields() { + util::test_is_safe::([1u8, 1], true); + util::test_is_safe::([0u8, 1], false); + util::test_is_safe::([1u8, 2], false); +} + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum Enum { + A { + a: u8, + #[zerocopy(invariant(read(a) == read(b)))] + b: u8, + }, + B { + a: u8, + #[zerocopy(invariant(read(a) != read(b)))] + b: u8, + }, + Tuple(bool, bool), + Unit, +} + +#[test] +fn selected_variant() { + for (bytes, valid) in [ + ([0u8, 5, 5], true), + ([0, 5, 6], false), + ([1, 5, 6], true), + ([1, 5, 5], false), + ([2, 1, 1], true), + ([2, 2, 1], false), + ([3, 2, 2], true), + ([4, 0, 0], false), + ] { + util::test_is_safe::(bytes, valid); + } +} + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +enum CEnum { + A { + #[zerocopy(invariant(read(a) > 0))] + a: u32, + }, + B { + #[zerocopy(invariant(read(b) == 0))] + b: u32, + }, +} + +#[test] +fn c_enum() { + util::test_is_safe::([0u32, 1], true); + util::test_is_safe::([0u32, 0], false); + util::test_is_safe::([1u32, 0], true); + util::test_is_safe::([1u32, 1], false); + util::test_is_safe::([2u32, 0], false); +} + +// These names must still bind to fields, even in the presence of outer +// functions with the same names and generated source-pointer bindings. +fn candidate_() -> u8 { + 42 +} +fn r#type() -> u8 { + 42 +} + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct Names { + candidate: u8, + #[zerocopy(invariant(read(candidate) == read(candidate_)))] + candidate_: u8, + #[zerocopy(invariant(read(r#type) == read(candidate)))] + r#type: u8, +} + +#[test] +fn field_names() { + util::test_is_safe::([1u8, 1, 1], true); + util::test_is_safe::([1u8, 2, 1], false); + util::test_is_safe::([1u8, 1, 2], false); +} + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct Generic { + a: T, + #[zerocopy(invariant(read(a) == read(b)))] + b: T, +} + +#[test] +fn generic() { + util::test_is_safe::, _>([1u8, 1], true); + util::test_is_safe::, _>([1u8, 2], false); + util::test_is_safe::, _>([1u8, 2], false); +} + +#[derive(imp::TryFromBytes, imp::KnownLayout, imp::Immutable)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct Unsized { + #[zerocopy(invariant(read(a) > 0))] + a: u8, + #[zerocopy(invariant(b.len() == read(a) as usize && b.transmute::<[bool], _, imp::BecauseImmutable>().unaligned_as_ref().iter().any(|v| *v)))] + b: [bool], +} + +#[test] +fn unsized_tail() { + use imp::TryFromBytes as _; + + imp::assert!(Unsized::try_ref_from_bytes(&[2, 0, 1]).is_ok()); + imp::assert!(Unsized::try_ref_from_bytes(&[1, 0, 1]).is_err()); + imp::assert!(Unsized::try_ref_from_bytes(&[2, 0, 0]).is_err()); + imp::assert!(Unsized::try_ref_from_bytes(&[1]).is_err()); + imp::assert!(Unsized::try_ref_from_bytes(&[0, 0, 1]).is_err()); + imp::assert!(Unsized::try_ref_from_bytes(&[1, 2, 1]).is_err()); +} + +#[derive(imp::TryFromBytes, imp::FromBytes, imp::FromZeros)] +#[zerocopy(crate = "zerocopy_renamed", on_error = "skip")] +struct SkipInfallibleDerives { + #[zerocopy(invariant(read(a) > 0))] + a: u8, +} + +util_assert_impl_all!(SkipInfallibleDerives: imp::TryFromBytes); +util_assert_not_impl_any!(SkipInfallibleDerives: imp::FromBytes, imp::FromZeros); + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed", on_error = "skip")] +enum SkipUnsupportedEnum { + A { + #[zerocopy(invariant(true))] + a: u8, + }, +} + +util_assert_not_impl_any!(SkipUnsupportedEnum: imp::TryFromBytes); + +#[test] +fn skipped_derives_preserve_invariants() { + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); +} + +#[derive(imp::most_traits)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct MostTraitsStruct { + #[zerocopy(invariant(read(a) > 0))] + a: u8, +} + +#[derive(imp::most_traits)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum MostTraitsEnum { + A { + #[zerocopy(invariant(read(a) > 0))] + a: u8, + }, +} + +#[derive(imp::most_traits)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +union MostTraitsUnion { + #[zerocopy(invariant(read(a) > 0))] + a: u8, +} + +util_assert_impl_all!(MostTraitsStruct: imp::TryFromBytes); +util_assert_impl_all!(MostTraitsEnum: imp::TryFromBytes); +util_assert_impl_all!(MostTraitsUnion: imp::TryFromBytes); +util_assert_not_impl_any!(MostTraitsStruct: imp::FromBytes, imp::FromZeros); +util_assert_not_impl_any!(MostTraitsEnum: imp::FromBytes, imp::FromZeros); +util_assert_not_impl_any!(MostTraitsUnion: imp::FromBytes, imp::FromZeros); + +#[test] +fn most_traits_preserves_invariants() { + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); + util::test_is_safe::([0u8, 1], true); + util::test_is_safe::([0u8, 0], false); + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); +} + +mod generated_names { + use super::{imp, read, util}; + + fn candidate() -> bool { + true + } + + fn candidate_() -> bool { + true + } + + fn tag() -> bool { + true + } + + struct ___ZcAlignment; + + impl ___ZcAlignment { + fn check() -> bool { + true + } + } + + #[allow(non_snake_case)] + fn ___ZEROCOPY_TAG_A() -> bool { + true + } + + struct ___ZerocopyTag; + + impl ___ZerocopyTag { + fn check() -> bool { + true + } + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Struct { + #[zerocopy(invariant(candidate() && candidate_() && tag() && ___ZcAlignment::check() && ___ZEROCOPY_TAG_A() && ___ZerocopyTag::check() && read(a) > 0))] + a: u8, + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(u8)] + enum Enum { + A { + #[zerocopy(invariant(candidate() && candidate_() && tag() && ___ZcAlignment::check() && ___ZEROCOPY_TAG_A() && ___ZerocopyTag::check() && read(a) > 0))] + a: u8, + }, + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + union Union { + #[zerocopy(invariant(candidate() && candidate_() && tag() && ___ZcAlignment::check() && ___ZEROCOPY_TAG_A() && ___ZerocopyTag::check() && read(a) > 0))] + a: u8, + } + + #[test] + fn helpers_are_not_shadowed() { + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); + util::test_is_safe::([0u8, 1], true); + util::test_is_safe::([0u8, 0], false); + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); + } +} + +mod unions { + use super::{imp, read, util}; + + static CALLS: imp::core::sync::atomic::AtomicUsize = + imp::core::sync::atomic::AtomicUsize::new(0); + + fn record(mark: usize, result: bool) -> bool { + let order = imp::core::sync::atomic::Ordering::SeqCst; + CALLS.store(CALLS.load(order) * 10 + mark, order); + result + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + union Ordered { + #[zerocopy(invariant(record(1, read(a))))] + #[zerocopy(invariant(record(2, true)))] + a: bool, + #[zerocopy(invariant(record(3, read(b) == 2)))] + b: u8, + } + + #[test] + fn field_validity_invariants_and_fallback() { + for (byte, valid, calls) in [(0u8, false, 13), (1, true, 12), (2, true, 3), (3, false, 3)] { + CALLS.store(0, imp::core::sync::atomic::Ordering::SeqCst); + util::test_is_safe::([byte], valid); + imp::assert_eq!(CALLS.load(imp::core::sync::atomic::Ordering::SeqCst), calls); + } + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + union EarlyReturn { + #[zerocopy(invariant(return true))] + #[zerocopy(invariant(false))] + a: u8, + #[zerocopy(invariant(return read(b) == 7))] + b: u8, + } + + #[test] + fn return_is_local_to_each_hook() { + // The first hook's `return true` must not bypass its field's second + // hook. Rejecting that field must still allow the second field to pass. + util::test_is_safe::([0u8], false); + util::test_is_safe::([7u8], true); + } + + fn candidate_() -> u8 { + 42 + } + fn r#type() -> u8 { + 42 + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + union Names { + #[zerocopy(invariant(read(candidate) == 1))] + candidate: u8, + #[zerocopy(invariant(read(candidate_) == 2))] + candidate_: u8, + #[zerocopy(invariant(read(r#type) == 3))] + r#type: u8, + } + + #[test] + fn field_names() { + for byte in 0u8..=4 { + util::test_is_safe::([byte], (1..=3).contains(&byte)); + } + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + union Generic { + #[zerocopy(invariant(read(a) == T::default()))] + a: T, + } + + #[test] + fn generic() { + util::test_is_safe::, _>([0u8], true); + util::test_is_safe::, _>([1u8], false); + util::test_is_safe::, _>([2u8], false); + util::test_is_safe::, _>([0u8], true); + util::test_is_safe::, _>([1u8], false); + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + union InteriorMutable { + #[zerocopy(invariant(true))] + a: imp::ManuallyDrop>, + } + + #[test] + fn field_need_not_be_immutable() { + util::test_is_safe::([0u8], true); + util::test_is_safe::([1u8], true); + util::test_is_safe::([2u8], false); + } +} + +mod future_fields { + use super::{imp, read, util}; + + fn require_nonzero(ok: bool) { + imp::assert!(ok); + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Struct { + #[zerocopy(invariant({ require_nonzero(read(a) != 0); true }))] + a: u8, + require_nonzero: u8, + } + + #[test] + fn later_field_does_not_shadow_helper() { + util::test_is_safe::([1u8, 0], true); + use imp::TryFromBytes as _; + imp::assert!(::std::panic::catch_unwind(|| { + Struct::try_read_from_bytes(&[0, 0]).is_ok() + }) + .is_err()); + } + + // Field bindings must not introduce item aliases that capture helper calls + // inside macros. + macro_rules! checked_nonzero { + ($field:expr) => {{ + require_nonzero(read($field) != 0); + true + }}; + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct MacroHelper { + require_nonzero: u8, + #[zerocopy(invariant(checked_nonzero!(a)))] + a: u8, + } + + #[test] + fn earlier_field_does_not_shadow_macro_helper() { + util::test_is_safe::([0u8, 1], true); + use imp::TryFromBytes as _; + imp::assert!(::std::panic::catch_unwind(|| { + MacroHelper::try_read_from_bytes(&[0, 0]).is_ok() + }) + .is_err()); + } +} + +#[cfg(__ZEROCOPY_INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS)] +mod mixed_contexts { + #[allow(unused_imports)] // The host macro omits this fixture on cross builds. + use super::{imp, util}; + + // The field token in the invariant must select the validated pointer, + // even though the declaration has a different hygiene context. + fn field() -> &'static imp::ReadOnly { + static VALUE: imp::ReadOnly = imp::ReadOnly::new(1); + &VALUE + } + + trait ReadHelper { + fn unaligned_as_ref(self) -> &'static imp::ReadOnly; + } + + impl &'static imp::ReadOnly> ReadHelper for F { + fn unaligned_as_ref(self) -> &'static imp::ReadOnly { + self() + } + } + + ::zerocopy_derive::__test_hygienically_mixed_invariant! { + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Mixed { + #[zerocopy(invariant(**CallField.unaligned_as_ref() > 0))] + DefField: u8, + } + + #[test] + fn field_binding_uses_semantic_name() { + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); + } + } +} + +mod coverage { + use imp::TryFromBytes as _; + + use super::{imp, read, util}; + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + struct Panics { + #[zerocopy(invariant(imp::core::panic!("invariant panic")))] + a: u8, + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + union UnionPanics { + #[zerocopy(invariant(imp::core::panic!("invariant panic")))] + a: u8, + #[zerocopy(invariant(imp::core::panic!("fallback must not run")))] + b: u8, + c: u8, + } + + #[test] + fn panics_propagate_without_union_fallback() { + for result in [ + ::std::panic::catch_unwind(|| util::test_is_safe::([0u8], true)), + ::std::panic::catch_unwind(|| util::test_is_safe::([0u8], true)), + ] { + let panic = result.unwrap_err(); + imp::assert_eq!(panic.downcast_ref::<&str>(), imp::Some(&"invariant panic")); + } + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Inner { + #[zerocopy(invariant(read(a) != 0))] + a: u8, + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Outer { + #[zerocopy(invariant(imp::core::panic!("outer invariant")))] + a: Inner, + b: Panics, + } + + #[test] + fn nested_invariant_failure_short_circuits() { + util::test_is_safe::([0u8, 0], false); + let panic = ::std::panic::catch_unwind(|| util::test_is_safe::([1u8, 0], true)) + .unwrap_err(); + imp::assert_eq!(panic.downcast_ref::<&str>(), imp::Some(&"outer invariant")); + } + + #[derive(imp::TryFromBytes, imp::IntoBytes, imp::KnownLayout, imp::Immutable)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Mutable { + #[zerocopy(invariant(read(a) != 0))] + a: u8, + } + + #[test] + fn conversions_check_invariants_but_mutation_is_unrestricted() { + imp::assert!(Mutable::try_mut_from_bytes(&mut [0]).is_err()); + let mut bytes = [1]; + let value = Mutable::try_mut_from_bytes(&mut bytes).unwrap(); + value.a = 0; + imp::assert_eq!(bytes, [0]); + imp::assert!(Mutable::try_ref_from_bytes(&bytes).is_err()); + imp::assert!(Mutable::try_read_from_bytes(&bytes).is_err()); + } + + #[derive(imp::TryFromBytes)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct Overlapping { + #[zerocopy(invariant({ read(a); true }))] + a: (), + #[zerocopy(invariant({ read(a); read(b) != 0 }))] + b: u8, + } + + #[test] + fn overlapping_field_projections() { + util::test_is_safe::([1u8], true); + util::test_is_safe::([0u8], false); + } + + static CALLS: imp::core::sync::atomic::AtomicUsize = + imp::core::sync::atomic::AtomicUsize::new(0); + + #[derive(imp::TryFromBytes, imp::KnownLayout, imp::Immutable)] + #[zerocopy(crate = "zerocopy_renamed")] + #[repr(C)] + struct SideEffect { + #[zerocopy(invariant({ CALLS.fetch_add(1, imp::core::sync::atomic::Ordering::SeqCst); read(a) != 0 }))] + a: u8, + } + + #[test] + fn discarded_conversions_have_side_effects() { + for byte in [0, 1] { + let _ = SideEffect::try_ref_from_bytes(&[byte]); + let _ = SideEffect::try_read_from_bytes(&[byte]); + } + imp::assert_eq!(CALLS.load(imp::core::sync::atomic::Ordering::SeqCst), 4); + } +} diff --git a/zerocopy/zerocopy-derive/tests/struct_try_from_bytes.rs b/zerocopy/zerocopy-derive/tests/struct_try_from_bytes.rs index bdbf976fb6..ccffd73a92 100644 --- a/zerocopy/zerocopy-derive/tests/struct_try_from_bytes.rs +++ b/zerocopy/zerocopy-derive/tests/struct_try_from_bytes.rs @@ -52,6 +52,37 @@ fn two() { crate::util::test_is_safe::([2u8], false); } +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct FieldBindings { + candidate: bool, + candidate_: bool, + field: bool, + r#type: bool, +} + +#[derive(imp::TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +struct TupleFields(bool, bool, bool, bool); + +#[test] +fn field_bindings() { + // User field names must not shadow the source pointer or generated + // temporaries, and tuple indices must produce valid local bindings. + for bytes in [[0u8; 4], [1u8; 4]] { + util::test_is_safe::(bytes, true); + util::test_is_safe::(bytes, true); + } + for idx in 0..4 { + let mut bytes = [0u8; 4]; + bytes[idx] = 2; + util::test_is_safe::(bytes, false); + util::test_is_safe::(bytes, false); + } +} + #[derive(imp::KnownLayout, imp::TryFromBytes)] #[zerocopy(crate = "zerocopy_renamed")] #[repr(C)] diff --git a/zerocopy/zerocopy-derive/tests/ui.rs b/zerocopy/zerocopy-derive/tests/ui.rs index 35a4ce92a8..e3ae221b9a 100644 --- a/zerocopy/zerocopy-derive/tests/ui.rs +++ b/zerocopy/zerocopy-derive/tests/ui.rs @@ -22,11 +22,11 @@ use testutil::UiTestRunner; ignore )] fn ui() { - // This tests the behavior when `--cfg zerocopy_derive_union_into_bytes` is - // present. + // This tests the behavior when experimental features are enabled. UiTestRunner::new() .rustc_arg("--cfg=zerocopy_derive_union_into_bytes") .rustc_arg("--cfg=zerocopy_unstable_linux") + .rustc_arg("--cfg=zerocopy_unstable_ptr") .rustc_arg("-Wwarnings") // To ensure .stderr files reflect typical user encounter .run(); diff --git a/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.msrv.stderr new file mode 100644 index 0000000000..417b74eede --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.msrv.stderr @@ -0,0 +1,58 @@ +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:13:10 + | +13 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:21:10 + | +21 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable + --> $DIR/invariant.rs:33:10 + | +33 | #[derive(FromBytes)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:33:10 + | +33 | #[derive(FromBytes)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:42:10 + | +42 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable + --> $DIR/invariant.rs:50:10 + | +50 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:50:10 + | +50 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 7 previous errors + diff --git a/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.nightly.stderr new file mode 100644 index 0000000000..417b74eede --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.nightly.stderr @@ -0,0 +1,58 @@ +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:13:10 + | +13 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:21:10 + | +21 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable + --> $DIR/invariant.rs:33:10 + | +33 | #[derive(FromBytes)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:33:10 + | +33 | #[derive(FromBytes)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:42:10 + | +42 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable + --> $DIR/invariant.rs:50:10 + | +50 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:50:10 + | +50 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 7 previous errors + diff --git a/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.rs b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.rs new file mode 100644 index 0000000000..155379296f --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.rs @@ -0,0 +1,61 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +extern crate zerocopy_renamed; + +use zerocopy_renamed::{FromBytes, TryFromBytes}; + +#[derive(TryFromBytes)] +//~^ ERROR: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable +#[zerocopy(crate = "zerocopy_renamed")] +struct Struct { + #[zerocopy(invariant(true))] + a: u8, +} + +#[derive(TryFromBytes)] +//~^ ERROR: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum Enum { + A { + #[zerocopy(invariant(true))] + a: u8, + }, +} + +// Skipping an unsupported derive must still require the experimental cfgs. +#[derive(FromBytes)] +//~^ ERROR: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable +//~^^ ERROR: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable +#[zerocopy(crate = "zerocopy_renamed", on_error = "skip")] +struct Skipped { + #[zerocopy(invariant(true))] + a: u8, +} + +#[derive(TryFromBytes)] +//~^ ERROR: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable +#[zerocopy(crate = "zerocopy_renamed")] +union Union { + #[zerocopy(invariant(true))] + a: u8, +} + +#[derive(TryFromBytes)] +//~^ ERROR: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable +//~^^ ERROR: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable +#[zerocopy(crate = "zerocopy_renamed", on_error = "skip")] +enum SkippedEnum { + A { + #[zerocopy(invariant(true))] + a: u8, + }, +} + +fn main() {} diff --git a/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.stable.stderr new file mode 100644 index 0000000000..417b74eede --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/cfgs/invariant.stable.stderr @@ -0,0 +1,58 @@ +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:13:10 + | +13 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:21:10 + | +21 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable + --> $DIR/invariant.rs:33:10 + | +33 | #[derive(FromBytes)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:33:10 + | +33 | #[derive(FromBytes)] + | ^^^^^^^^^ + | + = note: this error originates in the derive macro `FromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:42:10 + | +42 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `on_error` is experimental; pass '--cfg zerocopy_unstable_linux' to enable + --> $DIR/invariant.rs:50:10 + | +50 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: `invariant` is experimental; pass '--cfg zerocopy_unstable_ptr' to enable + --> $DIR/invariant.rs:50:10 + | +50 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 7 previous errors + diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant.msrv.stderr new file mode 100644 index 0000000000..3a971f7c37 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant.msrv.stderr @@ -0,0 +1,81 @@ +error: invariants are only supported on named fields + --> $DIR/invariant.rs:16:26 + | +16 | #[zerocopy(invariant(true))] + | ^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:26:30 + | +26 | #[zerocopy(invariant(true))] + | ^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:36:26 + | +36 | #[zerocopy(invariant(true))] + | ^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:44:16 + | +44 | #[zerocopy(invariant)] + | ^^^^^^^^^ + +error: unexpected end of input, expected an expression + --> $DIR/invariant.rs:52:26 + | +52 | #[zerocopy(invariant())] + | ^ + +error: unexpected token + --> $DIR/invariant.rs:60:30 + | +60 | #[zerocopy(invariant(true, false))] + | ^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:68:16 + | +68 | #[zerocopy(invaraint(true))] + | ^^^^^^^^^^^^^^^ + +error: cannot derive `FromZeros` for a type with invariants + --> $DIR/invariant.rs:84:26 + | +84 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ + +error: cannot derive `FromBytes` for a type with invariants + --> $DIR/invariant.rs:92:26 + | +92 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:102:26 + | +102 | #[zerocopy(invariant(true))] + | ^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:110:16 + | +110 | #[zerocopy(invariant[true])] + | ^^^^^^^^^^^^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:118:16 + | +118 | #[zerocopy(invariant{true})] + | ^^^^^^^^^^^^^^^ + +error[E0308]: mismatched types + --> $DIR/invariant.rs:76:26 + | +76 | #[zerocopy(invariant(0u8))] + | ^^^ expected `bool`, found `u8` + +error: aborting due to 13 previous errors + +For more information about this error, try `rustc --explain E0308`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant.nightly.stderr new file mode 100644 index 0000000000..95e87048f5 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant.nightly.stderr @@ -0,0 +1,84 @@ +error: invariants are only supported on named fields + --> $DIR/invariant.rs:16:26 + | +16 | #[zerocopy(invariant(true))] + | ^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:26:30 + | +26 | #[zerocopy(invariant(true))] + | ^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:36:26 + | +36 | #[zerocopy(invariant(true))] + | ^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:44:16 + | +44 | #[zerocopy(invariant)] + | ^^^^^^^^^ + +error: unexpected end of input, expected an expression + --> $DIR/invariant.rs:52:26 + | +52 | #[zerocopy(invariant())] + | ^ + +error: unexpected token + --> $DIR/invariant.rs:60:30 + | +60 | #[zerocopy(invariant(true, false))] + | ^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:68:16 + | +68 | #[zerocopy(invaraint(true))] + | ^^^^^^^^^^^^^^^ + +error: cannot derive `FromZeros` for a type with invariants + --> $DIR/invariant.rs:84:26 + | +84 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: cannot derive `FromBytes` for a type with invariants + --> $DIR/invariant.rs:92:26 + | +92 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:102:26 + | +102 | #[zerocopy(invariant(true))] + | ^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:110:16 + | +110 | #[zerocopy(invariant[true])] + | ^^^^^^^^^^^^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:118:16 + | +118 | #[zerocopy(invariant{true})] + | ^^^^^^^^^^^^^^^ + +error[E0308]: mismatched types + --> $DIR/invariant.rs:76:26 + | +73 | #[derive(TryFromBytes)] + | ------------ expected `bool` because of return type +... +76 | #[zerocopy(invariant(0u8))] + | ^^^ expected `bool`, found `u8` + +error: aborting due to 13 previous errors + +For more information about this error, try `rustc --explain E0308`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant.rs b/zerocopy/zerocopy-derive/tests/ui/invariant.rs new file mode 100644 index 0000000000..d287318776 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant.rs @@ -0,0 +1,123 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +extern crate zerocopy_renamed; + +use zerocopy_renamed::{FromBytes, FromZeros, TryFromBytes}; + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct Tuple( + #[zerocopy(invariant(true))] + //~[msrv, stable, nightly]^ ERROR: invariants are only supported on named fields + u8, +); + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum TupleVariant { + A( + #[zerocopy(invariant(true))] + //~[msrv, stable, nightly]^ ERROR: invariants are only supported on named fields + u8, + ), +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum VariantAttribute { + #[zerocopy(invariant(true))] + //~[msrv, stable, nightly]^ ERROR: invariants are only supported on named fields + A, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct BareAttribute { + #[zerocopy(invariant)] + //~[msrv, stable, nightly]^ ERROR: expected `invariant(...)` + a: u8, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct MissingExpression { + #[zerocopy(invariant())] + //~[msrv, stable, nightly]^ ERROR: unexpected end of input, expected an expression + a: u8, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct ExtraExpression { + #[zerocopy(invariant(true, false))] + //~[msrv, stable, nightly]^ ERROR: unexpected token + a: u8, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct MisspelledAttribute { + #[zerocopy(invaraint(true))] + //~[msrv, stable, nightly]^ ERROR: expected `invariant(...)` + a: u8, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct NonBoolean { + #[zerocopy(invariant(0u8))] + //~[msrv, stable, nightly]^ ERROR: mismatched types + a: u8, +} + +#[derive(FromZeros)] +#[zerocopy(crate = "zerocopy_renamed")] +struct Zeroable { + #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + //~[msrv, stable, nightly]^ ERROR: cannot derive `FromZeros` for a type with invariants + a: u8, +} + +#[derive(FromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct Infallible { + #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + //~[msrv, stable, nightly]^ ERROR: cannot derive `FromBytes` for a type with invariants + a: u8, +} + +// Attribute errors must not be silently ignored by `on_error = "skip"`, or +// bypassed by the trivial `FromBytes` implementation of `is_safe`. +#[derive(FromBytes)] +#[zerocopy(crate = "zerocopy_renamed", on_error = "skip")] +struct SkippedTuple( + #[zerocopy(invariant(true))] + //~[msrv, stable, nightly]^ ERROR: invariants are only supported on named fields + u8, +); + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct Brackets { + #[zerocopy(invariant[true])] + //~[msrv, stable, nightly]^ ERROR: expected `invariant(...)` + a: u8, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct Braces { + #[zerocopy(invariant{true})] + //~[msrv, stable, nightly]^ ERROR: expected `invariant(...)` + a: u8, +} + +fn main() {} diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant.stable.stderr new file mode 100644 index 0000000000..e9d05aa314 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant.stable.stderr @@ -0,0 +1,84 @@ +error: invariants are only supported on named fields + --> $DIR/invariant.rs:16:26 + | +16 | #[zerocopy(invariant(true))] + | ^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:26:30 + | +26 | #[zerocopy(invariant(true))] + | ^^^^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:36:26 + | +36 | #[zerocopy(invariant(true))] + | ^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:44:16 + | +44 | #[zerocopy(invariant)] + | ^^^^^^^^^ + +error: unexpected end of input, expected an expression + --> $DIR/invariant.rs:52:26 + | +52 | #[zerocopy(invariant())] + | ^ + +error: unexpected token + --> $DIR/invariant.rs:60:30 + | +60 | #[zerocopy(invariant(true, false))] + | ^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:68:16 + | +68 | #[zerocopy(invaraint(true))] + | ^^^^^^^^^^^^^^^ + +error: cannot derive `FromZeros` for a type with invariants + --> $DIR/invariant.rs:84:26 + | +84 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ + +error: cannot derive `FromBytes` for a type with invariants + --> $DIR/invariant.rs:92:26 + | +92 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ + +error: invariants are only supported on named fields + --> $DIR/invariant.rs:102:26 + | +102 | #[zerocopy(invariant(true))] + | ^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:110:16 + | +110 | #[zerocopy(invariant[true])] + | ^^^^^^^^^^^^^^^ + +error: expected `invariant(...)` + --> $DIR/invariant.rs:118:16 + | +118 | #[zerocopy(invariant{true})] + | ^^^^^^^^^^^^^^^ + +error[E0308]: mismatched types + --> $DIR/invariant.rs:76:26 + | +73 | #[derive(TryFromBytes)] + | ------------ expected `bool` because of return type +... +76 | #[zerocopy(invariant(0u8))] + | ^^^ expected `bool`, found `u8` + +error: aborting due to 13 previous errors + +For more information about this error, try `rustc --explain E0308`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.msrv.stderr new file mode 100644 index 0000000000..d791a7c824 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.msrv.stderr @@ -0,0 +1,9 @@ +error[E0425]: cannot find value `b` in this scope + --> $DIR/invariant_field_scope.rs:16:28 + | +16 | #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + | ^ a field by this name exists in `Self` + +error: aborting due to previous error + +For more information about this error, try `rustc --explain E0425`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.nightly.stderr new file mode 100644 index 0000000000..1b60662dfb --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.nightly.stderr @@ -0,0 +1,9 @@ +error[E0425]: cannot find value `b` in this scope + --> $DIR/invariant_field_scope.rs:16:28 + | +16 | #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0425`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.rs b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.rs new file mode 100644 index 0000000000..6cf236f9ba --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.rs @@ -0,0 +1,22 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +extern crate zerocopy_renamed; + +use zerocopy_renamed::TryFromBytes; + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct LaterField { + #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + //~[msrv, stable, nightly]^ ERROR: cannot find value `b` in this scope + a: u8, + b: u8, +} + +fn main() {} diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.stable.stderr new file mode 100644 index 0000000000..1b60662dfb --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_field_scope.stable.stderr @@ -0,0 +1,9 @@ +error[E0425]: cannot find value `b` in this scope + --> $DIR/invariant_field_scope.rs:16:28 + | +16 | #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error: aborting due to 1 previous error + +For more information about this error, try `rustc --explain E0425`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_lints.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.msrv.stderr new file mode 100644 index 0000000000..b5f1d18cb4 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.msrv.stderr @@ -0,0 +1,26 @@ +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:23:26 + | +23 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + | +note: the lint level is defined here + --> $DIR/invariant_lints.rs:9:9 + | +9 | #![deny(deprecated)] + | ^^^^^^^^^^ + +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:33:30 + | +33 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:43:26 + | +43 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + +error: aborting due to 3 previous errors + diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_lints.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.nightly.stderr new file mode 100644 index 0000000000..5517891dfa --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.nightly.stderr @@ -0,0 +1,52 @@ +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:23:26 + | +23 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + | +note: the lint level is defined here + --> $DIR/invariant_lints.rs:9:9 + | + 9 | #![deny(deprecated)] + | ^^^^^^^^^^ + +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:33:30 + | +33 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:43:26 + | +43 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + +warning: function `old_check` is never used + --> $DIR/invariant_lints.rs:16:4 + | +16 | fn old_check() -> bool { + | ^^^^^^^^^ + | + = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default + +warning: struct `Struct` is never constructed + --> $DIR/invariant_lints.rs:22:8 + | +22 | struct Struct { + | ^^^^^^ + +warning: enum `Enum` is never used + --> $DIR/invariant_lints.rs:31:6 + | +31 | enum Enum { + | ^^^^ + +warning: union `Union` is never used + --> $DIR/invariant_lints.rs:42:7 + | +42 | union Union { + | ^^^^^ + +error: aborting due to 3 previous errors; 4 warnings emitted + diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_lints.rs b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.rs new file mode 100644 index 0000000000..c614c9088d --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.rs @@ -0,0 +1,48 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +#![deny(deprecated)] + +extern crate zerocopy_derive; +extern crate zerocopy_renamed; +use zerocopy_renamed::TryFromBytes; + +#[deprecated] +fn old_check() -> bool { + true +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +struct Struct { + #[zerocopy(invariant(old_check()))] + //~[msrv, stable, nightly]^ ERROR: use of deprecated function `old_check` + a: u8, +} + +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(u8)] +enum Enum { + A { + #[zerocopy(invariant(old_check()))] + //~[msrv, stable, nightly]^ ERROR: use of deprecated function `old_check` + a: u8, + }, +} + +#[derive(zerocopy_derive::most_traits)] +#[zerocopy(crate = "zerocopy_renamed")] +#[repr(C)] +union Union { + #[zerocopy(invariant(old_check()))] + //~[msrv, stable, nightly]^ ERROR: use of deprecated function `old_check` + a: u8, +} + +fn main() {} diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_lints.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.stable.stderr new file mode 100644 index 0000000000..5517891dfa --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_lints.stable.stderr @@ -0,0 +1,52 @@ +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:23:26 + | +23 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + | +note: the lint level is defined here + --> $DIR/invariant_lints.rs:9:9 + | + 9 | #![deny(deprecated)] + | ^^^^^^^^^^ + +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:33:30 + | +33 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + +error: use of deprecated function `old_check` + --> $DIR/invariant_lints.rs:43:26 + | +43 | #[zerocopy(invariant(old_check()))] + | ^^^^^^^^^ + +warning: function `old_check` is never used + --> $DIR/invariant_lints.rs:16:4 + | +16 | fn old_check() -> bool { + | ^^^^^^^^^ + | + = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default + +warning: struct `Struct` is never constructed + --> $DIR/invariant_lints.rs:22:8 + | +22 | struct Struct { + | ^^^^^^ + +warning: enum `Enum` is never used + --> $DIR/invariant_lints.rs:31:6 + | +31 | enum Enum { + | ^^^^ + +warning: union `Union` is never used + --> $DIR/invariant_lints.rs:42:7 + | +42 | union Union { + | ^^^^^ + +error: aborting due to 3 previous errors; 4 warnings emitted + diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.msrv.stderr new file mode 100644 index 0000000000..7026e51805 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.msrv.stderr @@ -0,0 +1,36 @@ +error[E0530]: let bindings cannot shadow constants + --> $DIR/invariant_name_collisions.rs:17:10 + | +13 | const CONST: () = (); + | --------------------- the constant `CONST` is defined here +... +17 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a constant + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0530]: let bindings cannot shadow unit structs + --> $DIR/invariant_name_collisions.rs:25:10 + | +14 | struct Unit; + | ------------ the unit struct `Unit` is defined here +... +25 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a unit struct + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0530]: let bindings cannot shadow const parameters + --> $DIR/invariant_name_collisions.rs:33:10 + | +33 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a const parameter +... +36 | struct ConstParam { + | - the const parameter `N` is defined here + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0530`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.nightly.stderr new file mode 100644 index 0000000000..7026e51805 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.nightly.stderr @@ -0,0 +1,36 @@ +error[E0530]: let bindings cannot shadow constants + --> $DIR/invariant_name_collisions.rs:17:10 + | +13 | const CONST: () = (); + | --------------------- the constant `CONST` is defined here +... +17 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a constant + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0530]: let bindings cannot shadow unit structs + --> $DIR/invariant_name_collisions.rs:25:10 + | +14 | struct Unit; + | ------------ the unit struct `Unit` is defined here +... +25 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a unit struct + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0530]: let bindings cannot shadow const parameters + --> $DIR/invariant_name_collisions.rs:33:10 + | +33 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a const parameter +... +36 | struct ConstParam { + | - the const parameter `N` is defined here + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0530`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.rs b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.rs new file mode 100644 index 0000000000..3bb0d79ae4 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.rs @@ -0,0 +1,41 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +extern crate zerocopy_renamed; + +use zerocopy_renamed::TryFromBytes; + +const CONST: () = (); +struct Unit; + +// Collisions must be rejected, not hidden by generated callable items. +#[derive(TryFromBytes)] +//~[msrv, stable, nightly]^ ERROR: let bindings cannot shadow constants +#[zerocopy(crate = "zerocopy_renamed")] +struct Constant { + #[zerocopy(invariant(true))] + CONST: u8, +} + +#[derive(TryFromBytes)] +//~[msrv, stable, nightly]^ ERROR: let bindings cannot shadow unit structs +#[zerocopy(crate = "zerocopy_renamed")] +struct Constructor { + #[zerocopy(invariant(true))] + Unit: u8, +} + +#[derive(TryFromBytes)] +//~[msrv, stable, nightly]^ ERROR: let bindings cannot shadow const parameters +#[zerocopy(crate = "zerocopy_renamed")] +struct ConstParam { + #[zerocopy(invariant(true))] + N: u8, +} + +fn main() {} diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.stable.stderr new file mode 100644 index 0000000000..7026e51805 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_name_collisions.stable.stderr @@ -0,0 +1,36 @@ +error[E0530]: let bindings cannot shadow constants + --> $DIR/invariant_name_collisions.rs:17:10 + | +13 | const CONST: () = (); + | --------------------- the constant `CONST` is defined here +... +17 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a constant + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0530]: let bindings cannot shadow unit structs + --> $DIR/invariant_name_collisions.rs:25:10 + | +14 | struct Unit; + | ------------ the unit struct `Unit` is defined here +... +25 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a unit struct + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0530]: let bindings cannot shadow const parameters + --> $DIR/invariant_name_collisions.rs:33:10 + | +33 | #[derive(TryFromBytes)] + | ^^^^^^^^^^^^ cannot be named the same as a const parameter +... +36 | struct ConstParam { + | - the const parameter `N` is defined here + | + = note: this error originates in the derive macro `TryFromBytes` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: aborting due to 3 previous errors + +For more information about this error, try `rustc --explain E0530`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.msrv.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.msrv.stderr new file mode 100644 index 0000000000..34b667080b --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.msrv.stderr @@ -0,0 +1,15 @@ +error[E0425]: cannot find value `b` in this scope + --> $DIR/invariant_union_scope.rs:18:28 + | +18 | #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error[E0425]: cannot find value `a` in this scope + --> $DIR/invariant_union_scope.rs:21:28 + | +21 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0425`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.nightly.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.nightly.stderr new file mode 100644 index 0000000000..34b667080b --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.nightly.stderr @@ -0,0 +1,15 @@ +error[E0425]: cannot find value `b` in this scope + --> $DIR/invariant_union_scope.rs:18:28 + | +18 | #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error[E0425]: cannot find value `a` in this scope + --> $DIR/invariant_union_scope.rs:21:28 + | +21 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0425`. diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.rs b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.rs new file mode 100644 index 0000000000..a307078993 --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.rs @@ -0,0 +1,26 @@ +// Copyright 2026 The Fuchsia Authors +// +// Licensed under a BSD-style license , Apache License, Version 2.0 +// , or the MIT +// license , at your option. +// This file may not be copied, modified, or distributed except according to +// those terms. + +extern crate zerocopy_renamed; + +use zerocopy_renamed::TryFromBytes; + +// Keep name-resolution errors separate from macro-expansion errors, which +// can prevent rustc from reporting these diagnostics. +#[derive(TryFromBytes)] +#[zerocopy(crate = "zerocopy_renamed")] +union Union { + #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + //~[msrv, stable, nightly]^ ERROR: cannot find value `b` in this scope + a: u8, + #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + //~[msrv, stable, nightly]^ ERROR: cannot find value `a` in this scope + b: u8, +} + +fn main() {} diff --git a/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.stable.stderr b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.stable.stderr new file mode 100644 index 0000000000..34b667080b --- /dev/null +++ b/zerocopy/zerocopy-derive/tests/ui/invariant_union_scope.stable.stderr @@ -0,0 +1,15 @@ +error[E0425]: cannot find value `b` in this scope + --> $DIR/invariant_union_scope.rs:18:28 + | +18 | #[zerocopy(invariant(**b.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error[E0425]: cannot find value `a` in this scope + --> $DIR/invariant_union_scope.rs:21:28 + | +21 | #[zerocopy(invariant(**a.unaligned_as_ref() > 0))] + | ^ not found in this scope + +error: aborting due to 2 previous errors + +For more information about this error, try `rustc --explain E0425`.