| //! Codegen for the reworked parsing framework. |
| //! |
| //! Emits into a parallel tree (`read_fonts::exp::tables`) rather than replacing |
| //! the existing output, so this can be developed against real tables without |
| //! breaking anything. See `docs/parsing-rework.md`. |
| //! |
| //! Only the read side is emitted: no compile side, and no traversal. |
| |
| use std::collections::HashSet; |
| |
| use proc_macro2::TokenStream; |
| use quote::{quote, ToTokens}; |
| |
| use crate::{ |
| flags_enums, |
| parsing::{ |
| Condition, Count, CountArg, Field, FieldType, GenericGroup, Item, Items, OffsetTarget, |
| Record, Table, TableFormat, |
| }, |
| }; |
| |
| pub(crate) fn generate_module(items: &Items) -> Result<TokenStream, syn::Error> { |
| // records that hold an offset are handed out paired with their base; the |
| // rest stay plain slices. Which one a field is depends on the record, so |
| // the set has to be known before any field is emitted. |
| let with_parent: HashSet<syn::Ident> = items |
| .iter() |
| .filter_map(|item| match item { |
| Item::Record(rec) if record_shape(rec) == RecordShape::ZerocopyWithParent => { |
| Some(rec.name.clone()) |
| } |
| _ => None, |
| }) |
| .collect(); |
| |
| let mut code = Vec::new(); |
| for item in items.iter() { |
| let item_code = match item { |
| Item::Table(item) => generate_table(item, &with_parent)?, |
| Item::Record(item) => generate_record(item, &with_parent)?, |
| Item::Format(item) => generate_format_group(item)?, |
| // flags and raw enums are pure scalar types, independent of how |
| // tables are read, so the existing emission is reused verbatim |
| Item::RawEnum(item) => flags_enums::generate_raw_enum(item), |
| Item::Flags(item) => flags_enums::generate_flags(item), |
| Item::GenericGroup(item) => generate_generic_group(item)?, |
| Item::Extern(..) => Default::default(), |
| }; |
| code.push(item_code); |
| } |
| |
| Ok(quote! { |
| #[allow(unused_imports)] |
| use crate::exp::prelude::*; |
| #(#code)* |
| }) |
| } |
| |
| /// Where a field's bytes are measured from. |
| /// |
| /// A table's fields are located from byte zero of its own data. A computed |
| /// record's are located from its position within the parent, which is the only |
| /// difference between the two emissions. |
| #[derive(Clone, Copy)] |
| struct Base { |
| /// The expression for the enclosing data. |
| data: fn() -> TokenStream, |
| /// The expression for the first field's start. |
| start: fn() -> TokenStream, |
| } |
| |
| const TABLE_BASE: Base = Base { |
| data: || quote!(self.data), |
| start: || quote!(0), |
| }; |
| |
| const RECORD_BASE: Base = Base { |
| data: || quote!(self.parent), |
| start: || quote!(self.pos), |
| }; |
| |
| // --------------------------------------------------------------------------- |
| // tables |
| // --------------------------------------------------------------------------- |
| |
| fn generate_table(item: &Table, with_parent: &HashSet<syn::Ident>) -> syn::Result<TokenStream> { |
| if item.attrs.write_only.is_some() { |
| return Ok(Default::default()); |
| } |
| let docs = &item.attrs.docs; |
| let name = item.raw_name(); |
| let generic = item.attrs.generic_offset.as_ref(); |
| let generic_with_default = generic.map(|t| quote!(#t = ())); |
| let phantom_decl = generic.map(|t| quote!(offset_type: core::marker::PhantomData<*const #t>)); |
| let phantom_init = generic.map(|_| quote!(offset_type: core::marker::PhantomData,)); |
| |
| let min_size = item.min_valid_size_expr(); |
| let byte_range_fns = byte_range_fns(&item.fields.fields, TABLE_BASE); |
| let getters = item |
| .fields |
| .iter() |
| .filter_map(|fld| { |
| getter( |
| fld, |
| &item.fields.fields, |
| TABLE_BASE, |
| generic.map(|g| &g.attr), |
| with_parent, |
| ) |
| }) |
| .collect::<Vec<_>>(); |
| |
| let read_args = item.attrs.read_args.as_ref(); |
| let args_type = read_args |
| .map(|a| a.args_type()) |
| .unwrap_or_else(|| quote!(())); |
| let destructure = read_args.map(|a| a.destructure_pattern()); |
| let stored_arg_decls = read_args |
| .map(|a| a.constructor_args().collect::<Vec<_>>()) |
| .unwrap_or_default(); |
| let stored_arg_inits = read_args |
| .map(|a| a.idents().map(|id| quote!(#id,)).collect::<Vec<_>>()) |
| .unwrap_or_default(); |
| let args_binding = if read_args.is_some() { |
| quote!(args) |
| } else { |
| quote!(_) |
| }; |
| let arg_getters = read_args |
| .map(|a| { |
| a.args |
| .iter() |
| .map(|arg| { |
| let ident = &arg.ident; |
| let typ = &arg.typ; |
| quote! { |
| pub fn #ident(&self) -> #typ { |
| self.#ident |
| } |
| } |
| }) |
| .collect::<Vec<_>>() |
| }) |
| .unwrap_or_default(); |
| |
| // tables that take args get a named constructor, since `Table::read` is |
| // only available when `Args = ()` |
| let named_read_ctor = read_args.map(|a| { |
| let ctor_args = a.constructor_args().collect::<Vec<_>>(); |
| let build = a.read_args_from_constructor_args(); |
| quote! { |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a> #name<'a> { |
| /// Reads this table, which requires external state. |
| pub fn read(data: Bytes<'a>, #( #ctor_args ),*) -> Option<Self> { |
| let args = #build; |
| <Self as Table<'a>>::read_with_args(data, args) |
| } |
| } |
| } |
| }); |
| |
| let of_unit_impl = generic.map(|t| { |
| quote! { |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a, #t> #name<'a, #t> { |
| /// This table with its specific generic replaced by `()`. |
| pub fn of_unit_type(&self) -> #name<'a, ()> { |
| #name { data: self.data, offset_type: core::marker::PhantomData } |
| } |
| } |
| } |
| }); |
| let format_impl = item.impl_format_trait(); |
| let discriminant_impl = exp_discriminant_impl(item); |
| let sanitize = sanitize_impl( |
| &name, |
| &item.fields.fields, |
| generic.map(|g| &g.attr), |
| with_parent, |
| TABLE_BASE, |
| true, |
| ); |
| let fast_sanitize = fast_sanitize_impl( |
| &name, |
| &item.fields.fields, |
| generic.map(|g| &g.attr), |
| with_parent, |
| TABLE_BASE, |
| true, |
| ); |
| let top_level = item.attrs.tag.as_ref().map(|tag| { |
| let tag_str = tag.value(); |
| let byte_tag = syn::LitByteStr::new(tag_str.as_bytes(), tag.span()); |
| quote! { |
| impl TopLevelTable for #name<'_> { |
| const TAG: Tag = Tag::new(#byte_tag); |
| } |
| } |
| }); |
| |
| Ok(quote! { |
| #( #docs )* |
| #[derive(Clone, Copy)] |
| pub struct #name<'a, #generic_with_default> { |
| data: Bytes<'a>, |
| #( #stored_arg_decls, )* |
| #phantom_decl |
| } |
| |
| impl<'a, #generic> Table<'a> for #name<'a, #generic> { |
| type Args = #args_type; |
| |
| const MIN_SIZE: usize = #min_size; |
| |
| fn read_with_args(data: Bytes<'a>, #args_binding: Self::Args) -> Option<Self> { |
| #destructure |
| #[allow(clippy::absurd_extreme_comparisons)] |
| if data.len() < <Self as Table<'a>>::MIN_SIZE { |
| return None; |
| } |
| Some(Self { data, #( #stored_arg_inits )* #phantom_init }) |
| } |
| } |
| |
| #named_read_ctor |
| #top_level |
| #format_impl |
| #discriminant_impl |
| #sanitize |
| #fast_sanitize |
| |
| #of_unit_impl |
| |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a, #generic> #name<'a, #generic> { |
| /// The data this table's offsets are measured from. |
| pub fn offset_data(&self) -> Bytes<'a> { |
| self.data |
| } |
| |
| #( #arg_getters )* |
| #( #getters )* |
| #( #byte_range_fns )* |
| } |
| }) |
| } |
| |
| /// The inline discriminant a generic group reads to pick its payload type. |
| fn exp_discriminant_impl(item: &Table) -> Option<TokenStream> { |
| let field = item |
| .fields |
| .iter() |
| .find(|fld| fld.attrs.discriminant.is_some())?; |
| let name = item.raw_name(); |
| let parts: Vec<_> = item |
| .fields |
| .iter() |
| .take_while(|fld| fld.name != field.name) |
| .map(|fld| { |
| fld.known_min_size_stmt() |
| .expect("all fields before #[discriminant] must have a known size") |
| }) |
| .filter(|t| !t.is_empty()) |
| .collect(); |
| let offset = match parts.as_slice() { |
| [] => quote!(0), |
| [one] => one.to_owned(), |
| more => quote!( (#(#more)+*) ), |
| }; |
| Some(quote! { |
| impl Discriminant for #name<'_, ()> { |
| fn read_discriminant(data: Bytes<'_>) -> Option<u16> { |
| data.read_at(#offset) |
| } |
| } |
| }) |
| } |
| |
| // --------------------------------------------------------------------------- |
| // generic groups |
| // --------------------------------------------------------------------------- |
| |
| /// An enum over the payload types a wrapper can hold, chosen by a discriminant |
| /// in the wrapper. GPOS/GSUB lookups. |
| fn generate_generic_group(item: &GenericGroup) -> syn::Result<TokenStream> { |
| let docs = &item.attrs.docs; |
| let name = &item.name; |
| let inner = &item.inner_type; |
| let variants = item.variants.iter().map(|v| { |
| let vname = &v.name; |
| let typ = &v.typ; |
| quote!( #vname(#inner<'a, #typ<'a>>), ) |
| }); |
| let arms = item.variants.iter().map(|v| { |
| let vname = &v.name; |
| let type_id = &v.type_id; |
| quote!( #type_id => Some(#name::#vname(<#inner<'a, _> as Table<'a>>::read(data)?)), ) |
| }); |
| let of_unit_arms = item.variants.iter().map(|v| { |
| let vname = &v.name; |
| quote!( #name::#vname(inner) => inner.of_unit_type(), ) |
| }); |
| let group_name_str = name.to_string(); |
| let group_sanitize_arms = item |
| .variants |
| .iter() |
| .map(|v| { |
| let vname = &v.name; |
| quote!( Self::#vname(inner) => inner.sanitize_in(ctx), ) |
| }) |
| .collect::<Vec<_>>(); |
| let group_fast_arms = item |
| .variants |
| .iter() |
| .map(|v| { |
| let vname = &v.name; |
| quote!( Self::#vname(inner) => inner.fast_sanitize_in(ctx), ) |
| }) |
| .collect::<Vec<_>>(); |
| |
| Ok(quote! { |
| #( #docs )* |
| #[derive(Clone, Copy)] |
| pub enum #name<'a> { |
| #( #variants )* |
| } |
| |
| #[cfg(feature = "sanitize")] |
| impl<'a> Sanitize<'a> for #name<'a> { |
| const TYPE_NAME: &'static str = #group_name_str; |
| |
| fn sanitize_in(&self, ctx: &mut SanitizeContext) { |
| match self { |
| #( #group_sanitize_arms )* |
| } |
| } |
| } |
| |
| #[cfg(feature = "fast_sanitize")] |
| impl<'a> FastSanitize<'a> for #name<'a> { |
| fn fast_sanitize_in(&self, ctx: &mut FastSanitizeContext) -> bool { |
| match self { |
| #( #group_fast_arms )* |
| } |
| } |
| } |
| |
| impl<'a> Table<'a> for #name<'a> { |
| type Args = (); |
| |
| const MIN_SIZE: usize = <#inner<'a, ()> as Table<'a>>::MIN_SIZE; |
| |
| fn read_with_args(data: Bytes<'a>, _: ()) -> Option<Self> { |
| match <#inner<'_, ()> as Discriminant>::read_discriminant(data)? { |
| #( #arms )* |
| _ => None, |
| } |
| } |
| } |
| |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a> #name<'a> { |
| /// The inner table with its specific generic erased, so that one |
| /// concrete type carries the methods. |
| pub fn of_unit_type(&self) -> #inner<'a, ()> { |
| match self { |
| #( #of_unit_arms )* |
| } |
| } |
| } |
| }) |
| } |
| |
| // --------------------------------------------------------------------------- |
| // records |
| // --------------------------------------------------------------------------- |
| |
| /// Which of the shapes a record takes. |
| /// |
| /// See `docs/parsing-rework.md`. The choice is made by how the record's byte |
| /// length is known, plus whether it holds an offset. |
| #[derive(Debug, Clone, Copy, PartialEq)] |
| enum RecordShape { |
| /// Fixed size, no offsets: a plain zerocopy struct, handed out as `&'a [R]`. |
| Zerocopy, |
| /// Fixed size, holds an offset: zerocopy, reached through `WithParent`. |
| ZerocopyWithParent, |
| /// Size computed from read args: a cursor into the parent. |
| Computed, |
| } |
| |
| fn record_shape(item: &Record) -> RecordShape { |
| let computed = |
| item.attrs.read_args.is_some() || item.fields.iter().any(|f| f.has_computed_len()); |
| if computed { |
| RecordShape::Computed |
| } else if item.fields.iter().any(is_offset_field) { |
| RecordShape::ZerocopyWithParent |
| } else { |
| RecordShape::Zerocopy |
| } |
| } |
| |
| fn generate_record(item: &Record, with_parent: &HashSet<syn::Ident>) -> syn::Result<TokenStream> { |
| match record_shape(item) { |
| RecordShape::Zerocopy => generate_zerocopy_record(item, false), |
| RecordShape::ZerocopyWithParent => generate_zerocopy_record(item, true), |
| RecordShape::Computed => generate_computed_record(item, with_parent), |
| } |
| } |
| |
| /// A fixed-size record: the same zerocopy struct emitted today, except that any |
| /// offset accessors move onto `WithParent`, where the base is already held. |
| fn generate_zerocopy_record(item: &Record, with_parent: bool) -> syn::Result<TokenStream> { |
| let name = &item.name; |
| let docs = &item.attrs.docs; |
| let field_decls = item.fields.iter().map(|fld| { |
| let fname = &fld.name; |
| let fdocs = &fld.attrs.docs; |
| let typ = zerocopy_field_type(fld); |
| quote!( #( #fdocs )* pub #fname: #typ, ) |
| }); |
| let plain_getters = item.fields.iter().filter_map(zerocopy_plain_getter); |
| let raw_byte_len = item |
| .fields |
| .iter() |
| .map(|fld| { |
| let typ = fld.typ.cooked_type_tokens(); |
| quote!(#typ::RAW_BYTE_LEN) |
| }) |
| .collect::<Vec<_>>(); |
| |
| let with_parent_impl = with_parent.then(|| { |
| let offset_getters = item |
| .fields |
| .iter() |
| .filter_map(with_parent_offset_getter) |
| .collect::<Vec<_>>(); |
| let checks = item.fields.iter().filter_map(|fld| { |
| let FieldType::Offset { .. } = &fld.typ else { |
| return None; |
| }; |
| if fld.attrs.offset_getter.is_some() { |
| return None; |
| } |
| let getter = fld.offset_getter_name()?; |
| let raw = &fld.name; |
| let raw_str = raw.to_string(); |
| let nullable = is_nullable(fld); |
| let raw_value = if nullable { |
| quote!( self.#raw().offset().to_u32() ) |
| } else { |
| quote!( self.#raw().to_u32() ) |
| }; |
| Some(quote! { |
| { |
| let target = self.#getter(); |
| ctx.check_offset(#raw_str, #raw_value, target.is_some(), #nullable); |
| if let Some(target) = target { |
| ctx.enter_field(#raw_str); |
| target.sanitize_in(ctx); |
| ctx.exit_field(); |
| } |
| } |
| }) |
| }); |
| let type_name = name.to_string(); |
| let checks = checks.collect::<Vec<_>>(); |
| let fast_checks = item |
| .fields |
| .iter() |
| .filter_map(|fld| { |
| let FieldType::Offset { .. } = &fld.typ else { |
| return None; |
| }; |
| if fld.attrs.offset_getter.is_some() { |
| return None; |
| } |
| let getter = fld.offset_getter_name()?; |
| let raw = &fld.name; |
| let nullable = is_nullable(fld); |
| let raw_value = if nullable { |
| quote!( self.#raw().offset().to_u32() ) |
| } else { |
| quote!( self.#raw().to_u32() ) |
| }; |
| // a nullable offset may legitimately be zero; one that is not, may not |
| let null_case = if nullable { |
| quote!( if #raw_value == 0 { return true; } ) |
| } else { |
| quote!() |
| }; |
| Some(quote! { |
| { |
| #null_case |
| let Some(target) = self.#getter() else { |
| return false; |
| }; |
| if !target.fast_sanitize_in(ctx) { |
| return false; |
| } |
| } |
| }) |
| }) |
| .collect::<Vec<_>>(); |
| let fast_ctx_param = if fast_checks.is_empty() { |
| quote!(_ctx) |
| } else { |
| quote!(ctx) |
| }; |
| // a record whose offsets all have hand-written resolvers has nothing |
| // here: what those resolvers reach is outside what the pass can see |
| let ctx_param = if checks.is_empty() { |
| quote!(_ctx) |
| } else { |
| quote!(ctx) |
| }; |
| quote! { |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a> WithParent<'a, #name> { |
| #( #offset_getters )* |
| } |
| |
| #[cfg(feature = "fast_sanitize")] |
| impl<'a> FastSanitize<'a> for WithParent<'a, #name> { |
| fn fast_sanitize_in(&self, #fast_ctx_param: &mut FastSanitizeContext) -> bool { |
| #( #fast_checks )* |
| true |
| } |
| } |
| |
| #[cfg(feature = "sanitize")] |
| impl<'a> Sanitize<'a> for WithParent<'a, #name> { |
| const TYPE_NAME: &'static str = #type_name; |
| |
| fn sanitize_in(&self, #ctx_param: &mut SanitizeContext) { |
| // a record adds no step of its own: the path already says |
| // which field and which element we are in, and a record has |
| // no identity to guard against revisiting — its extent was |
| // checked by whoever located the run |
| #( #checks )* |
| } |
| } |
| } |
| }); |
| |
| Ok(quote! { |
| #( #docs )* |
| // `Default` is what lets a caller reaching past the end of a slice say |
| // `.copied().unwrap_or_default()`: every field is a scalar, and an |
| // offset defaults to null |
| #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, bytemuck::AnyBitPattern)] |
| #[repr(C, packed)] |
| pub struct #name { |
| #( #field_decls )* |
| } |
| |
| impl FixedSize for #name { |
| const RAW_BYTE_LEN: usize = #( #raw_byte_len )+*; |
| } |
| |
| impl #name { |
| #( #plain_getters )* |
| } |
| |
| #with_parent_impl |
| }) |
| } |
| |
| /// A record whose size is computed from its read args: a cursor holding the |
| /// parent, its own position within it, and the args. |
| fn generate_computed_record( |
| item: &Record, |
| with_parent: &HashSet<syn::Ident>, |
| ) -> syn::Result<TokenStream> { |
| let name = &item.name; |
| let docs = &item.attrs.docs; |
| let read_args = item.attrs.read_args.as_ref(); |
| let args_type = read_args |
| .map(|a| a.args_type()) |
| .unwrap_or_else(|| quote!(())); |
| let destructure = read_args.map(|a| a.destructure_pattern()); |
| let arg_getters = read_args |
| .map(|a| { |
| a.args |
| .iter() |
| .enumerate() |
| .map(|(i, arg)| { |
| let ident = &arg.ident; |
| let typ = &arg.typ; |
| let access = if a.args.len() == 1 { |
| quote!(self.args) |
| } else { |
| let idx = syn::Index::from(i); |
| quote!(self.args.#idx) |
| }; |
| quote! { |
| pub fn #ident(&self) -> #typ { |
| #access |
| } |
| } |
| }) |
| .collect::<Vec<_>>() |
| }) |
| .unwrap_or_default(); |
| |
| // a record whose length the font declares uses that, rather than the sum of |
| // its fields: the declared size may be larger than what the fields occupy |
| let size_body = match item.attrs.record_size.as_ref() { |
| Some(arg) => { |
| let arg = &arg.attr; |
| quote!( #arg as usize ) |
| } |
| None => { |
| let terms = item |
| .fields |
| .iter() |
| .map(|fld| len_expr(fld, None)) |
| .collect::<Vec<_>>(); |
| quote!( 0usize #( .saturating_add(#terms) )* ) |
| } |
| }; |
| let byte_range_fns = byte_range_fns(&item.fields.fields, RECORD_BASE); |
| let sanitize = sanitize_impl( |
| name, |
| &item.fields.fields, |
| None, |
| with_parent, |
| RECORD_BASE, |
| false, |
| ); |
| let fast_sanitize = fast_sanitize_impl( |
| name, |
| &item.fields.fields, |
| None, |
| with_parent, |
| RECORD_BASE, |
| false, |
| ); |
| let getters = item |
| .fields |
| .iter() |
| .filter_map(|fld| getter(fld, &item.fields.fields, RECORD_BASE, None, with_parent)) |
| .collect::<Vec<_>>(); |
| |
| Ok(quote! { |
| #( #docs )* |
| #[derive(Clone, Copy)] |
| pub struct #name<'a> { |
| /// The enclosing table's data: what this record's offsets are |
| /// measured from. |
| parent: Bytes<'a>, |
| /// This record's position within `parent`. |
| pos: usize, |
| args: #args_type, |
| } |
| |
| impl ComputedSize for #name<'_> { |
| type Args = #args_type; |
| |
| #[allow(unused_variables)] |
| fn computed_size(args: #args_type) -> usize { |
| #destructure |
| let _ = &args; |
| #size_body |
| } |
| } |
| |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a> #name<'a> { |
| /// Locates the record at `pos` bytes into `parent`. |
| /// |
| /// Performs no reads. |
| #[inline] |
| pub fn at(parent: Bytes<'a>, pos: usize, args: #args_type) -> Self { |
| Self { parent, pos, args } |
| } |
| |
| /// The data this record's offsets are measured from. |
| pub fn offset_data(&self) -> Bytes<'a> { |
| self.parent |
| } |
| |
| #( #arg_getters )* |
| #( #getters )* |
| #( #byte_range_fns )* |
| } |
| |
| impl<'a> ArrayElement<'a> for #name<'a> { |
| type Args = #args_type; |
| type Store = StridedStore<'a>; |
| type Output = Self; |
| |
| #[inline] |
| fn read(store: StridedStore<'a>, item: usize, args: #args_type) -> Self { |
| Self::at(store.data(), item, args) |
| } |
| } |
| |
| #sanitize |
| #fast_sanitize |
| }) |
| } |
| |
| // --------------------------------------------------------------------------- |
| // format groups |
| // --------------------------------------------------------------------------- |
| |
| fn generate_format_group(item: &TableFormat) -> syn::Result<TokenStream> { |
| let name = &item.name; |
| let docs = &item.attrs.docs; |
| let live = || { |
| item.variants |
| .iter() |
| .filter(|v| v.attrs.write_only.is_none()) |
| }; |
| let variants = live().map(|v| { |
| let vname = &v.name; |
| let typ = v.type_name(); |
| let vdocs = &v.attrs.docs; |
| quote!( #( #vdocs )* #vname(#typ<'a>), ) |
| }); |
| let mut has_match_stmt = false; |
| let match_arms = live() |
| .map(|v| { |
| let vname = &v.name; |
| let typ = v.type_name(); |
| let lhs = if let Some(expr) = v.attrs.match_stmt.as_deref() { |
| has_match_stmt = true; |
| let expr = &expr.expr; |
| quote!(format if #expr) |
| } else { |
| quote!(#typ::FORMAT) |
| }; |
| quote!( #lhs => Some(Self::#vname(<#typ as Table<'a>>::read(data)?)), ) |
| }) |
| .collect::<Vec<_>>(); |
| let offset_data_arms = live().map(|v| { |
| let vname = &v.name; |
| quote!( Self::#vname(item) => item.offset_data(), ) |
| }); |
| let min_size_terms = live() |
| .map(|v| { |
| let typ = v.type_name(); |
| quote!( <#typ as Table>::MIN_SIZE ) |
| }) |
| .collect::<Vec<_>>(); |
| let name_str = name.to_string(); |
| let sanitize_arms = live() |
| .map(|v| { |
| let vname = &v.name; |
| quote!( Self::#vname(item) => item.sanitize_in(ctx), ) |
| }) |
| .collect::<Vec<_>>(); |
| let fast_arms = live() |
| .map(|v| { |
| let vname = &v.name; |
| quote!( Self::#vname(item) => item.fast_sanitize_in(ctx), ) |
| }) |
| .collect::<Vec<_>>(); |
| let format_typ = &item.format; |
| let format_offset = item |
| .format_offset |
| .as_ref() |
| .map(|lit| lit.base10_parse::<usize>().unwrap_or(0)) |
| .unwrap_or(0); |
| let maybe_allow_lint = has_match_stmt.then(|| quote!(#[allow(clippy::redundant_guards)])); |
| |
| Ok(quote! { |
| #( #docs )* |
| #[derive(Clone, Copy)] |
| pub enum #name<'a> { |
| #( #variants )* |
| } |
| |
| #[allow(clippy::needless_lifetimes)] |
| impl<'a> #name<'a> { |
| /// The data this table's offsets are measured from. |
| pub fn offset_data(&self) -> Bytes<'a> { |
| match self { |
| #( #offset_data_arms )* |
| } |
| } |
| } |
| |
| #[cfg(feature = "sanitize")] |
| impl<'a> Sanitize<'a> for #name<'a> { |
| const TYPE_NAME: &'static str = #name_str; |
| |
| fn sanitize_in(&self, ctx: &mut SanitizeContext) { |
| match self { |
| #( #sanitize_arms )* |
| } |
| } |
| } |
| |
| #[cfg(feature = "fast_sanitize")] |
| impl<'a> FastSanitize<'a> for #name<'a> { |
| fn fast_sanitize_in(&self, ctx: &mut FastSanitizeContext) -> bool { |
| match self { |
| #( #fast_arms )* |
| } |
| } |
| } |
| |
| impl<'a> Table<'a> for #name<'a> { |
| type Args = (); |
| |
| /// The smallest of the variants: which one is present is not known |
| /// until the format word is read. |
| const MIN_SIZE: usize = { |
| let sizes = [ #( #min_size_terms, )* ]; |
| let mut min = usize::MAX; |
| let mut i = 0; |
| while i < sizes.len() { |
| if sizes[i] < min { |
| min = sizes[i]; |
| } |
| i += 1; |
| } |
| min |
| }; |
| |
| #maybe_allow_lint |
| fn read_with_args(data: Bytes<'a>, _: ()) -> Option<Self> { |
| let format: #format_typ = data.read_at(#format_offset)?; |
| match format { |
| #( #match_arms )* |
| _ => None, |
| } |
| } |
| } |
| }) |
| } |
| |
| // --------------------------------------------------------------------------- |
| // fields |
| // --------------------------------------------------------------------------- |
| |
| fn is_offset_field(fld: &Field) -> bool { |
| match &fld.typ { |
| FieldType::Offset { .. } => true, |
| FieldType::Array { inner_typ } => matches!(inner_typ.as_ref(), FieldType::Offset { .. }), |
| _ => false, |
| } |
| } |
| |
| fn is_nullable(fld: &Field) -> bool { |
| fld.attrs.nullable.is_some() |
| } |
| |
| /// The declared type of a field inside a zerocopy record. |
| fn zerocopy_field_type(fld: &Field) -> TokenStream { |
| match &fld.typ { |
| FieldType::Offset { typ, .. } if is_nullable(fld) => quote!(BigEndian<Nullable<#typ>>), |
| FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { |
| if typ == "u8" { |
| quote!(u8) |
| } else { |
| quote!(BigEndian<#typ>) |
| } |
| } |
| FieldType::Struct { typ } => quote!(#typ), |
| other => panic!("zerocopy record cannot hold {other:?}"), |
| } |
| } |
| |
| fn zerocopy_plain_getter(fld: &Field) -> Option<TokenStream> { |
| if !fld.has_getter() { |
| return None; |
| } |
| let name = &fld.name; |
| let docs = &fld.attrs.docs; |
| let (typ, body) = match &fld.typ { |
| FieldType::Offset { typ, .. } if is_nullable(fld) => { |
| (quote!(Nullable<#typ>), quote!(self.#name.get())) |
| } |
| FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { |
| if typ == "u8" { |
| (quote!(u8), quote!(self.#name)) |
| } else { |
| (quote!(#typ), quote!(self.#name.get())) |
| } |
| } |
| FieldType::Struct { typ } => (quote!(&#typ), quote!(&self.#name)), |
| other => panic!("zerocopy record cannot hold {other:?}"), |
| }; |
| Some(quote! { |
| #( #docs )* |
| pub fn #name(&self) -> #typ { |
| #body |
| } |
| }) |
| } |
| |
| /// The offset accessor for a zerocopy record, emitted on `WithParent` so that |
| /// it needs no `data` argument. |
| fn with_parent_offset_getter(fld: &Field) -> Option<TokenStream> { |
| if fld.attrs.offset_getter.is_some() { |
| return None; |
| } |
| let (_, target) = match &fld.typ { |
| FieldType::Offset { typ, target } => (typ, target), |
| _ => return None, |
| }; |
| let raw_name = &fld.name; |
| let getter_name = fld.offset_getter_name()?; |
| let return_type = offset_target_type(target, false); |
| let args = fld |
| .attrs |
| .read_offset_args |
| .as_ref() |
| .map(|a| a.to_tokens_for_table_getter()); |
| let resolve = match args { |
| Some(args) => quote!(resolve_with_args(self.#raw_name(), #args)), |
| None => quote!(resolve(self.#raw_name())), |
| }; |
| let doc = format!(" Resolve [`{raw_name}`][Self::{raw_name}]."); |
| Some(quote! { |
| #[doc = #doc] |
| pub fn #getter_name(&self) -> Option<#return_type> { |
| self.parent().#resolve |
| } |
| }) |
| } |
| |
| /// The type an offset resolves to. |
| fn offset_target_type(target: &OffsetTarget, is_generic: bool) -> TokenStream { |
| match target { |
| OffsetTarget::Table(ident) if is_generic => ident.to_token_stream(), |
| OffsetTarget::Table(ident) => quote!(#ident<'a>), |
| OffsetTarget::Array(inner) => { |
| let inner = match inner.as_ref() { |
| FieldType::Scalar { typ } => quote!(BigEndian<#typ>), |
| FieldType::Struct { typ } => quote!(#typ), |
| other => panic!("unexpected offset array target {other:?}"), |
| }; |
| quote!(&'a [#inner]) |
| } |
| } |
| } |
| |
| /// The byte length of one field. |
| /// |
| /// `base` is `None` when emitting inside `ComputedSize::computed_size`, which |
| /// has no `self`. |
| fn len_expr(fld: &Field, base: Option<Base>) -> TokenStream { |
| if !fld.has_computed_len() { |
| let typ = fld.typ.cooked_type_tokens(); |
| return quote!(#typ::RAW_BYTE_LEN); |
| } |
| let read_args = fld.attrs.read_with_args.as_deref().map(|a| { |
| if base.is_some() { |
| a.to_tokens_for_table_getter() |
| } else { |
| a.to_tokens_for_validation() |
| } |
| }); |
| |
| if let FieldType::Struct { typ } = &fld.typ { |
| return quote!( <#typ as ComputedSize>::computed_size(#read_args) ); |
| } |
| |
| match fld.attrs.count.as_deref() { |
| Some(Count::All(_)) => { |
| let data = base |
| .map(|b| (b.data)()) |
| .unwrap_or_else(|| quote!(self.data)); |
| match &fld.typ { |
| FieldType::Array { inner_typ } => { |
| let inner = inner_typ.cooked_type_tokens(); |
| quote!(#data.len().saturating_sub(start) / #inner::RAW_BYTE_LEN * #inner::RAW_BYTE_LEN) |
| } |
| _ => quote!(#data.len().saturating_sub(start)), |
| } |
| } |
| Some(other) => { |
| let count_expr = other.count_expr(); |
| let size_expr = match &fld.typ { |
| FieldType::Array { inner_typ } => { |
| let inner = inner_typ.cooked_type_tokens(); |
| quote!( #inner::RAW_BYTE_LEN ) |
| } |
| FieldType::ComputedArray(array) => { |
| let inner = array.raw_inner_type(); |
| quote!( <#inner as ComputedSize>::computed_size(#read_args) ) |
| } |
| FieldType::VarLenArray(_) => { |
| // a var-len array's extent can only be found by walking it, |
| // which the store does; nothing follows it in a table |
| return quote!(0); |
| } |
| _ => unreachable!("count not valid here"), |
| }; |
| match other { |
| Count::SingleArg(CountArg::Literal(lit)) if lit.base10_digits() == "1" => size_expr, |
| _ => quote!( (#count_expr).saturating_mul(#size_expr) ), |
| } |
| } |
| None => quote!(compile_error!("missing count attribute?")), |
| } |
| } |
| |
| /// `let foo = self.foo();` for each field a `#[count(..)]` expression names. |
| fn count_arg_decls(fld: &Field, fields: &[Field]) -> Vec<TokenStream> { |
| fld.count_arg_names() |
| .map(|name| { |
| let is_opt = fields |
| .iter() |
| .find(|f| &f.name == name) |
| .map(|f| f.is_conditional()) |
| .unwrap_or(false); |
| let unwrap = is_opt.then(|| quote!(.unwrap_or_default())); |
| quote!(let #name = self.#name() #unwrap;) |
| }) |
| .collect() |
| } |
| |
| /// The `*_byte_range` accessors, which locate each field in turn. |
| fn byte_range_fns(fields: &[Field], base: Base) -> Vec<TokenStream> { |
| let mut prev_end = (base.start)(); |
| let mut out = Vec::new(); |
| for fld in fields { |
| let fn_name = fld.shape_byte_range_fn_name(); |
| let len = len_expr(fld, Some(base)); |
| let required_decls = count_arg_decls(fld, fields); |
| // a field may start where another field's offset points, rather than |
| // after the preceding field; the ones after it then follow on from |
| // there, so several arrays can share one offset |
| if let Some(at) = fld.attrs.at_offset.as_ref() { |
| let offset_fld = &at.attr; |
| prev_end = quote!( self.#offset_fld().to_u32() as usize ); |
| } |
| let end = match fld.attrs.conditional.as_deref() { |
| // the field is there if the record's declared size leaves room for |
| // it, which is a statement about where the field ends rather than |
| // about any other field's value |
| Some(Condition::IfFits) => quote! { |
| if start + #len <= self.pos + <Self as ComputedSize>::computed_size(self.args) { |
| start + #len |
| } else { |
| start |
| } |
| }, |
| Some(cond) => { |
| let cond = cond.condition_tokens_for_read(); |
| quote!( if #cond { start + #len } else { start } ) |
| } |
| None => quote!( start + #len ), |
| }; |
| out.push(quote! { |
| pub fn #fn_name(&self) -> Range<usize> { |
| #( #required_decls )* |
| let start = #prev_end; |
| let end = #end; |
| start..end |
| } |
| }); |
| prev_end = quote!( self.#fn_name().end ); |
| } |
| out |
| } |
| |
| /// A field accessor, plus the resolved accessor if the field is an offset. |
| /// |
| /// The three shapes, which preserve exactly what the crate does today: |
| /// |
| /// - a field covered by `MIN_SIZE` is guaranteed present, so it is returned |
| /// bare and the read unwraps; |
| /// - a non-conditional field beyond `MIN_SIZE` is also returned bare, reading |
| /// as empty or zero when its extent is not there (`unwrap_or_default`); |
| /// - a conditional field returns `Option`, because it may legitimately be |
| /// absent. |
| /// |
| /// Only offset *resolution* adds an `Option` of its own, since an offset can be |
| /// null or unreadable whatever the field's shape. |
| fn getter( |
| fld: &Field, |
| fields: &[Field], |
| base: Base, |
| generic: Option<&syn::Ident>, |
| with_parent: &HashSet<syn::Ident>, |
| ) -> Option<TokenStream> { |
| if !fld.has_getter() { |
| return None; |
| } |
| let name = &fld.name; |
| let docs = &fld.attrs.docs; |
| let data = (base.data)(); |
| let range_fn = fld.shape_byte_range_fn_name(); |
| let is_conditional = fld.is_conditional(); |
| // guaranteed present by the MIN_SIZE check performed when the table was read |
| let guaranteed = fld.validated_at_parse && !is_conditional; |
| let count_decls = count_arg_decls(fld, fields); |
| |
| let (mut return_type, mut read_stmt) = match &fld.typ { |
| FieldType::Array { inner_typ } if matches!(inner_typ.as_ref(), FieldType::Struct { typ } if with_parent.contains(typ)) => |
| { |
| let FieldType::Struct { typ } = inner_typ.as_ref() else { |
| unreachable!() |
| }; |
| // the raw `&'a [#typ]` is still there, as the array's store |
| let count = fld |
| .attrs |
| .count |
| .as_deref() |
| .map(|c| c.count_expr()) |
| .unwrap_or_else(|| quote!(0)); |
| ( |
| quote!(Array<'a, WithParent<'a, #typ>>), |
| quote!({ |
| #( #count_decls )* |
| Array::of_zerocopy_records_or_empty( |
| #data, |
| range.start, |
| transforms::to_usize(#count), |
| ) |
| }), |
| ) |
| } |
| FieldType::Array { .. } => { |
| let inner = raw_array_item_type(fld); |
| let tail = if guaranteed { |
| quote!(.unwrap()) |
| } else { |
| quote!(.unwrap_or_default()) |
| }; |
| ( |
| quote!(&'a [#inner]), |
| quote!( #data.read_array(range) #tail ), |
| ) |
| } |
| FieldType::ComputedArray(array) => { |
| let inner = array.raw_inner_type(); |
| let args = fld |
| .attrs |
| .read_with_args |
| .as_deref() |
| .map(|a| a.to_tokens_for_table_getter()) |
| .unwrap_or_else(|| quote!(())); |
| let count = fld |
| .attrs |
| .count |
| .as_deref() |
| .map(|c| c.count_expr()) |
| .unwrap_or_else(|| quote!(0)); |
| ( |
| quote!(Array<'a, #inner<'a>>), |
| quote!({ |
| #( #count_decls )* |
| Array::of_computed_or_empty( |
| #data, |
| range.start, |
| transforms::to_usize(#count), |
| #args, |
| ) |
| }), |
| ) |
| } |
| FieldType::VarLenArray(array) => { |
| let inner = array.raw_inner_type(); |
| ( |
| quote!(VariableSizeArray<'a, #inner<'a>>), |
| quote!( #data.split_off(range.start).map(VariableSizeArray::of_variable_size).unwrap_or_default() ), |
| ) |
| } |
| // an embedded record. one that carries read args has a computed size, |
| // so it is a cursor; one that does not is fixed-size and zerocopy, and |
| // is paired with its base by `WithParent`. codegen requires the latter |
| // to be covered by MIN_SIZE, which is what lets the accessor be |
| // non-optional |
| FieldType::Struct { typ } => match fld.attrs.read_with_args.as_deref() { |
| Some(args) => { |
| let args = args.to_tokens_for_table_getter(); |
| ( |
| quote!(#typ<'a>), |
| quote!( #typ::at(#data, range.start, #args) ), |
| ) |
| } |
| None => ( |
| quote!(WithParent<'a, #typ>), |
| quote!( WithParent::at(#data, range.start).unwrap() ), |
| ), |
| }, |
| FieldType::Offset { typ, .. } if is_nullable(fld) => { |
| let tail = if guaranteed { |
| quote!(.unwrap()) |
| } else { |
| quote!(.unwrap_or_default()) |
| }; |
| ( |
| quote!(Nullable<#typ>), |
| quote!( #data.read_at(range.start) #tail ), |
| ) |
| } |
| FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { |
| let tail = if guaranteed { |
| quote!(.unwrap()) |
| } else { |
| quote!(.unwrap_or_default()) |
| }; |
| (quote!(#typ), quote!( #data.read_at(range.start) #tail )) |
| } |
| FieldType::PendingResolution { .. } => panic!("should have resolved {fld:?}"), |
| }; |
| |
| if is_conditional { |
| // a conditional field may be absent, which is the one case that is an |
| // `Option` rather than a default |
| let inner = match &fld.typ { |
| FieldType::Array { .. } => { |
| let item = raw_array_item_type(fld); |
| quote!(&'a [#item]) |
| } |
| _ => return_type.clone(), |
| }; |
| return_type = quote!(Option<#inner>); |
| read_stmt = match &fld.typ { |
| FieldType::Array { .. } => { |
| quote!( (!range.is_empty()).then(|| #data.read_array(range)).flatten() ) |
| } |
| FieldType::Struct { typ } => match fld.attrs.read_with_args.as_deref() { |
| Some(args) => { |
| let args = args.to_tokens_for_table_getter(); |
| quote!( (!range.is_empty()).then(|| #typ::at(#data, range.start, #args)) ) |
| } |
| None => { |
| quote!( (!range.is_empty()).then(|| WithParent::at(#data, range.start)).flatten() ) |
| } |
| }, |
| _ => quote!( (!range.is_empty()).then(|| #data.read_at(range.start)).flatten() ), |
| }; |
| } |
| |
| let offset_getter = table_offset_getter(fld, base, generic); |
| |
| Some(quote! { |
| #( #docs )* |
| pub fn #name(&self) -> #return_type { |
| let range = self.#range_fn(); |
| #read_stmt |
| } |
| |
| #offset_getter |
| }) |
| } |
| |
| fn raw_array_item_type(fld: &Field) -> TokenStream { |
| let FieldType::Array { inner_typ } = &fld.typ else { |
| unreachable!() |
| }; |
| match inner_typ.as_ref() { |
| FieldType::Offset { typ, .. } if is_nullable(fld) => quote!(BigEndian<Nullable<#typ>>), |
| FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { |
| if typ == "u8" { |
| quote!(u8) |
| } else { |
| quote!(BigEndian<#typ>) |
| } |
| } |
| FieldType::Struct { typ } | FieldType::PendingResolution { typ } => quote!(#typ), |
| other => unreachable!("an array should never contain {other:?}"), |
| } |
| } |
| |
| /// The resolved accessor for an offset field on a table or computed record. |
| fn table_offset_getter( |
| fld: &Field, |
| base: Base, |
| generic: Option<&syn::Ident>, |
| ) -> Option<TokenStream> { |
| if fld.attrs.offset_getter.is_some() { |
| return None; |
| } |
| let target = match &fld.typ { |
| FieldType::Offset { target, .. } => target, |
| FieldType::Array { inner_typ } => match inner_typ.as_ref() { |
| FieldType::Offset { target, .. } => target, |
| _ => return None, |
| }, |
| _ => return None, |
| }; |
| let raw_name = &fld.name; |
| let getter_name = fld.offset_getter_name()?; |
| let data = (base.data)(); |
| let is_generic = matches!(target, OffsetTarget::Table(id) if Some(id) == generic); |
| let args = fld |
| .attrs |
| .read_offset_args |
| .as_ref() |
| .map(|a| a.to_tokens_for_table_getter()); |
| let doc = format!(" Resolve [`{raw_name}`][Self::{raw_name}]."); |
| |
| if fld.is_array() { |
| let OffsetTarget::Table(target_ident) = target else { |
| panic!("arrays of offsets to arrays are not in the spec"); |
| }; |
| let target_lifetime = (!is_generic).then(|| quote!(<'a>)); |
| let offset_typ = match &fld.typ { |
| FieldType::Array { inner_typ } => match inner_typ.as_ref() { |
| FieldType::Offset { typ, .. } => typ.clone(), |
| _ => unreachable!(), |
| }, |
| _ => unreachable!(), |
| }; |
| let offset_typ = if is_nullable(fld) { |
| quote!(Nullable<#offset_typ>) |
| } else { |
| quote!(#offset_typ) |
| }; |
| let args_token = args.clone().unwrap_or_else(|| quote!(())); |
| let where_clause = is_generic.then(|| quote!(where T: Table<'a, Args = ()>)); |
| if fld.is_conditional() { |
| return Some(quote! { |
| #[doc = #doc] |
| pub fn #getter_name(&self) -> Option<Array<'a, OffsetTo<#target_ident #target_lifetime, #offset_typ>>> |
| #where_clause |
| { |
| Some(Array::of_offsets(self.#raw_name()?, #data, #args_token)) |
| } |
| }); |
| } |
| return Some(quote! { |
| #[doc = #doc] |
| pub fn #getter_name(&self) -> Array<'a, OffsetTo<#target_ident #target_lifetime, #offset_typ>> |
| #where_clause |
| { |
| Array::of_offsets(self.#raw_name(), #data, #args_token) |
| } |
| }); |
| } |
| |
| let return_type = offset_target_type(target, is_generic); |
| let resolve = match args { |
| Some(args) => quote!(resolve_with_args(offset, #args)), |
| None => quote!(resolve(offset)), |
| }; |
| let bind_offset = if fld.is_conditional() { |
| quote!( let offset = self.#raw_name()?; ) |
| } else { |
| quote!( let offset = self.#raw_name(); ) |
| }; |
| let where_clause = is_generic.then(|| quote!(where T: Table<'a, Args = ()>)); |
| Some(quote! { |
| #[doc = #doc] |
| pub fn #getter_name(&self) -> Option<#return_type> #where_clause { |
| #bind_offset |
| #data.#resolve |
| } |
| }) |
| } |
| |
| // --------------------------------------------------------------------------- |
| // sanitize |
| // --------------------------------------------------------------------------- |
| |
| /// Emits the pass that reports what the accessors stopped reporting. |
| /// |
| /// Every check comes from something the table already exposes: a field's extent |
| /// is its generated `*_byte_range`, and whether an offset resolves is what its |
| /// resolved accessor answers. So this is a mechanical walk of the same field |
| /// list the accessors come from, and it cannot drift from them. |
| fn sanitize_impl( |
| name: &syn::Ident, |
| fields: &[Field], |
| generic: Option<&syn::Ident>, |
| with_parent: &HashSet<syn::Ident>, |
| base: Base, |
| is_table: bool, |
| ) -> TokenStream { |
| let type_name = name.to_string(); |
| let data = (base.data)(); |
| |
| // Checking every field's extent individually is quadratic: a field's |
| // `*_byte_range` is defined as the previous field's end, so evaluating all |
| // of them walks the chain once per field. |
| // |
| // Fields are laid out in order, so the last one's end is the largest, and |
| // one comparison against it clears every field before it. Only when that |
| // fails is it worth finding out which fields are actually short. |
| // |
| // `#[at_offset]` breaks the ordering — it jumps somewhere else — so the |
| // fields are split into runs at each jump and each run is cleared |
| // separately. |
| let mut runs: Vec<Vec<&Field>> = Vec::new(); |
| for fld in fields { |
| if runs.is_empty() || fld.attrs.at_offset.is_some() { |
| runs.push(Vec::new()); |
| } |
| runs.last_mut().expect("just pushed").push(fld); |
| } |
| let extent_checks = runs.iter().filter_map(|run| { |
| let last = run.last()?; |
| let last_range = last.shape_byte_range_fn_name(); |
| let per_field = run.iter().map(|fld| { |
| let fname_str = fld.name.to_string(); |
| let range_fn = fld.shape_byte_range_fn_name(); |
| quote!( ctx.check_extent(#fname_str, self.#range_fn(), #data); ) |
| }); |
| Some(quote! { |
| if self.#last_range().end > #data.len() { |
| #( #per_field )* |
| } |
| }) |
| }); |
| |
| let mut checks: Vec<TokenStream> = extent_checks.collect(); |
| |
| for fld in fields { |
| if !fld.has_getter() { |
| continue; |
| } |
| let fname = &fld.name; |
| let fname_str = fname.to_string(); |
| |
| // an offset: did it resolve, and what is on the other side? |
| if let Some(getter) = fld.offset_getter_name() { |
| if fld.attrs.offset_getter.is_some() { |
| // hand-written resolver; we cannot know its shape |
| continue; |
| } |
| let nullable = is_nullable(fld); |
| if fld.is_array() { |
| checks.push(quote! { |
| ctx.enter_field(#fname_str); |
| { |
| let targets = self.#getter(); |
| let budget = ctx.element_budget(targets.len()); |
| for (i, target) in targets.iter().enumerate().take(budget) { |
| if ctx.is_done() { |
| break; |
| } |
| ctx.enter_index(i); |
| match target { |
| Some(target) => target.sanitize_in(ctx), |
| None => ctx.report(Problem::NullOffset), |
| } |
| ctx.exit_index(); |
| } |
| } |
| ctx.exit_field(); |
| }); |
| } else if matches!( |
| &fld.typ, |
| FieldType::Offset { |
| target: OffsetTarget::Table(_), |
| .. |
| } |
| ) { |
| let raw = quote!( self.#fname() ); |
| let raw = if fld.is_conditional() { |
| quote!( #raw.unwrap_or_default() ) |
| } else { |
| raw |
| }; |
| let raw = if nullable { |
| quote!( #raw.offset().to_u32() ) |
| } else { |
| quote!( #raw.to_u32() ) |
| }; |
| checks.push(quote! { |
| { |
| let target = self.#getter(); |
| ctx.check_offset(#fname_str, #raw, target.is_some(), #nullable); |
| if let Some(target) = target { |
| ctx.enter_field(#fname_str); |
| target.sanitize_in(ctx); |
| ctx.exit_field(); |
| } |
| } |
| }); |
| } |
| continue; |
| } |
| |
| // a run of records that can themselves have something wrong |
| match &fld.typ { |
| FieldType::ComputedArray(_) => { |
| checks.push(quote! { |
| ctx.enter_field(#fname_str); |
| { |
| let items = self.#fname(); |
| let budget = ctx.element_budget(items.len()); |
| for (i, item) in items.iter().enumerate().take(budget) { |
| if ctx.is_done() { |
| break; |
| } |
| ctx.enter_index(i); |
| item.sanitize_in(ctx); |
| ctx.exit_index(); |
| } |
| } |
| ctx.exit_field(); |
| }); |
| } |
| FieldType::Array { inner_typ } if matches!(inner_typ.as_ref(), FieldType::Struct { typ } if with_parent.contains(typ)) => |
| { |
| checks.push(quote! { |
| ctx.enter_field(#fname_str); |
| { |
| let items = self.#fname(); |
| let budget = ctx.element_budget(items.len()); |
| for (i, item) in items.iter().enumerate().take(budget) { |
| if ctx.is_done() { |
| break; |
| } |
| ctx.enter_index(i); |
| item.sanitize_in(ctx); |
| ctx.exit_index(); |
| } |
| } |
| ctx.exit_field(); |
| }); |
| } |
| FieldType::Struct { typ } if with_parent.contains(typ) => { |
| checks.push(quote! { |
| ctx.enter_field(#fname_str); |
| self.#fname().sanitize_in(ctx); |
| ctx.exit_field(); |
| }); |
| } |
| _ => {} |
| } |
| } |
| |
| let where_clause = generic.map(|t| quote!( where #t: Table<'a, Args = ()> + Sanitize<'a> )); |
| let ctx_param = if checks.is_empty() { |
| quote!(_ctx) |
| } else { |
| quote!(ctx) |
| }; |
| let body = if is_table { |
| quote! { |
| if !#ctx_param.enter_table(#type_name, #data) { |
| return; |
| } |
| #( #checks )* |
| #ctx_param.exit_table(); |
| } |
| } else { |
| // a record adds no step and no node: the path already says which field |
| // and element it is, and it cannot be reached through an offset |
| quote!( #( #checks )* ) |
| }; |
| quote! { |
| #[cfg(feature = "sanitize")] |
| impl<'a, #generic> Sanitize<'a> for #name<'a, #generic> #where_clause { |
| const TYPE_NAME: &'static str = #type_name; |
| |
| fn sanitize_in(&self, #ctx_param: &mut SanitizeContext) { |
| #body |
| } |
| } |
| } |
| } |
| |
| /// Emits the pass that answers yes or no. |
| /// |
| /// The same checks [`sanitize_impl`] makes, with everything that exists only to |
| /// explain them removed: no type name, no field names, no path, no report. What |
| /// is left is a walk that returns `false` at the first thing wrong, and a |
| /// binary that links none of the literals the other one needs. |
| fn fast_sanitize_impl( |
| name: &syn::Ident, |
| fields: &[Field], |
| generic: Option<&syn::Ident>, |
| with_parent: &HashSet<syn::Ident>, |
| base: Base, |
| is_table: bool, |
| ) -> TokenStream { |
| let data = (base.data)(); |
| let mut checks = Vec::new(); |
| // an extent check just returns; only descending into something needs the |
| // context, and a record that holds nothing to descend into never touches it |
| let mut uses_ctx = is_table; |
| |
| // one comparison per run of fields, as in the detailed pass; here there is |
| // nothing to report so the per-field breakdown is simply absent |
| let mut runs: Vec<Vec<&Field>> = Vec::new(); |
| for fld in fields { |
| if runs.is_empty() || fld.attrs.at_offset.is_some() { |
| runs.push(Vec::new()); |
| } |
| runs.last_mut().expect("just pushed").push(fld); |
| } |
| for run in &runs { |
| let Some(last) = run.last() else { continue }; |
| let range_fn = last.shape_byte_range_fn_name(); |
| checks.push(quote! { |
| if self.#range_fn().end > #data.len() { |
| return false; |
| } |
| }); |
| } |
| |
| for fld in fields { |
| if !fld.has_getter() { |
| continue; |
| } |
| let fname = &fld.name; |
| |
| if let Some(getter) = fld.offset_getter_name() { |
| if fld.attrs.offset_getter.is_some() { |
| continue; |
| } |
| let nullable = is_nullable(fld); |
| if fld.is_array() { |
| let miss = if nullable { |
| quote!(continue) |
| } else { |
| quote!(return false) |
| }; |
| uses_ctx = true; |
| checks.push(quote! { |
| { |
| let targets = self.#getter(); |
| let budget = ctx.element_budget(targets.len()); |
| for target in targets.iter().take(budget) { |
| let Some(target) = target else { #miss }; |
| if !target.fast_sanitize_in(ctx) { |
| return false; |
| } |
| } |
| } |
| }); |
| } else if matches!( |
| &fld.typ, |
| FieldType::Offset { |
| target: OffsetTarget::Table(_), |
| .. |
| } |
| ) { |
| // a nullable offset may be absent; one that is not may not be |
| let body = if nullable { |
| quote! { |
| if let Some(target) = self.#getter() { |
| if !target.fast_sanitize_in(ctx) { |
| return false; |
| } |
| } |
| } |
| } else { |
| quote! { |
| let Some(target) = self.#getter() else { |
| return false; |
| }; |
| if !target.fast_sanitize_in(ctx) { |
| return false; |
| } |
| } |
| }; |
| uses_ctx = true; |
| checks.push(quote!( { #body } )); |
| } |
| continue; |
| } |
| |
| let descends = matches!(&fld.typ, FieldType::ComputedArray(_)) |
| || matches!(&fld.typ, FieldType::Struct { typ } if with_parent.contains(typ)) |
| || matches!(&fld.typ, FieldType::Array { inner_typ } |
| if matches!(inner_typ.as_ref(), FieldType::Struct { typ } if with_parent.contains(typ))); |
| uses_ctx |= descends; |
| match &fld.typ { |
| FieldType::ComputedArray(_) => checks.push(quote! { |
| { |
| let items = self.#fname(); |
| let budget = ctx.element_budget(items.len()); |
| for item in items.iter().take(budget) { |
| if !item.fast_sanitize_in(ctx) { |
| return false; |
| } |
| } |
| } |
| }), |
| FieldType::Array { inner_typ } if matches!(inner_typ.as_ref(), FieldType::Struct { typ } if with_parent.contains(typ)) => |
| { |
| uses_ctx = true; |
| checks.push(quote! { |
| { |
| let items = self.#fname(); |
| let budget = ctx.element_budget(items.len()); |
| for item in items.iter().take(budget) { |
| if !item.fast_sanitize_in(ctx) { |
| return false; |
| } |
| } |
| } |
| }) |
| } |
| FieldType::Struct { typ } if with_parent.contains(typ) => checks.push(quote! { |
| if !self.#fname().fast_sanitize_in(ctx) { |
| return false; |
| } |
| }), |
| _ => {} |
| } |
| } |
| |
| let where_clause = generic.map(|t| quote!( where #t: Table<'a, Args = ()> + FastSanitize<'a> )); |
| let ctx_param = if uses_ctx { quote!(ctx) } else { quote!(_ctx) }; |
| let body = if is_table { |
| quote! { |
| if !#ctx_param.enter(#data, <Self as Table<'a>>::MIN_SIZE) { |
| return false; |
| } |
| #( #checks )* |
| #ctx_param.exit(); |
| true |
| } |
| } else { |
| quote! { |
| #( #checks )* |
| true |
| } |
| }; |
| quote! { |
| #[cfg(feature = "fast_sanitize")] |
| impl<'a, #generic> FastSanitize<'a> for #name<'a, #generic> #where_clause { |
| fn fast_sanitize_in(&self, #ctx_param: &mut FastSanitizeContext) -> bool { |
| #body |
| } |
| } |
| } |
| } |