diff --git a/font-codegen/src/fields.rs b/font-codegen/src/fields.rs index f2791a0ac..6c727cc10 100644 --- a/font-codegen/src/fields.rs +++ b/font-codegen/src/fields.rs @@ -93,8 +93,8 @@ impl Fields { self.fields.iter() } - pub(crate) fn find(&self, ident: &syn::Ident) -> Option<&Field> { - self.iter().find(|fld| &fld.name == ident) + pub(crate) fn find(&self, mut f: impl FnMut(&Field) -> bool) -> Option<&Field> { + self.iter().find(|fld| f(fld)) } pub(crate) fn iter_compile_decls(&self) -> impl Iterator + '_ { @@ -298,6 +298,13 @@ impl Condition { Condition::IfFlag { field, flag } => quote!(self.#field.contains(#flag)), } } + + pub(crate) fn condition_tokens_for_sanitize(&self) -> TokenStream { + match self { + Condition::SinceVersion(version) => quote!(version.compatible(#version)), + Condition::IfFlag { field, flag } => quote!(#field.contains(#flag)), + } + } } /// All the state required to generate a constructor for a table/record @@ -556,6 +563,13 @@ impl Field { self.attrs.conditional.is_some() } + pub(crate) fn is_versioned(&self) -> bool { + matches!( + self.attrs.conditional.as_deref(), + Some(Condition::SinceVersion(_)) + ) + } + /// Sanity check we are in a sane state for the end of phase fn sanity_check(&self, phase: Phase) -> syn::Result<()> { check_resolution(phase, &self.typ)?; @@ -602,6 +616,18 @@ impl Field { } } + if self.attrs.sanitize_len_only.is_some() + && !matches!( + self.typ, + FieldType::Array { .. } | FieldType::ComputedArray(_) | FieldType::VarLenArray(_) + ) + { + return Err(logged_syn_error( + self.name.span(), + "#[sanitize_len_only] is only valid on array fields", + )); + } + if let Some(comp_attr) = &self.attrs.compile_with { if self.attrs.compile.is_some() { return Err(logged_syn_error( @@ -928,7 +954,7 @@ impl Field { self.attrs.count.is_some() } - fn is_offset_or_array_of_offsets(&self) -> bool { + pub(crate) fn is_offset_or_array_of_offsets(&self) -> bool { match &self.typ { FieldType::Offset { .. } => true, FieldType::Array { inner_typ } @@ -1334,6 +1360,217 @@ impl Field { }; Some(quote!( #name: #init_stmt )) } + + /// Generate the sanitize statement for this field. + /// + /// `needed` is the set of field names that must be bound to local variables + /// (because they're referenced by later fields). `generic` is the generic + /// offset type parameter, if any. + pub(crate) fn sanitize_stmt( + &self, + needed: &std::collections::HashSet, + generic: Option<&syn::Ident>, + ) -> syn::Result { + let is_needed = needed.contains(&self.name); + let inner = self.sanitize_stmt_inner(is_needed, generic); + + if let Some(condition) = &self.attrs.conditional { + let cond_tokens = condition.attr.condition_tokens_for_sanitize(); + Ok(quote! { + if #cond_tokens { + #inner + } + }) + } else { + Ok(inner) + } + } + + fn sanitize_stmt_inner(&self, is_needed: bool, _generic: Option<&syn::Ident>) -> TokenStream { + if let Some(sanitize_fn) = &self.attrs.sanitize_with { + let fn_name = &sanitize_fn.attr.fn_name; + let args = &sanitize_fn.attr.inputs; + if args.is_empty() { + return quote!(#fn_name(ctx)?;); + } else { + return quote!(#fn_name(ctx, #( #args ),*)?;); + } + } + match &self.typ { + FieldType::Scalar { typ } if is_needed => { + let name = &self.name; + quote!(let #name = ctx.read::<#typ>()?;) + } + FieldType::Scalar { typ } => quote!(ctx.advance::<#typ>();), + FieldType::Offset { typ, target } => match target { + OffsetTarget::Table(target_table) => { + let args = self.sanitize_offset_args(); + quote!(ctx.sanitize_offset::<#typ, #target_table>(#args)?;) + } + OffsetTarget::Array(inner) => { + let inner_t = match inner.deref() { + FieldType::Scalar { typ } => quote!(BigEndian<#typ>), + FieldType::Struct { typ } => quote!(#typ), + _ => panic!("should have errored before now"), + }; + let args = self.attrs.read_offset_args.as_ref().unwrap(); + let args = args.to_tokens_for_validation(); + let len_only = self.attrs.sanitize_len_only.is_some(); + let recurse_fn = + if matches!(inner.deref(), FieldType::Scalar { .. }) || len_only { + quote!(|_, _| Ok(())) + } else { + quote!(|t, ctx| t.sanitize_struct(ctx, ())) + }; + quote!(ctx.sanitize_offset_to_array::<#typ, #inner_t, _>(#args, #len_only, #recurse_fn)?;) + } + }, + FieldType::Struct { .. } => quote!(compile_error!("struct field needs sanitize_with");), + FieldType::Array { inner_typ } => { + let count = self.attrs.count.as_ref().expect("array has count"); + if matches!(&count.attr, Count::All(_)) { + return quote!(compile_error!("#[count(..)] fields require #[sanitize(fn)] attribute");); + } + let count_expr = count.count_expr(); + match inner_typ.as_ref() { + FieldType::Scalar { typ } => { + quote!(ctx.sanitize_array::<#typ>(#count_expr)?;) + } + FieldType::Offset { + typ: offset_typ, + target: OffsetTarget::Table(target_table), + } => { + let args = self.sanitize_offset_args(); + quote!(ctx.sanitize_array_of_offsets::<#offset_typ, #target_table>(#count_expr, #args)?;) + } + FieldType::Offset { .. } => { + quote!(compile_error!("sanitize not impl'd for array of offsets to arrays");) + } + FieldType::Struct { typ } if self.attrs.sanitize_len_only.is_some() => { + quote!(ctx.sanitize_array::<#typ>(#count_expr)?;) + } + FieldType::Struct { typ } => { + let args = self.sanitize_read_with_args_or_unit(); + quote!(ctx.sanitize_array_of_structs::<#typ>(#count_expr, #args)?;) + } + _ => quote!(compile_error!("unexpected inner type for sanitize")), + } + } + + FieldType::ComputedArray(array) => { + let inner = array.raw_inner_type(); + let Some(count) = self.attrs.count.as_ref().unwrap().single_field() else { + return quote!(compile_error!( + "computed array should always have simple count attr" + )); + }; + let args = self.attrs.read_with_args.as_ref().unwrap(); + let args = args.to_tokens_for_validation(); + let recurse = self.attrs.sanitize_len_only.is_none(); + quote! { + ctx.sanitize_computed_array::<#inner>(#count as _, #args, #recurse)?; + } + } + FieldType::VarLenArray(array) => { + let inner = array.raw_inner_type(); + let count = self.attrs.count.as_ref().and_then(|c| c.single_field()); + let Some(count) = count else { + return quote!(compile_error!( + "var len array needs a simple count field for sanitize" + )); + }; + let recurse = self.attrs.sanitize_len_only.is_none(); + quote! { + ctx.sanitize_var_len_array::<#inner>(#count as _, #recurse)?; + } + } + + FieldType::PendingResolution { .. } => { + panic!("should have been resolved before sanitize codegen") + } + } + } + + fn sanitize_offset_args(&self) -> TokenStream { + match &self.attrs.read_offset_args { + Some(args) => args.attr.to_tokens_for_validation(), + None => quote!(()), + } + } + + fn sanitize_read_with_args_or_unit(&self) -> TokenStream { + match &self.attrs.read_with_args { + Some(args) => args.to_tokens_for_validation(), + None => quote!(()), + } + } + + /// Generate a sanitize statement for this field in a record context. + /// + /// Returns `None` for non-offset fields (nothing to recurse into). + pub(crate) fn sanitize_record_stmt(&self) -> Option { + if let Some(sanitize_fn) = &self.attrs.sanitize_with { + let fn_name = &sanitize_fn.attr.fn_name; + let args = &sanitize_fn.attr.inputs; + if args.is_empty() { + return Some(quote!(self.#fn_name(ctx)?;)); + } else { + return Some(quote!(self.#fn_name(ctx, #( self.#args() ),*)?;)); + } + } + let name = &self.name; + let args = match &self.attrs.read_offset_args { + Some(args) => args.attr.to_tokens_for_table_getter(), + None => quote!(()), + }; + match &self.typ { + FieldType::Offset { + target: OffsetTarget::Table(target), + .. + } => Some(quote!(self.#name().sanitize_offset::<#target>(ctx, #args)?;)), + + FieldType::Offset { + target: OffsetTarget::Array(inner), + .. + } => { + let inner_t = match inner.deref() { + FieldType::Scalar { typ } => quote!(BigEndian<#typ>), + FieldType::Struct { typ } => quote!(#typ), + _ => panic!("should have errored before now"), + }; + let args = self.attrs.read_offset_args.as_ref().unwrap(); + let args = args.to_tokens_for_table_getter(); + let len_only = self.attrs.sanitize_len_only.is_some(); + let recurse_fn = match inner.deref() { + _ if len_only => quote!(|_, _| Ok(())), + FieldType::Scalar { .. } => quote!(|_, _| Ok(())), + FieldType::Struct { .. } => quote!(|t, ctx| t.sanitize_struct(ctx, ())), + _ => unreachable!("would panic above"), + }; + Some( + quote!(ctx.sanitize_resolved_offset_to_array::<_, #inner_t, _>(self.#name(), #args, #len_only, #recurse_fn)?;), + ) + } + + FieldType::Array { inner_typ } => match inner_typ.as_ref() { + FieldType::Offset { + target: OffsetTarget::Table(target), + .. + } => Some(quote!(self.#name().sanitize_offset::<#target>(ctx, #args)?;)), + + FieldType::Offset { + target: OffsetTarget::Array(_), + .. + } => Some(quote!(compile_error!( + "sanitize impl missing for array of offsets to arrays" + ))), + + _ => None, + }, + + _ => None, + } + } } impl FieldType { diff --git a/font-codegen/src/format_group.rs b/font-codegen/src/format_group.rs index aec81e412..6a75ddb6b 100644 --- a/font-codegen/src/format_group.rs +++ b/font-codegen/src/format_group.rs @@ -59,11 +59,7 @@ pub(crate) fn generate(item: &TableFormat, items: &Items) -> syn::Result table) }); - let format_offset = item - .format_offset - .as_ref() - .map(|lit| lit.base10_parse::().unwrap()) - .unwrap_or(0); + let format_offset = item.format_offset(); let getters = generate_shared_getters(item, items)?; let getters = (!getters.is_empty()).then(|| { @@ -91,6 +87,8 @@ pub(crate) fn generate(item: &TableFormat, items: &Items) -> syn::Result syn::Result #name<'a> { fn dyn_inner<'b>(&'b self) -> &'b dyn SomeTable<'a> { @@ -164,6 +164,45 @@ pub(crate) fn generate(item: &TableFormat, items: &Items) -> syn::Result TokenStream { + let name = &item.name; + let format = &item.format; + let format_offset = item.format_offset(); + + let mut has_any_match_stmt = false; + let match_arms: Vec<_> = item + .variants + .iter() + .filter(|v| v.attrs.write_only.is_none()) + .map(|variant| { + let typ = variant.type_name(); + let lhs = if let Some(expr) = variant.attrs.match_stmt.as_deref() { + has_any_match_stmt = true; + let expr = &expr.expr; + quote!(format if #expr) + } else { + quote!(#typ::FORMAT) + }; + quote!(#lhs => #typ::sanitize(ctx, ()),) + }) + .collect(); + + let maybe_allow_lint = has_any_match_stmt.then(|| quote!(#[allow(clippy::redundant_guards)])); + + quote! { + impl Sanitize for #name<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let format: #format = ctx.peek_at(#format_offset)?; + #maybe_allow_lint + match format { + #( #match_arms )* + other => Err(ReadError::InvalidFormat(other.into())), + } + } + } + } +} + pub(crate) fn generate_compile(item: &TableFormat, items: &Items) -> syn::Result { let name = &item.name; let docs = &item.attrs.docs; @@ -421,6 +460,17 @@ fn generate_from_obj(item: &TableFormat, parse_module: &syn::Path) -> syn::Resul }) } +impl TableFormat { + fn format_offset(&self) -> usize { + self.format_offset + .as_ref() + .map(|lit| { + lit.base10_parse::() + .expect("format offset must be unsigned") + }) + .unwrap_or(0) + } +} // An overwrought and likely incorrect way of converting 'Format1' to 'format_1' -_- fn make_snake_case_ident(ident: &syn::Ident) -> syn::Ident { let input = ident.to_string(); diff --git a/font-codegen/src/generic_group.rs b/font-codegen/src/generic_group.rs index 9c3f53a5f..16e1e8f44 100644 --- a/font-codegen/src/generic_group.rs +++ b/font-codegen/src/generic_group.rs @@ -3,9 +3,9 @@ use proc_macro2::TokenStream; use quote::quote; -use crate::parsing::GenericGroup; +use crate::parsing::{GenericGroup, Items}; -pub(crate) fn generate(item: &GenericGroup) -> syn::Result { +pub(crate) fn generate(item: &GenericGroup, items: &Items) -> syn::Result { let docs = &item.attrs.docs; let name = &item.name; let inner = &item.inner_type; @@ -33,6 +33,8 @@ pub(crate) fn generate(item: &GenericGroup) -> syn::Result { " This lets us return a single concrete type we can call methods on.", ]; + let sanitize = generate_sanitize(item, items); + Ok(quote! { #( #docs)* pub enum #name <'a> { @@ -69,6 +71,8 @@ pub(crate) fn generate(item: &GenericGroup) -> syn::Result { } } + #sanitize + #[cfg(feature = "experimental_traverse")] impl<'a> #name <'a> { fn dyn_inner(&self) -> &(dyn SomeTable<'a> + 'a) { @@ -99,6 +103,36 @@ pub(crate) fn generate(item: &GenericGroup) -> syn::Result { }) } +fn generate_sanitize(item: &GenericGroup, items: &Items) -> Option { + if !items.sanitize { + return None; + } + let name = &item.name; + let inner = &item.inner_type; + + let match_arms: Vec<_> = item + .variants + .iter() + .map(|var| { + let type_id = &var.type_id; + let typ = &var.typ; + quote!(#type_id => #inner::<#typ>::sanitize(ctx, _args),) + }) + .collect(); + + Some(quote! { + impl Sanitize for #name<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let discriminant = #inner::read_discriminant(ctx.data())?; + match discriminant { + #( #match_arms )* + other => Err(ReadError::InvalidFormat(other as _)), + } + } + } + }) +} + pub(crate) fn generate_compile( item: &GenericGroup, parse_module: &syn::Path, diff --git a/font-codegen/src/lib.rs b/font-codegen/src/lib.rs index b5dc5728e..a136ce608 100644 --- a/font-codegen/src/lib.rs +++ b/font-codegen/src/lib.rs @@ -77,8 +77,8 @@ pub(crate) fn generate_parse_module(items: &Items) -> Result record::generate(item, items)?, - Item::Table(item) => table::generate(item)?, - Item::GenericGroup(item) => generic_group::generate(item)?, + Item::Table(item) => table::generate(item, items)?, + Item::GenericGroup(item) => generic_group::generate(item, items)?, Item::Format(item) => format_group::generate(item, items)?, Item::RawEnum(item) => flags_enums::generate_raw_enum(item), Item::Flags(item) => flags_enums::generate_flags(item), diff --git a/font-codegen/src/parsing.rs b/font-codegen/src/parsing.rs index 3f2ffeae6..eab510778 100644 --- a/font-codegen/src/parsing.rs +++ b/font-codegen/src/parsing.rs @@ -24,6 +24,9 @@ use crate::Phase; #[derive(Debug)] pub(crate) struct Items { pub(crate) parse_module_path: syn::Path, + /// Whether the `#![sanitize]` module attribute is present, + /// opting this module into `Sanitize` trait codegen. + pub(crate) sanitize: bool, // we use an IndexMap so that we generate code in the same order as items // are declared in the input file. items: IndexMap, @@ -171,7 +174,7 @@ mod kw { impl Parse for Items { fn parse(input: ParseStream) -> Result { let mut items = IndexMap::new(); - let parse_module_path = get_parse_module_path(input)?; + let (parse_module_path, sanitize) = get_module_attrs(input)?; while !input.is_empty() { let item = input.parse::()?; items.insert(item.name().clone(), item); @@ -179,24 +182,39 @@ impl Parse for Items { Ok(Self { items, parse_module_path, + sanitize, }) } } -fn get_parse_module_path(input: ParseStream) -> syn::Result { +/// Parse module-level inner attributes. +/// +/// Required: `#![parse_module(read_fonts::tables::foo)]` +/// Optional: `#![sanitize]` — opts this module into `Sanitize` trait codegen. +fn get_module_attrs(input: ParseStream) -> syn::Result<(syn::Path, bool)> { let attrs = input.call(Attribute::parse_inner)?; - match attrs.as_slice() { - [one] if one.path().is_ident("parse_module") => one.parse_args(), - [one] => Err(logged_syn_error(one.span(), "unexpected attribute")), - [_, two, ..] => Err(logged_syn_error( - two.span(), - "expected at most one top-level attribute", - )), - [] => Err(logged_syn_error( - Span::call_site(), - "expected #![parse_module(..)] attribute", - )), + + let mut parse_module_path = None; + let mut sanitize = false; + + for attr in &attrs { + if attr.path().is_ident("parse_module") { + parse_module_path = Some(attr.parse_args()?); + } else if attr.path().is_ident("sanitize") { + sanitize = true; + } else { + return Err(logged_syn_error( + attr.span(), + "unexpected attribute; expected `parse_module` or `sanitize`", + )); + } } + + let parse_module_path = parse_module_path.ok_or_else(|| { + logged_syn_error(Span::call_site(), "expected #![parse_module(..)] attribute") + })?; + + Ok((parse_module_path, sanitize)) } impl Parse for Item { diff --git a/font-codegen/src/parsing/attrs.rs b/font-codegen/src/parsing/attrs.rs index 7375895aa..7897589e8 100644 --- a/font-codegen/src/parsing/attrs.rs +++ b/font-codegen/src/parsing/attrs.rs @@ -95,6 +95,10 @@ pub(crate) struct FieldAttrs { pub(crate) validate: Option>, /// Marks this field as the discriminant for a generic offset type. pub(crate) discriminant: Option, + /// During sanitize, only check the length of this field (don't recurse). + pub(crate) sanitize_len_only: Option, + /// Custom sanitize fn, like compile_with but for sanitize. + pub(crate) sanitize_with: Option>, } #[derive(Clone, Debug, Default)] @@ -126,6 +130,12 @@ pub(crate) struct FieldReadArgs { pub(crate) inputs: Vec, } +#[derive(Debug, Clone)] +pub(crate) struct SanitizeWith { + pub(crate) fn_name: syn::Ident, + pub(crate) inputs: Vec, +} + #[derive(Clone, Debug)] pub(crate) enum Condition { SinceVersion(VersionSpec), @@ -277,6 +287,8 @@ static TRAVERSE_WITH: &str = "traverse_with"; static TO_OWNED: &str = "to_owned"; static VALIDATE: &str = "validate"; static DISCRIMINANT: &str = "discriminant"; +static SANITIZE_LEN_ONLY: &str = "sanitize_len_only"; +static SANITIZE_WITH: &str = "sanitize_with"; static MATCH_IF: &str = "match_if"; static WRITE_FONTS_ONLY: &str = "write_fonts_only"; @@ -347,6 +359,10 @@ impl Parse for FieldAttrs { this.format = Some(Attr::new(ident.clone(), parse_attr_eq_value(&attr)?)) } else if ident == DISCRIMINANT { this.discriminant = Some(attr.path().clone()); + } else if ident == SANITIZE_LEN_ONLY { + this.sanitize_len_only = Some(attr.path().clone()); + } else if ident == SANITIZE_WITH { + this.sanitize_with = Some(Attr::new(ident.clone(), attr.parse_args()?)); } else { return Err(logged_syn_error( ident.span(), @@ -496,6 +512,19 @@ impl Parse for FieldReadArgs { } } +impl Parse for SanitizeWith { + fn parse(input: ParseStream) -> syn::Result { + let fn_name = input.parse::()?; + let mut inputs = Vec::new(); + while !input.is_empty() { + input.parse::()?; + input.parse::()?; + inputs.push(input.parse::()?); + } + Ok(SanitizeWith { fn_name, inputs }) + } +} + impl Parse for VersionSpec { fn parse(input: ParseStream) -> syn::Result { let fork = input.fork(); @@ -853,3 +882,35 @@ fn parse_if_flag(attr: &syn::Attribute) -> syn::Result { ) }) } + +#[cfg(test)] +mod tests { + use super::*; + + fn parse_field_attrs(input: &str) -> syn::Result { + syn::parse_str(input) + } + + #[test] + fn parse_sanitize_with() { + let attrs = parse_field_attrs("#[sanitize_with(my_custom_sanitize)]").unwrap(); + let sanitize = attrs.sanitize_with.expect("should have sanitize_with"); + assert_eq!(sanitize.attr.fn_name.to_string(), "my_custom_sanitize"); + assert!(sanitize.attr.inputs.is_empty()); + } + + #[test] + fn parse_sanitize_with_args() { + let attrs = + parse_field_attrs("#[sanitize_with(my_custom_sanitize, $value_format)]").unwrap(); + let sanitize = attrs.sanitize_with.expect("should have sanitize_with"); + assert_eq!(sanitize.attr.fn_name.to_string(), "my_custom_sanitize"); + assert_eq!(sanitize.attr.inputs.len(), 1); + assert_eq!(sanitize.attr.inputs[0].to_string(), "value_format"); + } + + #[test] + fn sanitize_with_requires_arg() { + assert!(parse_field_attrs("#[sanitize_with]").is_err()); + } +} diff --git a/font-codegen/src/record.rs b/font-codegen/src/record.rs index 3d8e97f85..c968c9797 100644 --- a/font-codegen/src/record.rs +++ b/font-codegen/src/record.rs @@ -48,6 +48,10 @@ pub(crate) fn generate(item: &Record, all_items: &Items) -> syn::Result syn::Result TokenStream { } } +fn generate_sanitize(item: &Record, needs_read_args: bool) -> syn::Result { + let name = &item.name; + let lifetime = item.lifetime.is_some().then(|| quote!(<'_>)); + let has_offsets = item.fields.iter().any(Field::is_offset_or_array_of_offsets); + + let stmts: Vec<_> = item + .fields + .iter() + .filter_map(Field::sanitize_record_stmt) + .collect(); + let body = quote!( #( #stmts )* ); + + let (args_arg, destructure_args) = match item.attrs.read_args.as_ref() { + Some(args) => { + let typ = args.args_type(); + if has_offsets { + let destructure = args.destructure_pattern_for_sanitize(&body); + let args_args = quote!(args: #typ); + (args_args, Some(destructure)) + } else { + // if we don't contain offsets we don't have a body, so args + // don't matter + (quote!( _args: #typ), None) + } + } + None => (quote!(_args: ()), None), + }; + + let can_skip = (!has_offsets).then(|| { + quote! { + fn can_skip() -> bool { true } + } + }); + let read_args = needs_read_args.then(|| { + quote! { + impl ReadArgs for #name { + type Args = (); + } + } + }); + + Ok(quote! { + #read_args + + impl SanitizeStruct for #name #lifetime { + #can_skip + + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, #args_arg) -> Result<(), ReadError> { + #destructure_args + #( #stmts )* + ctx.finish() + } + } + }) +} + fn generate_traversal(item: &Record) -> syn::Result { let name = &item.name; let name_str = name.to_string(); diff --git a/font-codegen/src/table.rs b/font-codegen/src/table.rs index ec026c7c8..4f96da7c0 100644 --- a/font-codegen/src/table.rs +++ b/font-codegen/src/table.rs @@ -1,15 +1,19 @@ //! codegen for table objects +use std::collections::HashSet; + use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use syn::spanned::Spanned; use crate::{ - parsing::{logged_syn_error, Attr, Field, Table, TableReadArg, TableReadArgs}, + parsing::{ + logged_syn_error, Attr, Condition, Field, Items, Table, TableReadArg, TableReadArgs, + }, Phase, }; -pub(crate) fn generate(item: &Table) -> syn::Result { +pub(crate) fn generate(item: &Table, items: &Items) -> syn::Result { if item.attrs.write_only.is_some() { return Ok(Default::default()); } @@ -50,6 +54,10 @@ pub(crate) fn generate(item: &Table) -> syn::Result { let optional_format_trait_impl = item.impl_format_trait(); let optional_discriminant_trait_impl = item.impl_discriminant_trait(); let font_read = generate_font_read(item)?; + let sanitize = items + .sanitize + .then(|| generate_sanitize(item)) + .transpose()?; let debug = generate_debug(item)?; let top_level = item.attrs.tag.as_ref().map(|tag| { let tag_str = tag.value(); @@ -129,6 +137,9 @@ pub(crate) fn generate(item: &Table) -> syn::Result { #impl_of_unit_type + #sanitize + + #( #docs )* #[derive(Clone)] pub struct #raw_name<'a, #generic_with_default> { @@ -220,6 +231,34 @@ fn generate_font_read(item: &Table) -> syn::Result { }) } +fn generate_sanitize(item: &Table) -> syn::Result { + let name = item.raw_name(); + let stmts = item.iter_sanitze_statements()?; + let body = quote!( #( #stmts )* ); + let (args_type, args_arg, destructure_args) = match item.attrs.read_args.as_ref() { + Some(args) => { + let typ = args.args_type(); + let destructure = args.destructure_pattern_for_sanitize(&body); + let args_args = quote!(args: #typ); + (typ, args_args, Some(destructure)) + } + None => (quote!(()), quote!(_args: ()), None), + }; + + let generic = item.attrs.generic_offset.as_ref(); + let generic_bounds = generic.map(|t| quote!(#t: Sanitize)); + + Ok(quote! { + impl<#generic_bounds> Sanitize for #name<'_, #generic> { + fn sanitize(ctx: &mut SanitizeContext, #args_arg) -> Result<(), ReadError> { + #destructure_args + #( #stmts )* + ctx.finish() + } + } + }) +} + fn generate_debug(item: &Table) -> syn::Result { let name = item.raw_name(); let name_str = name.to_string(); @@ -364,6 +403,56 @@ impl Table { self.fields.sanity_check(phase) } + pub(crate) fn iter_sanitze_statements(&self) -> syn::Result> { + let needed = self.fields_to_read_during_sanitize(); + let generic = self.attrs.generic_offset.as_ref().map(|attr| &attr.attr); + + self.fields + .iter() + .map(|field| field.sanitize_stmt(&needed, generic)) + .collect() + } + + /// A set of idents for fields that are referenced by other fields + fn fields_to_read_during_sanitize(&self) -> HashSet { + let mut needed: HashSet<_> = self + .fields + .iter() + .flat_map(Field::count_arg_names) + .cloned() + .collect(); + + // Version field is needed if any field has #[since_version] + let has_versioned = self.fields.iter().any(Field::is_versioned); + if has_versioned { + if let Some(vf) = self.fields.version_field() { + needed.insert(vf.name.clone()); + } + } + + // Flags fields referenced by #[if_flag] + for field in self.fields.iter() { + if let Some(Condition::IfFlag { + field: flag_field, .. + }) = field.attrs.conditional.as_deref() + { + needed.insert(flag_field.clone()); + } + } + + // Fields referenced by #[read_with] or #[read_offset_with] + for field in self.fields.iter() { + if let Some(args) = &field.attrs.read_with_args { + needed.extend(args.inputs.iter().cloned()); + } + if let Some(args) = &field.attrs.read_offset_args { + needed.extend(args.inputs.iter().cloned()); + } + } + + needed + } + fn iter_field_byte_range_fns(&self) -> impl Iterator + '_ { let mut prev_field_end_expr = quote!(0); let mut iter = self.fields.iter(); @@ -372,14 +461,14 @@ impl Table { let field = iter.next()?; let fn_name = field.shape_byte_range_fn_name(); let len_expr = field.field_len_expr(); - let required_field_decls = field.count_arg_names().map(|fld| { + let required_field_decls = field.count_arg_names().map(|name| { let is_opt = self .fields - .find(fld) + .find(|fld| fld.name == *name) .map(|x| x.is_conditional()) .unwrap_or(false); let maybe_unwrap_or_default = (is_opt).then(|| quote!(.unwrap_or_default())); - quote!(let #fld = self.#fld() #maybe_unwrap_or_default ;) + quote!(let #name = self.#name() #maybe_unwrap_or_default ;) }); // okay so for conditions, how do we evaluate them? @@ -425,7 +514,7 @@ impl Table { } pub(crate) fn impl_format_trait(&self) -> Option { - let field = self.fields.iter().find(|fld| fld.attrs.format.is_some())?; + let field = self.fields.find(|fld| fld.attrs.format.is_some())?; let name = self.raw_name(); let value = &field.attrs.format.as_ref().unwrap(); let typ = field.typ.cooked_type_tokens(); @@ -531,6 +620,39 @@ impl TableReadArgs { } } + /// Like [`destructure_pattern`], but for use in sanitize bodies. + /// + /// A sanitize body only references the subset of read args that participate + /// in sanitization, so any arg ident that does not appear in `body` is bound + /// with a leading underscore to avoid an `unused_variables` warning. ("Used" + /// is derived from the emitted body, so it can never drift from what we + /// generate; an `_`-prefixed binding is still usable, so a false "unused" + /// verdict can never break compilation.) + /// + /// [`destructure_pattern`]: Self::destructure_pattern + pub(crate) fn destructure_pattern_for_sanitize(&self, body: &TokenStream) -> TokenStream { + let mut used = HashSet::new(); + collect_idents(body, &mut used); + let bind = |ident: &syn::Ident| -> syn::Ident { + if used.contains(&ident.to_string()) { + ident.clone() + } else { + quote::format_ident!("_{}", ident) + } + }; + match self.args.as_slice() { + [] => Default::default(), + [TableReadArg { ident, .. }] => { + let binding = bind(ident); + quote!(let #binding = args;) + } + other => { + let bindings = other.iter().map(|arg| bind(&arg.ident)); + quote!( let ( #(#bindings,)* ) = args; ) + } + } + } + pub(crate) fn constructor_args(&self) -> impl Iterator + '_ { self.args .iter() @@ -562,3 +684,16 @@ impl TableReadArgs { }) } } + +/// Recursively collect the string form of every identifier in `tokens`. +fn collect_idents(tokens: &TokenStream, out: &mut HashSet) { + for tt in tokens.clone() { + match tt { + proc_macro2::TokenTree::Ident(id) => { + out.insert(id.to_string()); + } + proc_macro2::TokenTree::Group(g) => collect_idents(&g.stream(), out), + _ => {} + } + } +} diff --git a/read-fonts/generated/generated_test_conditions.rs b/read-fonts/generated/generated_test_conditions.rs index 97c698384..d44eb1fe6 100644 --- a/read-fonts/generated/generated_test_conditions.rs +++ b/read-fonts/generated/generated_test_conditions.rs @@ -29,6 +29,20 @@ impl<'a> FontRead<'a> for MajorMinorVersion<'a> { } } +impl Sanitize for MajorMinorVersion<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let version = ctx.read::()?; + ctx.advance::(); + if version.compatible((1u16, 1u16)) { + ctx.advance::(); + } + if version.compatible((2u16, 0u16)) { + ctx.advance::(); + } + ctx.finish() + } +} + #[derive(Clone)] pub struct MajorMinorVersion<'a> { data: FontData<'a>, @@ -465,6 +479,20 @@ impl<'a> FontRead<'a> for FlagDay<'a> { } } +impl Sanitize for FlagDay<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + let flags = ctx.read::()?; + if flags.contains(GotFlags::FOO) { + ctx.advance::(); + } + if flags.contains(GotFlags::BAR) { + ctx.advance::(); + } + ctx.finish() + } +} + #[derive(Clone)] pub struct FlagDay<'a> { data: FontData<'a>, diff --git a/read-fonts/generated/generated_test_count_all.rs b/read-fonts/generated/generated_test_count_all.rs index 5a1ab9003..59714e434 100644 --- a/read-fonts/generated/generated_test_count_all.rs +++ b/read-fonts/generated/generated_test_count_all.rs @@ -29,6 +29,14 @@ impl<'a> FontRead<'a> for CountAll16<'a> { } } +impl Sanitize for CountAll16<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + sanitize_remainder(ctx)?; + ctx.finish() + } +} + #[derive(Clone)] pub struct CountAll16<'a> { data: FontData<'a>, @@ -119,6 +127,14 @@ impl<'a> FontRead<'a> for CountAll32<'a> { } } +impl Sanitize for CountAll32<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + sanitize_remainder(ctx)?; + ctx.finish() + } +} + #[derive(Clone)] pub struct CountAll32<'a> { data: FontData<'a>, diff --git a/read-fonts/generated/generated_test_enum.rs b/read-fonts/generated/generated_test_enum.rs index e5879ed8b..0625a4345 100644 --- a/read-fonts/generated/generated_test_enum.rs +++ b/read-fonts/generated/generated_test_enum.rs @@ -118,6 +118,19 @@ impl FixedSize for MyRecord { const RAW_BYTE_LEN: usize = MyEnum1::RAW_BYTE_LEN + MyEnum2::RAW_BYTE_LEN; } +impl ReadArgs for MyRecord { + type Args = (); +} + +impl SanitizeStruct for MyRecord { + fn can_skip() -> bool { + true + } + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: ()) -> Result<(), ReadError> { + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for MyRecord { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { diff --git a/read-fonts/generated/generated_test_formats.rs b/read-fonts/generated/generated_test_formats.rs index 0bb85f1e1..2211e2aff 100644 --- a/read-fonts/generated/generated_test_formats.rs +++ b/read-fonts/generated/generated_test_formats.rs @@ -33,6 +33,15 @@ impl<'a> FontRead<'a> for Table1<'a> { } } +impl Sanitize for Table1<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + ctx.advance::(); + ctx.advance::(); + ctx.finish() + } +} + #[derive(Clone)] pub struct Table1<'a> { data: FontData<'a>, @@ -138,6 +147,15 @@ impl<'a> FontRead<'a> for Table2<'a> { } } +impl Sanitize for Table2<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + let value_count = ctx.read::()?; + ctx.sanitize_array::(transforms::to_usize(value_count))?; + ctx.finish() + } +} + #[derive(Clone)] pub struct Table2<'a> { data: FontData<'a>, @@ -234,6 +252,14 @@ impl<'a> FontRead<'a> for Table3<'a> { } } +impl Sanitize for Table3<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + ctx.advance::(); + ctx.finish() + } +} + #[derive(Clone)] pub struct Table3<'a> { data: FontData<'a>, @@ -354,6 +380,18 @@ impl<'a> MinByteRange<'a> for MyTable<'a> { } } +impl Sanitize for MyTable<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let format: u16 = ctx.peek_at(0usize)?; + match format { + Table1::FORMAT => Table1::sanitize(ctx, ()), + Table2::FORMAT => Table2::sanitize(ctx, ()), + Table3::FORMAT => Table3::sanitize(ctx, ()), + other => Err(ReadError::InvalidFormat(other.into())), + } + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> MyTable<'a> { fn dyn_inner<'b>(&'b self) -> &'b dyn SomeTable<'a> { diff --git a/read-fonts/generated/generated_test_generic_group.rs b/read-fonts/generated/generated_test_generic_group.rs index 5c918c8d6..e4244efb1 100644 --- a/read-fonts/generated/generated_test_generic_group.rs +++ b/read-fonts/generated/generated_test_generic_group.rs @@ -49,6 +49,15 @@ impl<'a, T> MyLookup<'a, T> { } } +impl> Sanitize for MyLookup<'_, T> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + let sub_table_count = ctx.read::()?; + ctx.sanitize_array_of_offsets::(transforms::to_usize(sub_table_count), ())?; + ctx.finish() + } +} + /// A generic table parameterized by the type of its subtable offsets. #[derive(Clone)] pub struct MyLookup<'a, T = ()> { @@ -207,6 +216,17 @@ impl<'a> MinByteRange<'a> for MySubtable<'a> { } } +impl Sanitize for MySubtable<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let format: u16 = ctx.peek_at(0usize)?; + match format { + MySubtableFormat1::FORMAT => MySubtableFormat1::sanitize(ctx, ()), + MySubtableFormat2::FORMAT => MySubtableFormat2::sanitize(ctx, ()), + other => Err(ReadError::InvalidFormat(other.into())), + } + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> MySubtable<'a> { fn dyn_inner<'b>(&'b self) -> &'b dyn SomeTable<'a> { @@ -262,6 +282,14 @@ impl<'a> FontRead<'a> for MySubtableFormat1<'a> { } } +impl Sanitize for MySubtableFormat1<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + ctx.advance::(); + ctx.finish() + } +} + #[derive(Clone)] pub struct MySubtableFormat1<'a> { data: FontData<'a>, @@ -357,6 +385,15 @@ impl<'a> FontRead<'a> for MySubtableFormat2<'a> { } } +impl Sanitize for MySubtableFormat2<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + let count = ctx.read::()?; + ctx.sanitize_array::(transforms::to_usize(count))?; + ctx.finish() + } +} + #[derive(Clone)] pub struct MySubtableFormat2<'a> { data: FontData<'a>, @@ -465,6 +502,17 @@ impl<'a> MyLookupGroup<'a> { } } +impl Sanitize for MyLookupGroup<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let discriminant = MyLookup::read_discriminant(ctx.data())?; + match discriminant { + 1 => MyLookup::::sanitize(ctx, _args), + 2 => MyLookup::::sanitize(ctx, _args), + other => Err(ReadError::InvalidFormat(other as _)), + } + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> MyLookupGroup<'a> { fn dyn_inner(&self) -> &(dyn SomeTable<'a> + 'a) { @@ -516,6 +564,14 @@ impl<'a> FontRead<'a> for ContainsLookupGroup<'a> { } } +impl Sanitize for ContainsLookupGroup<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + ctx.sanitize_offset::(())?; + ctx.finish() + } +} + #[derive(Clone)] pub struct ContainsLookupGroup<'a> { data: FontData<'a>, diff --git a/read-fonts/generated/generated_test_offsets_arrays.rs b/read-fonts/generated/generated_test_offsets_arrays.rs index 2a9bb5544..5d5994f9f 100644 --- a/read-fonts/generated/generated_test_offsets_arrays.rs +++ b/read-fonts/generated/generated_test_offsets_arrays.rs @@ -29,6 +29,39 @@ impl<'a> FontRead<'a> for KindsOfOffsets<'a> { } } +impl Sanitize for KindsOfOffsets<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let version = ctx.read::()?; + ctx.sanitize_offset::(())?; + ctx.sanitize_offset::(())?; + let array_offset_count = ctx.read::()?; + ctx.sanitize_offset_to_array::, _>( + array_offset_count, + false, + |_, _| Ok(()), + )?; + ctx.sanitize_offset_to_array::( + array_offset_count, + false, + |t, ctx| t.sanitize_struct(ctx, ()), + )?; + if version.compatible((1u16, 1u16)) { + ctx.sanitize_offset_to_array::( + array_offset_count, + false, + |t, ctx| t.sanitize_struct(ctx, ()), + )?; + } + if version.compatible((1u16, 1u16)) { + ctx.sanitize_offset::(())?; + } + if version.compatible((1u16, 1u16)) { + ctx.sanitize_offset::(())?; + } + ctx.finish() + } +} + #[derive(Clone)] pub struct KindsOfOffsets<'a> { data: FontData<'a>, @@ -317,6 +350,22 @@ impl<'a> FontRead<'a> for KindsOfArraysOfOffsets<'a> { } } +impl Sanitize for KindsOfArraysOfOffsets<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let version = ctx.read::()?; + let count = ctx.read::()?; + ctx.sanitize_array_of_offsets::(transforms::to_usize(count), ())?; + ctx.sanitize_array_of_offsets::(transforms::to_usize(count), ())?; + if version.compatible((1u16, 1u16)) { + ctx.sanitize_array_of_offsets::(transforms::to_usize(count), ())?; + } + if version.compatible((1u16, 1u16)) { + ctx.sanitize_array_of_offsets::(transforms::to_usize(count), ())?; + } + ctx.finish() + } +} + #[derive(Clone)] pub struct KindsOfArraysOfOffsets<'a> { data: FontData<'a>, @@ -518,6 +567,22 @@ impl<'a> FontRead<'a> for KindsOfArrays<'a> { } } +impl Sanitize for KindsOfArrays<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let version = ctx.read::()?; + let count = ctx.read::()?; + ctx.sanitize_array::(transforms::to_usize(count))?; + ctx.sanitize_array_of_structs::(transforms::to_usize(count), ())?; + if version.compatible(1u16) { + ctx.sanitize_array::(transforms::to_usize(count))?; + } + if version.compatible(1u16) { + ctx.sanitize_array_of_structs::(transforms::to_usize(count), ())?; + } + ctx.finish() + } +} + #[derive(Clone)] pub struct KindsOfArrays<'a> { data: FontData<'a>, @@ -693,6 +758,15 @@ impl<'a> FontRead<'a> for VarLenHaver<'a> { } } +impl Sanitize for VarLenHaver<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let count = ctx.read::()?; + ctx.sanitize_var_len_array::(count as _, false)?; + ctx.advance::(); + ctx.finish() + } +} + #[derive(Clone)] pub struct VarLenHaver<'a> { data: FontData<'a>, @@ -802,6 +876,14 @@ impl<'a> FontRead<'a> for Dummy<'a> { } } +impl Sanitize for Dummy<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + ctx.advance::(); + ctx.finish() + } +} + #[derive(Clone)] pub struct Dummy<'a> { data: FontData<'a>, @@ -883,6 +965,19 @@ impl FixedSize for Shmecord { const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + u32::RAW_BYTE_LEN; } +impl ReadArgs for Shmecord { + type Args = (); +} + +impl SanitizeStruct for Shmecord { + fn can_skip() -> bool { + true + } + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: ()) -> Result<(), ReadError> { + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for Shmecord { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { diff --git a/read-fonts/generated/generated_test_read_args.rs b/read-fonts/generated/generated_test_read_args.rs index 4fe8ca2df..e58a32feb 100644 --- a/read-fonts/generated/generated_test_read_args.rs +++ b/read-fonts/generated/generated_test_read_args.rs @@ -45,6 +45,17 @@ impl<'a> BaseArray<'a> { } } +impl Sanitize for BaseArray<'_> { + fn sanitize(ctx: &mut SanitizeContext, args: u16) -> Result<(), ReadError> { + let mark_class_count = args; + let base_count = ctx.read::()?; + ctx.sanitize_computed_array::(base_count as _, mark_class_count, false)?; + let face_count = ctx.read::()?; + ctx.sanitize_computed_array::(face_count as _, mark_class_count, true)?; + ctx.finish() + } +} + #[derive(Clone)] pub struct BaseArray<'a> { data: FontData<'a>, @@ -109,10 +120,10 @@ impl<'a> BaseArray<'a> { } pub fn face_records_byte_range(&self) -> Range { - let base_count = self.base_count(); + let face_count = self.face_count(); let start = self.face_count_byte_range().end; let end = start - + (transforms::to_usize(base_count)).saturating_mul( + + (transforms::to_usize(face_count)).saturating_mul( ::compute_size(self.mark_class_count()).unwrap_or(0), ); start..end @@ -220,6 +231,15 @@ impl<'a> BaseRecord<'a> { } } +impl SanitizeStruct for BaseRecord<'_> { + fn can_skip() -> bool { + true + } + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: u16) -> Result<(), ReadError> { + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for BaseRecord<'a> { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { @@ -291,6 +311,14 @@ impl<'a> FaceRecord<'a> { } } +impl SanitizeStruct for FaceRecord<'_> { + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, args: u16) -> Result<(), ReadError> { + let _mark_class_count = args; + self.face_offsets().sanitize_offset::(ctx, ())?; + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for FaceRecord<'a> { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { @@ -332,6 +360,13 @@ impl<'a> FontRead<'a> for Face<'a> { } } +impl Sanitize for Face<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + ctx.finish() + } +} + #[derive(Clone)] pub struct Face<'a> { data: FontData<'a>, diff --git a/read-fonts/generated/generated_test_records.rs b/read-fonts/generated/generated_test_records.rs index e9bf69420..45d754960 100644 --- a/read-fonts/generated/generated_test_records.rs +++ b/read-fonts/generated/generated_test_records.rs @@ -29,6 +29,21 @@ impl<'a> FontRead<'a> for BasicTable<'a> { } } +impl Sanitize for BasicTable<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + let simple_count = ctx.read::()?; + ctx.sanitize_array_of_structs::(transforms::to_usize(simple_count), ())?; + let arrays_inner_count = ctx.read::()?; + let array_records_count = ctx.read::()?; + ctx.sanitize_computed_array::( + array_records_count as _, + arrays_inner_count, + true, + )?; + ctx.finish() + } +} + #[derive(Clone)] pub struct BasicTable<'a> { data: FontData<'a>, @@ -178,6 +193,19 @@ impl FixedSize for SimpleRecord { const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + u32::RAW_BYTE_LEN; } +impl ReadArgs for SimpleRecord { + type Args = (); +} + +impl SanitizeStruct for SimpleRecord { + fn can_skip() -> bool { + true + } + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: ()) -> Result<(), ReadError> { + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for SimpleRecord { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { @@ -253,6 +281,15 @@ impl<'a> ContainsArrays<'a> { } } +impl SanitizeStruct for ContainsArrays<'_> { + fn can_skip() -> bool { + true + } + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: u16) -> Result<(), ReadError> { + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for ContainsArrays<'a> { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { @@ -317,6 +354,23 @@ impl FixedSize for ContainsOffsets { const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + Offset16::RAW_BYTE_LEN + Offset32::RAW_BYTE_LEN; } +impl ReadArgs for ContainsOffsets { + type Args = (); +} + +impl SanitizeStruct for ContainsOffsets { + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: ()) -> Result<(), ReadError> { + ctx.sanitize_resolved_offset_to_array::<_, SimpleRecord, _>( + self.array_offset(), + self.off_array_count(), + false, + |t, ctx| t.sanitize_struct(ctx, ()), + )?; + self.other_offset().sanitize_offset::(ctx, ())?; + ctx.finish() + } +} + #[cfg(feature = "experimental_traverse")] impl<'a> SomeRecord<'a> for ContainsOffsets { fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { @@ -368,6 +422,14 @@ impl<'a> FontRead<'a> for VarLenItem<'a> { } } +impl Sanitize for VarLenItem<'_> { + fn sanitize(ctx: &mut SanitizeContext, _args: ()) -> Result<(), ReadError> { + ctx.advance::(); + sanitize_data(ctx)?; + ctx.finish() + } +} + #[derive(Clone)] pub struct VarLenItem<'a> { data: FontData<'a>, @@ -433,3 +495,196 @@ impl<'a> std::fmt::Debug for VarLenItem<'a> { (self as &dyn SomeTable<'a>).fmt(f) } } + +#[derive(Clone, Debug, Copy, bytemuck :: AnyBitPattern)] +#[repr(C)] +#[repr(packed)] +pub struct HasOffsetsWithArgs { + pub merp_len: BigEndian, + /// Read an offset that takes an argument, in a record + pub feature_offset: BigEndian, + /// custom offset getter in a record + pub fake_offset: BigEndian, +} + +impl HasOffsetsWithArgs { + pub fn merp_len(&self) -> u16 { + self.merp_len.get() + } + + /// Read an offset that takes an argument, in a record + pub fn feature_offset(&self) -> Offset16 { + self.feature_offset.get() + } + + /// Read an offset that takes an argument, in a record + /// + /// The `data` argument should be retrieved from the parent table + /// By calling its `offset_data` method. + pub fn feature<'a>(&self, data: FontData<'a>) -> Result, ReadError> { + let args = self.merp_len(); + self.feature_offset().resolve_with_args(data, args) + } + + /// custom offset getter in a record + pub fn fake_offset(&self) -> Offset16 { + self.fake_offset.get() + } +} + +impl FixedSize for HasOffsetsWithArgs { + const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + Offset16::RAW_BYTE_LEN + Offset16::RAW_BYTE_LEN; +} + +impl ReadArgs for HasOffsetsWithArgs { + type Args = (); +} + +impl SanitizeStruct for HasOffsetsWithArgs { + fn sanitize_struct(&self, ctx: &mut SanitizeContext<'_>, _args: ()) -> Result<(), ReadError> { + self.feature_offset() + .sanitize_offset::(ctx, self.merp_len())?; + self.sanitize_fake_offset(ctx)?; + ctx.finish() + } +} + +#[cfg(feature = "experimental_traverse")] +impl<'a> SomeRecord<'a> for HasOffsetsWithArgs { + fn traverse(self, data: FontData<'a>) -> RecordResolver<'a> { + RecordResolver { + name: "HasOffsetsWithArgs", + get_field: Box::new(move |idx, _data| match idx { + 0usize => Some(Field::new("merp_len", self.merp_len())), + 1usize => Some(Field::new( + "feature_offset", + FieldType::offset(self.feature_offset(), self.feature(_data)), + )), + 2usize => Some(Field::new( + "fake_offset", + FieldType::offset(self.fake_offset(), self.fake(_data)), + )), + _ => None, + }), + data, + } + } +} + +impl<'a> MinByteRange<'a> for HasReadArgs<'a> { + fn min_byte_range(&self) -> Range { + 0..self.merps_byte_range().end + } + fn min_table_bytes(&self) -> &'a [u8] { + let range = self.min_byte_range(); + self.data.as_bytes().get(range).unwrap_or_default() + } +} + +impl ReadArgs for HasReadArgs<'_> { + type Args = u16; +} + +impl<'a> FontRead<'a> for HasReadArgs<'a> { + fn read_with_args(data: FontData<'a>, args: u16) -> Result { + let merp_len = args; + + #[allow(clippy::absurd_extreme_comparisons)] + if data.len() < Self::MIN_SIZE { + return Err(ReadError::OutOfBounds); + } + Ok(Self { data, merp_len }) + } +} + +impl<'a> HasReadArgs<'a> { + /// A constructor that requires additional arguments. + /// + /// This type requires some external state in order to be + /// parsed. + pub fn read(data: FontData<'a>, merp_len: u16) -> Result { + let args = merp_len; + Self::read_with_args(data, args) + } +} + +impl Sanitize for HasReadArgs<'_> { + fn sanitize(ctx: &mut SanitizeContext, args: u16) -> Result<(), ReadError> { + let merp_len = args; + ctx.advance::(); + ctx.sanitize_array::(transforms::to_usize(merp_len))?; + ctx.finish() + } +} + +#[derive(Clone)] +pub struct HasReadArgs<'a> { + data: FontData<'a>, + merp_len: u16, +} + +#[allow(clippy::needless_lifetimes)] +impl<'a> HasReadArgs<'a> { + pub const MIN_SIZE: usize = u16::RAW_BYTE_LEN; + basic_table_impls!(impl_the_methods); + + pub fn derp(&self) -> u16 { + let range = self.derp_byte_range(); + self.data.read_at(range.start).ok().unwrap() + } + + pub fn merps(&self) -> &'a [BigEndian] { + let range = self.merps_byte_range(); + self.data.read_array(range).ok().unwrap_or_default() + } + + pub(crate) fn merp_len(&self) -> u16 { + self.merp_len + } + + pub fn derp_byte_range(&self) -> Range { + let start = 0; + let end = start + u16::RAW_BYTE_LEN; + start..end + } + + pub fn merps_byte_range(&self) -> Range { + let merp_len = self.merp_len(); + let start = self.derp_byte_range().end; + let end = start + (transforms::to_usize(merp_len)).saturating_mul(i16::RAW_BYTE_LEN); + start..end + } +} + +const _: () = assert!(FontData::default_data_long_enough(HasReadArgs::MIN_SIZE)); + +impl Default for HasReadArgs<'_> { + fn default() -> Self { + Self { + data: FontData::default_table_data(), + merp_len: Default::default(), + } + } +} + +#[cfg(feature = "experimental_traverse")] +impl<'a> SomeTable<'a> for HasReadArgs<'a> { + fn type_name(&self) -> &str { + "HasReadArgs" + } + fn get_field(&self, idx: usize) -> Option> { + match idx { + 0usize => Some(Field::new("derp", self.derp())), + 1usize => Some(Field::new("merps", self.merps())), + _ => None, + } + } +} + +#[cfg(feature = "experimental_traverse")] +#[allow(clippy::needless_lifetimes)] +impl<'a> std::fmt::Debug for HasReadArgs<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + (self as &dyn SomeTable<'a>).fmt(f) + } +} diff --git a/read-fonts/src/codegen_test.rs b/read-fonts/src/codegen_test.rs index 147e94780..365ed4a45 100644 --- a/read-fonts/src/codegen_test.rs +++ b/read-fonts/src/codegen_test.rs @@ -10,6 +10,23 @@ pub mod records { include!("../generated/generated_test_records.rs"); + #[allow(dead_code)] + fn sanitize_data(_ctx: &mut SanitizeContext) -> Result<(), ReadError> { + Ok(()) + } + + impl HasOffsetsWithArgs { + #[allow(dead_code)] + fn sanitize_fake_offset(&self, ctx: &mut SanitizeContext) -> Result<(), ReadError> { + self.fake_offset().sanitize_offset::(ctx, 0) + } + } + + impl HasOffsetsWithArgs { + pub fn fake<'a>(&self, data: FontData<'a>) -> Result, ReadError> { + self.fake_offset().resolve_with_args(data, 0) + } + } } pub mod formats { @@ -41,6 +58,20 @@ pub mod offsets_arrays { type Args = (); } + impl SanitizeStruct for VarSizeDummy<'_> { + fn can_skip() -> bool { + true + } + + fn sanitize_struct( + &self, + _ctx: &mut SanitizeContext<'_>, + _args: (), + ) -> Result<(), ReadError> { + Ok(()) + } + } + impl<'a> FontRead<'a> for VarSizeDummy<'a> { fn read_with_args(data: FontData<'a>, _: ()) -> Result { let count: u16 = data.read_at(0)?; @@ -205,6 +236,10 @@ pub mod count_all { let count32 = CountAll32::read(data).unwrap(); assert_eq!(count32.remainder().len(), remainder_len / 4); } + + fn sanitize_remainder(_ctx: &mut SanitizeContext) -> Result<(), ReadError> { + Ok(()) + } } pub mod conditions { @@ -367,3 +402,234 @@ pub mod generic_group { } } } + +#[cfg(test)] +mod sanitize_tests { + use crate::{ + codegen_test::{offsets_arrays::*, records::*}, + sanitize::{Sanitize, SanitizeContext, SanitizeState}, + FontData, ReadError, + }; + use font_test_data::bebuffer::BeBuffer; + use font_types::MajorMinor; + + fn sanitize>(data: &[u8]) -> Result<(), ReadError> { + let mut state = SanitizeState::default(); + let mut ctx = SanitizeContext::new(FontData::new(data), &mut state); + T::sanitize(&mut ctx, ()) + } + + // --- KindsOfOffsets (v1.0) layout: --- + // MajorMinor(4) + nonnullable Offset16(2) + nullable Offset16(2) + + // array_offset_count u16(2) + array_offset Offset16(2) + record_array_offset Offset16(2) + // = 14 byte header + + /// A valid Dummy subtable (value: u16 + _reserved: u16 = 4 bytes) + const DUMMY_BYTES: [u16; 2] = [0xdead, 0x0000]; + + #[test] + fn simple_offsets_valid() { + // nonnullable → Dummy at offset 14, everything else null/zero + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(14u16) // nonnullable → Dummy right after header + .push(0u16) // nullable (null) + .push(0u16) // array_offset_count = 0 + .push(0u16) // array_offset (null, count is 0) + .push(0u16) // record_array_offset (null, count is 0) + // Dummy subtable + .extend(DUMMY_BYTES); + let result = sanitize::(buf.data()); + assert!(result.is_ok(), "expected Ok, got {result:?}"); + } + + #[test] + fn null_offset_is_not_an_error() { + // All simple offsets null — sanitize skips null offsets + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(0u16) // nonnullable (null — sanitize treats null as skip) + .push(0u16) // nullable (null) + .push(0u16) // array_offset_count = 0 + .push(0u16) // array_offset (null) + .push(0u16); // record_array_offset (null) + let result = sanitize::(buf.data()); + assert!(result.is_ok(), "expected Ok, got {result:?}"); + } + + #[test] + fn offset_out_of_bounds() { + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(9999u16) // nonnullable → way past end + .push(0u16) // nullable (null) + .push(0u16) // count = 0 + .push(0u16) // array_offset + .push(0u16); // record_array_offset + assert!(sanitize::(buf.data()).is_err()); + } + + #[test] + fn subtable_too_small() { + // Offset points to valid position but only 2 bytes of data (Dummy needs 4) + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(14u16) // nonnullable → offset 14 + .push(0u16) + .push(0u16) + .push(0u16) + .push(0u16) + // Only 2 bytes — Dummy needs 4 + .push(0xdeadu16); + assert!(sanitize::(buf.data()).is_err()); + } + + #[test] + fn offset_to_scalar_array_valid() { + // array_offset_count = 2, array_offset → [u16; 2], record_array_offset null + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(0u16) // nonnullable (null) + .push(0u16) // nullable (null) + .push(2u16) // array_offset_count = 2 + .push(14u16) // array_offset → right after header + .push(0u16) // record_array_offset (null) + // 2 u16 values + .extend([0x1111u16, 0x2222]); + assert!(sanitize::(buf.data()).is_ok()); + } + + #[test] + fn offset_to_record_array_valid() { + // Shmecord = u16(2) + u32(4) = 6 bytes each + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(0u16) // nonnullable (null) + .push(0u16) // nullable (null) + .push(1u16) // array_offset_count = 1 + .push(0u16) // array_offset (null) + .push(14u16) // record_array_offset → right after header + // 1 Shmecord + .push(42u16) + .push(99u32); + assert!(sanitize::(buf.data()).is_ok()); + } + + #[test] + fn offset_to_array_count_overflows() { + // count = 0xFFFF but only a few bytes of data + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(0u16) + .push(0u16) + .push(0xFFFFu16) // array_offset_count = huge + .push(14u16) // array_offset → valid position + .push(0u16) + // Only 4 bytes of data, not enough for 65535 u16s + .extend([1u16, 2]); + assert!(sanitize::(buf.data()).is_err()); + } + + // --- KindsOfArraysOfOffsets (v1.0) layout: --- + // MajorMinor(4) + count u16(2) + nonnullable_offsets [Offset16]*count + + // nullable_offsets [Offset16]*count + // (versioned fields skipped for v1.0) + + #[test] + fn array_of_offsets_valid() { + // 2 nonnullable offsets → valid Dummies, 2 nullable offsets (null) + let header_size = 4 + 2 + 2 * 2 + 2 * 2; // = 14 + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(2u16) // count + // nonnullable offsets → Dummy at header_size and header_size+4 + .push(header_size as u16) + .push((header_size + 4) as u16) + // nullable offsets (both null) + .push(0u16) + .push(0u16) + // Dummy 0 + .extend(DUMMY_BYTES) + // Dummy 1 + .extend(DUMMY_BYTES); + assert!(sanitize::(buf.data()).is_ok()); + } + + #[test] + fn array_of_offsets_one_bad() { + let header_size = 4 + 2 + 2 * 2 + 2 * 2; // = 14 + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(2u16) // count + // first offset valid, second out of bounds + .push(header_size as u16) + .push(0xFFFFu16) + // nullable offsets (null) + .push(0u16) + .push(0u16) + // Only one Dummy + .extend(DUMMY_BYTES); + assert!(sanitize::(buf.data()).is_err()); + } + + #[test] + fn malformed_subtable_propagates() { + // Offset points to a Dummy that's only 2 bytes (needs 4) + let header_size = 4 + 2 + 2 + 2; // = 10 + let buf = BeBuffer::new() + .push(MajorMinor::VERSION_1_0) + .push(1u16) // count = 1 + .push(header_size as u16) // nonnullable → offset 10 + .push(0u16) // nullable (null) + // Truncated Dummy — only 2 bytes + .push(0xdeadu16); + assert!(sanitize::(buf.data()).is_err()); + } + + // --- BasicTable layout: --- + // simple_count u16(2) + [SimpleRecord]*simple_count + + // arrays_inner_count u16(2) + array_records_count u32(4) + + // ComputedArray + // + // SimpleRecord = u16(2) + u32(4) = 6 bytes + // ContainsArrays(array_len=N) = [u16]*N + [SimpleRecord]*N + + #[test] + fn computed_array_valid() { + // simple_count=1, one SimpleRecord, arrays_inner_count=1, + // array_records_count=1, one ContainsArrays with 1 scalar + 1 SimpleRecord + let buf = BeBuffer::new() + .push(1u16) // simple_count + // SimpleRecord + .push(1u16) + .push(2u32) + // arrays_inner_count + .push(1u16) + // array_records_count + .push(1u32) + // ContainsArrays { scalars: [u16; 1], records: [SimpleRecord; 1] } + .push(42u16) // scalar + .push(10u16) // SimpleRecord.val1 + .push(20u32); // SimpleRecord.va2 + assert!(sanitize::(buf.data()).is_ok()); + } + + #[test] + fn computed_array_truncated() { + // array_records_count=1 but not enough data for the ContainsArrays + let buf = BeBuffer::new() + .push(0u16) // simple_count = 0 + .push(2u16) // arrays_inner_count = 2 (each ContainsArrays has 2 scalars + 2 records) + .push(1u32) // array_records_count = 1 + // Only 2 bytes — not enough for ContainsArrays(2) + .push(0u16); + assert!(sanitize::(buf.data()).is_err()); + } + + #[test] + fn data_too_short_for_header() { + // Not even enough bytes for the version field + let buf = BeBuffer::new().push(0u16); + assert!(sanitize::(buf.data()).is_err()); + } +} diff --git a/read-fonts/src/font_data.rs b/read-fonts/src/font_data.rs index ba6a24c64..e5d85e1d4 100644 --- a/read-fonts/src/font_data.rs +++ b/read-fonts/src/font_data.rs @@ -28,7 +28,7 @@ pub struct FontData<'a> { #[derive(Debug, Default, Clone, Copy)] pub struct Cursor<'a> { pos: usize, - data: FontData<'a>, + pub(crate) data: FontData<'a>, } // we reuse a single buffer for all tables, but it gets padded with a u16 diff --git a/read-fonts/src/lib.rs b/read-fonts/src/lib.rs index bdb466289..69e9a38bf 100644 --- a/read-fonts/src/lib.rs +++ b/read-fonts/src/lib.rs @@ -87,6 +87,9 @@ pub mod tables; #[cfg(feature = "experimental_traverse")] pub mod traversal; +#[cfg(any(test, feature = "codegen_test"))] +mod sanitize; + #[cfg(any(test, feature = "codegen_test"))] pub mod codegen_test; @@ -110,6 +113,8 @@ pub(crate) mod codegen_prelude { pub use crate::read::{ ComputeSize, Discriminant, FontRead, Format, ReadArgs, ReadError, VarSize, }; + #[cfg(any(test, feature = "codegen_test"))] + pub(crate) use crate::sanitize::{Sanitize, SanitizeContext, SanitizeOffset, SanitizeStruct}; pub use crate::table_provider::TopLevelTable; pub use crate::table_ref::MinByteRange; pub use std::ops::Range; diff --git a/read-fonts/src/read.rs b/read-fonts/src/read.rs index dd1036413..978204285 100644 --- a/read-fonts/src/read.rs +++ b/read-fonts/src/read.rs @@ -50,7 +50,7 @@ pub trait FontRead<'a>: Sized + ReadArgs { /// This is separate from [`FontRead`] so that it can also be a supertrait of /// [`ComputeSize`], which does not need a lifetime. pub trait ReadArgs { - type Args: Copy; + type Args: Copy + 'static; } /// A trait for tables that have multiple possible formats. diff --git a/read-fonts/src/sanitize.rs b/read-fonts/src/sanitize.rs new file mode 100644 index 000000000..217939c00 --- /dev/null +++ b/read-fonts/src/sanitize.rs @@ -0,0 +1,335 @@ +//! the traits we'll need to generate for sanitize + +#![allow(dead_code)] // just until sanitize P.2 lands +use bytemuck::AnyBitPattern; +use types::{BigEndian, FixedSize, Nullable, Scalar}; + +use crate::{ + array::VarLenArray, font_data::Cursor, read::VarSize, ComputeSize, FontData, FontRead, Offset, + ReadArgs, ReadError, ResolveOffset, +}; + +/// The bytes of the current table being sanitized, along with shared sanitize state. +/// +/// This is bundled together because we need to update the shared state as we +/// navigate the bytes. +pub struct SanitizeContext<'a> { + cursor: Cursor<'a>, + state: &'a mut SanitizeState, +} + +/// State tracked during during a sanitize pass +#[derive(Clone, Debug, Default)] +pub(crate) struct SanitizeState { + // only used in COLRv1 + _recursion_depth: u32, + // gpos/gsub + _subtable_depth: u32, + _max_ops: u32, + // some stuff goes in here? +} + +impl<'a> SanitizeContext<'a> { + #[cfg(test)] + pub(crate) fn new(data: FontData<'a>, state: &'a mut SanitizeState) -> Self { + Self { + cursor: data.cursor(), + state, + } + } + + pub(crate) fn data(&self) -> FontData<'a> { + self.cursor.data + } + + /// Read a scalar and advance the cursor + pub(crate) fn read(&mut self) -> Result { + self.cursor.read() + } + + /// Read a scalar at a specific offset without advancing the cursor. + /// + /// the position is absolute in the underlying data; this is only expected + /// to be called when parsing a format group. + pub(crate) fn peek_at(&self, offset: usize) -> Result { + assert_eq!(self.cursor.position(), Ok(0)); + self.cursor.data.read_at(offset) + } + + /// Recursively sanitize an offset, and advance the cursor + pub(crate) fn sanitize_offset(&mut self, args: T::Args) -> Result<(), ReadError> + where + O: Offset + Scalar, + T: Sanitize, + { + let offset = self.read::()?; + self.descend_into_offset(offset, |ctx| T::sanitize(ctx, args)) + } + + /// Track state while descending into a child offset. + /// + /// Most importantly, this updates the context's data so it points to the + /// the new offset's position, so that any subsequent offsets are resolved + /// relative to that. + fn descend_into_offset( + &mut self, + offset: impl Offset, + f: impl FnOnce(&mut SanitizeContext) -> Result<(), ReadError>, + ) -> Result<(), ReadError> { + let offset = match offset.to_usize() { + 0 => return Ok(()), + other => other, + }; + + let offset_data = self + .cursor + .data + .split_off(offset) + .ok_or(ReadError::OutOfBounds)?; + + //TODO: track descent here? + let mut child_ctx = SanitizeContext { + cursor: offset_data.cursor(), + state: self.state, + }; + + f(&mut child_ctx) + } + + /// Advance the cursor past a scalar + pub(crate) fn advance(&mut self) { + self.cursor.advance::(); + } + + /// Advance the cursor by an arbitrary number of bytes + pub(crate) fn advance_by(&mut self, n_bytes: usize) { + self.cursor.advance_by(n_bytes); + } + + /// advance the cursor by the length of the array, if the length doesn't overflow. + pub(crate) fn sanitize_array(&mut self, count: usize) -> Result<(), ReadError> { + let len = count + .checked_mul(T::RAW_BYTE_LEN) + .ok_or(ReadError::OutOfBounds)?; + self.advance_by(len); + Ok(()) + } + + /// Advance the cursor by the length of the array, and recursively visit the offsets + pub(crate) fn sanitize_array_of_offsets( + &mut self, + count: usize, + args: T::Args, + ) -> Result<(), ReadError> + where + O: Offset + Scalar, + T: Sanitize, + BigEndian: AnyBitPattern + FixedSize, + { + let array = self.cursor.read_array::>(count)?; + array.sanitize_offset::(self, args) + } + + /// Sanitize an offset that points to an array. + /// + /// this has a slightly funny signature because it needs to handle both + /// scalar and struct members, and the structs might need to be recursed + pub(crate) fn sanitize_offset_to_array( + &mut self, + count: u16, + len_only: bool, + f: F, + ) -> Result<(), ReadError> + where + O: Offset + Scalar, + T: AnyBitPattern + FixedSize, + F: Fn(&T, &mut SanitizeContext) -> Result<(), ReadError>, + { + let offset = self.read::()?; + self.sanitize_resolved_offset_to_array(offset, count, len_only, f) + } + + /// Sanitize an offset-to-array where we already have the offset value. + /// + /// Used in records, where the offset is accessed via a getter rather than + /// read from the cursor. + pub(crate) fn sanitize_resolved_offset_to_array( + &mut self, + offset: O, + count: u16, + len_only: bool, + f: F, + ) -> Result<(), ReadError> + where + O: Offset + Scalar, + T: AnyBitPattern + FixedSize, + F: Fn(&T, &mut SanitizeContext) -> Result<(), ReadError>, + { + if offset.to_usize() == 0 { + return Ok(()); + } + let array: &[T] = offset.resolve_with_args(self.cursor.data, count)?; + if !len_only { + self.descend_into_offset(offset, |ctx| array.iter().try_for_each(|t| f(t, ctx))) + } else { + Ok(()) + } + } + + /// Advance the cursor by the length of the array, recursing if necessary + pub(crate) fn sanitize_array_of_structs( + &mut self, + count: usize, + args: T::Args, + ) -> Result<(), ReadError> { + if T::can_skip() { + self.sanitize_array::(count) + } else { + let array = self.cursor.read_array::(count)?; + array.iter().try_for_each(|t| t.sanitize_struct(self, args)) + } + } + + pub(crate) fn sanitize_computed_array( + &mut self, + count: usize, + args: T::Args, + recurse: bool, + ) -> Result<(), ReadError> + where + T: ComputeSize + SanitizeStruct + FontRead<'a>, + { + if recurse { + let array = self.cursor.read_computed_array::(count, args)?; + array + .iter() + .try_for_each(|t| t.and_then(|t| t.sanitize_struct(self, args))) + } else { + T::compute_size(args) + .and_then(|len| len.checked_mul(count).ok_or(ReadError::OutOfBounds)) + .map(|n_bytes| self.advance_by(n_bytes)) + } + } + + pub(crate) fn sanitize_var_len_array( + &mut self, + count: usize, + recurse: bool, + ) -> Result<(), ReadError> + where + T: VarSize + SanitizeStruct + FontRead<'a>, + { + let remaining = self.cursor.remaining().ok_or(ReadError::OutOfBounds)?; + let total_len = T::total_len_for_count(remaining, count)?; + if recurse { + let array = VarLenArray::::read(remaining)?; + for item in array.iter().take(count) { + item?.sanitize_struct(self, ())?; + } + } + self.advance_by(total_len); + Ok(()) + } + + /// Validate the state for this table, returning an error if sanitize failed + pub(crate) fn finish(&self) -> Result<(), ReadError> { + //TODO: this would be a good place to check max ops, unless we're worried + //about DDOS that doesn't touch offsets? + self.cursor.position().map(|_| ()) + } +} + +pub trait Sanitize: ReadArgs { + /// recursively sanitizes this + all subgraphs. + /// + /// does not need to be called manually? we'll do this automatically? + fn sanitize(ctx: &mut SanitizeContext<'_>, args: Self::Args) -> Result<(), ReadError>; +} + +/// Sanitize functionality that is called on concrete types, instead of just +/// with raw bytes. +/// +/// This is used for offsets and records. +pub trait SanitizeStruct: ReadArgs { + /// If the struct doesn't include offsets, we can just skip it. + fn can_skip() -> bool { + false + } + + /// Sanitize `self`, recursing into any offsets + fn sanitize_struct( + &self, + ctx: &mut SanitizeContext<'_>, + args: Self::Args, + ) -> Result<(), ReadError>; +} + +/// Recursively sanitize the table pointed at by an offset. +pub trait SanitizeOffset { + fn sanitize_offset( + &self, + ctx: &mut SanitizeContext<'_>, + args: T::Args, + ) -> Result<(), ReadError>; +} + +impl SanitizeOffset for O { + fn sanitize_offset( + &self, + ctx: &mut SanitizeContext<'_>, + args: T::Args, + ) -> Result<(), ReadError> { + ctx.descend_into_offset(*self, |ctx| T::sanitize(ctx, args)) + } +} + +impl SanitizeOffset for Nullable { + fn sanitize_offset( + &self, + ctx: &mut SanitizeContext<'_>, + args: T::Args, + ) -> Result<(), ReadError> { + self.offset().sanitize_offset::(ctx, args) + } +} + +impl SanitizeOffset for BigEndian { + fn sanitize_offset( + &self, + ctx: &mut SanitizeContext<'_>, + args: T::Args, + ) -> Result<(), ReadError> { + self.get().sanitize_offset::(ctx, args) + } +} + +impl SanitizeOffset for &[O] { + fn sanitize_offset( + &self, + ctx: &mut SanitizeContext<'_>, + args: T::Args, + ) -> Result<(), ReadError> { + self.iter() + .try_for_each(|off| off.sanitize_offset::(ctx, args)) + } +} + +#[cfg(test)] +mod tests { + use types::Offset16; + + use super::*; + + #[test] + fn verify_that_various_things_compile() { + fn sanitize() {} + + sanitize::(); + sanitize::>(); + sanitize::>(); + sanitize::>>(); + sanitize::<&[BigEndian>]>(); + sanitize::<&[BigEndian]>(); + sanitize::<&[Offset16]>(); + } +} diff --git a/resources/codegen_inputs/test_conditions.rs b/resources/codegen_inputs/test_conditions.rs index 1b34399d2..346e63c43 100644 --- a/resources/codegen_inputs/test_conditions.rs +++ b/resources/codegen_inputs/test_conditions.rs @@ -1,4 +1,5 @@ #![parse_module(read_fonts::codegen_test::conditions)] +#![sanitize] #[skip_constructor] // because we don't use it, this avoids an unused warning table MajorMinorVersion { diff --git a/resources/codegen_inputs/test_count_all.rs b/resources/codegen_inputs/test_count_all.rs index 1ea2ea69f..1cda592f6 100644 --- a/resources/codegen_inputs/test_count_all.rs +++ b/resources/codegen_inputs/test_count_all.rs @@ -1,15 +1,18 @@ // This file tests the generation of count(..) arrays with element size > 1. #![parse_module(read_fonts::codegen_test::count_all)] +#![sanitize] table CountAll16 { some_field: u16, #[count(..)] + #[sanitize_with(sanitize_remainder)] remainder: [u16] } table CountAll32 { some_field: u16, + #[sanitize_with(sanitize_remainder)] #[count(..)] remainder: [u32] } diff --git a/resources/codegen_inputs/test_enum.rs b/resources/codegen_inputs/test_enum.rs index 8ac077f1e..a1d974948 100644 --- a/resources/codegen_inputs/test_enum.rs +++ b/resources/codegen_inputs/test_enum.rs @@ -1,6 +1,7 @@ // This file tests the generation of bitflags. #![parse_module(read_fonts::codegen_test::enums)] +#![sanitize] enum u16 MyEnum1 { /// doc me baby diff --git a/resources/codegen_inputs/test_flags.rs b/resources/codegen_inputs/test_flags.rs index 979c1819d..e61ec5743 100644 --- a/resources/codegen_inputs/test_flags.rs +++ b/resources/codegen_inputs/test_flags.rs @@ -1,6 +1,7 @@ // This file tests the generation of bitflags. #![parse_module(read_fonts::codegen_test::flags)] +#![sanitize] /// Some flags! flags u16 ValueFormat { diff --git a/resources/codegen_inputs/test_formats.rs b/resources/codegen_inputs/test_formats.rs index 6033a8a93..f1e7c2977 100644 --- a/resources/codegen_inputs/test_formats.rs +++ b/resources/codegen_inputs/test_formats.rs @@ -5,6 +5,7 @@ // to only rebuild the test outputs. #![parse_module(read_fonts::codegen_test::formats)] +#![sanitize] table Table1 { #[format = 1] diff --git a/resources/codegen_inputs/test_generic_group.rs b/resources/codegen_inputs/test_generic_group.rs index da7b26e02..ddaa2d677 100644 --- a/resources/codegen_inputs/test_generic_group.rs +++ b/resources/codegen_inputs/test_generic_group.rs @@ -4,6 +4,7 @@ // Based on the Lookup / PositionLookup pattern in GPOS. #![parse_module(read_fonts::codegen_test::generic_group)] +#![sanitize] /// A generic table parameterized by the type of its subtable offsets. #[generic_offset(T)] diff --git a/resources/codegen_inputs/test_offsets_arrays.rs b/resources/codegen_inputs/test_offsets_arrays.rs index 1fb73f390..5fb32ba83 100644 --- a/resources/codegen_inputs/test_offsets_arrays.rs +++ b/resources/codegen_inputs/test_offsets_arrays.rs @@ -5,6 +5,7 @@ // to only rebuild the test outputs. #![parse_module(read_fonts::codegen_test::offsets_arrays)] +#![sanitize] #[skip_constructor] table KindsOfOffsets { @@ -96,6 +97,7 @@ table VarLenHaver { count: u16, #[count($count)] #[traverse_with(skip)] + #[sanitize_len_only] var_len: VarLenArray>, other_field: u32, } diff --git a/resources/codegen_inputs/test_read_args.rs b/resources/codegen_inputs/test_read_args.rs index 91b77a5cd..2366acc36 100644 --- a/resources/codegen_inputs/test_read_args.rs +++ b/resources/codegen_inputs/test_read_args.rs @@ -1,4 +1,5 @@ #![parse_module(read_fonts::codegen_test::read_args)] +#![sanitize] #[read_args(mark_class_count: u16)] #[skip_constructor] @@ -9,10 +10,11 @@ table BaseArray { /// Array of BaseRecords, in order of baseCoverage Index. #[count($base_count)] #[read_with($mark_class_count)] + #[sanitize_len_only] base_records: ComputedArray>, #[compile(array_len($face_records))] face_count: u16, - #[count($base_count)] + #[count($face_count)] #[read_with($mark_class_count)] face_records: ComputedArray>, diff --git a/resources/codegen_inputs/test_records.rs b/resources/codegen_inputs/test_records.rs index eec59a75f..e420972db 100644 --- a/resources/codegen_inputs/test_records.rs +++ b/resources/codegen_inputs/test_records.rs @@ -5,6 +5,7 @@ // to only rebuild the test outputs. #![parse_module(read_fonts::codegen_test::records)] +#![sanitize] #[validate(my_custom_validate)] table BasicTable { @@ -47,5 +48,27 @@ record ContainsOffsets { table VarLenItem { length: u32, #[count(..)] + #[sanitize_with(sanitize_data)] data: [u8], } + +#[skip_constructor] +record HasOffsetsWithArgs { + merp_len: u16, + /// Read an offset that takes an argument, in a record + #[read_offset_with($merp_len)] + feature_offset: Offset16, + /// custom offset getter in a record + #[offset_getter(fake)] + #[sanitize_with(sanitize_fake_offset)] + fake_offset: Offset16, +} + +#[read_args(merp_len: u16)] +#[skip_constructor] +table HasReadArgs { + derp: u16, + #[count($merp_len)] + merps: [i16], +} + diff --git a/write-fonts/generated/generated_test_records.rs b/write-fonts/generated/generated_test_records.rs index fb940b0e5..844477137 100644 --- a/write-fonts/generated/generated_test_records.rs +++ b/write-fonts/generated/generated_test_records.rs @@ -273,3 +273,92 @@ impl<'a> FontRead<'a> for VarLenItem { .map(|x| x.to_owned_table()) } } + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct HasOffsetsWithArgs { + pub merp_len: u16, + /// Read an offset that takes an argument, in a record + pub feature: OffsetMarker, + /// custom offset getter in a record + pub fake: OffsetMarker, +} + +impl FontWrite for HasOffsetsWithArgs { + fn write_into(&self, writer: &mut TableWriter) { + self.merp_len.write_into(writer); + self.feature.write_into(writer); + self.fake.write_into(writer); + } + fn table_type(&self) -> TableType { + TableType::Named("HasOffsetsWithArgs") + } +} + +impl Validate for HasOffsetsWithArgs { + fn validate_impl(&self, ctx: &mut ValidationCtx) { + ctx.in_table("HasOffsetsWithArgs", |ctx| { + ctx.in_field("feature", |ctx| { + self.feature.validate_impl(ctx); + }); + ctx.in_field("fake", |ctx| { + self.fake.validate_impl(ctx); + }); + }) + } +} + +impl FromObjRef for HasOffsetsWithArgs { + fn from_obj_ref( + obj: &read_fonts::codegen_test::records::HasOffsetsWithArgs, + offset_data: FontData, + ) -> Self { + HasOffsetsWithArgs { + merp_len: obj.merp_len(), + feature: obj.feature(offset_data).to_owned_table(), + fake: obj.fake(offset_data).to_owned_table(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct HasReadArgs { + pub derp: u16, + pub merps: Vec, +} + +impl FontWrite for HasReadArgs { + fn write_into(&self, writer: &mut TableWriter) { + self.derp.write_into(writer); + self.merps.write_into(writer); + } + fn table_type(&self) -> TableType { + TableType::Named("HasReadArgs") + } +} + +impl Validate for HasReadArgs { + fn validate_impl(&self, ctx: &mut ValidationCtx) { + ctx.in_table("HasReadArgs", |ctx| { + ctx.in_field("merps", |ctx| { + if self.merps.len() > to_usize(u16::MAX) { + ctx.report("array exceeds max length"); + } + }); + }) + } +} + +impl<'a> FromObjRef> for HasReadArgs { + fn from_obj_ref(obj: &read_fonts::codegen_test::records::HasReadArgs<'a>, _: FontData) -> Self { + let offset_data = obj.offset_data(); + HasReadArgs { + derp: obj.derp(), + merps: obj.merps().to_owned_obj(offset_data), + } + } +} + +#[allow(clippy::needless_lifetimes)] +impl<'a> FromTableRef> for HasReadArgs {}