diff --git a/.changes/unreleased/added-20260923-codec-strategy-table.yaml b/.changes/unreleased/added-20260923-codec-strategy-table.yaml index acae42eb..cdb78204 100644 --- a/.changes/unreleased/added-20260923-codec-strategy-table.yaml +++ b/.changes/unreleased/added-20260923-codec-strategy-table.yaml @@ -1,4 +1,4 @@ kind: Added body: |- - **Table-driven message codec** (#469, refs #463). `buffa_build::Config::codec_strategy(CodecStrategy::Table)` (plugin option `codec_strategy=table`, `CodeGenConfig::codec_strategy`) generates each message's binary `Message` implementation from a static table and interpreters that every message shares, instead of code specialised to the message's fields. The compiled code is substantially smaller, and messages made of many small fields are slower. `CodecStrategy::Unrolled` stays the default. `codec_strategy_in(strategy, &[paths])` (plugin option `codec_strategy_in==`, repeatable) chooses the strategy for matching messages and the messages nested in them, and the last matching rule wins. The wire format does not change, and a message with a `oneof` uses the table, including when a member is a message that has no table. Messages the table cannot handle stay unrolled, and `CodeGenWarning::TableCodecFallbackSummary` counts them. A rule that selects the table, by exact path, for a message that cannot use it is an error. The generated code needs Rust 1.77 or later, which `buffa-build` checks, and compiles in a crate with `#![forbid(unsafe_code)]`. The guide's "Smaller generated code" section has the measurements, the messages that stay unrolled and the behavioural differences. + **Table-driven message codec** (#469, refs #463). `buffa_build::Config::codec_strategy(CodecStrategy::Table)` (plugin option `codec_strategy=table`, `CodeGenConfig::codec_strategy`) generates each message's binary `Message` implementation from a static table and interpreters that every message shares, instead of code specialised to the message's fields. The compiled code is substantially smaller, and messages made of many small fields are slower. `CodecStrategy::Unrolled` stays the default. `codec_strategy_in(strategy, &[paths])` (plugin option `codec_strategy_in==`, repeatable) chooses the strategy for matching messages and the messages nested in them, and the last matching rule wins. The wire format does not change, and a message with a `oneof` or a `map` field uses the table, including when a member or value is a message that has no table. Messages the table cannot handle stay unrolled, and `CodeGenWarning::TableCodecFallbackSummary` counts them. A rule that selects the table, by exact path, for a message that cannot use it is an error. The generated code needs Rust 1.77 or later, which `buffa-build` checks, and compiles in a crate with `#![forbid(unsafe_code)]`. The guide's "Smaller generated code" section has the measurements, the messages that stay unrolled and the behavioural differences. time: 2026-09-23T03:10:00+00:00 diff --git a/DESIGN.md b/DESIGN.md index 2e1aa65e..5e9ac65d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -560,7 +560,7 @@ Within that bound, the shortcuts do not pay off. `merge` does not help: it consu The size, write, and merge code of an unrolled message is specialised to its fields. `CodecStrategy::Table` (a per-message option, `Unrolled` by default) replaces it with a static `buffa::table::Table` and a `Message` impl that forwards to interpreters in `buffa::table`. The decision to keep `Unrolled` as the default is in [#463](https://github.com/anthropics/buffa/issues/463), and the guide's "Smaller generated code" section has the measurements and the list of messages that stay unrolled. -A table holds a sorted array of 12-byte entries `{tag, offset, kind, tag_len, aux}`, a dense array that maps field numbers below 64 to entries, and the offset of the unknown-fields slot. `kind` is the field type crossed with its cardinality, so the interpreter dispatches once per field. Message, repeated-message, and enum fields carry a small descriptor (`Aux`) with the accessors that their storage needs, because a `MessageField`, a `Vec`, and an `EnumValue` cannot be read through an offset alone; a oneof has a `Group` descriptor and each of its members a `Member` one. Offsets come from `core::mem::offset_of!`, so the table needs Rust 1.77 and the generated code refers to it through `buffa::__table!`, which is a compile error on an older compiler. +A table holds a sorted array of 12-byte entries `{tag, offset, kind, tag_len, aux}`, a dense array that maps field numbers below 64 to entries, and the offset of the unknown-fields slot. `kind` is the field type crossed with its cardinality, so the interpreter dispatches once per field. Message, repeated-message, and enum fields carry a small descriptor (`Aux`) with the accessors that their storage needs, because a `MessageField`, a `Vec`, and an `EnumValue` cannot be read through an offset alone; a oneof has a `Group` descriptor and each of its members a `Member` one. A map field is one entry of kind `Map`, whose descriptor (`MapVt`) instantiates only the iteration of the collection and the insertion of a decoded entry per collection type; `buffa/src/table/map.rs` describes the mechanism. Offsets come from `core::mem::offset_of!`, so the table needs Rust 1.77 and the generated code refers to it through `buffa::__table!`, which is a compile error on an older compiler. Four decisions shape the runtime: diff --git a/buffa-codegen/src/lib.rs b/buffa-codegen/src/lib.rs index 7fd55583..c903ea7c 100644 --- a/buffa-codegen/src/lib.rs +++ b/buffa-codegen/src/lib.rs @@ -1094,14 +1094,16 @@ pub enum CodecStrategy { /// /// Not every message can use it. These stay [`Unrolled`](Self::Unrolled): /// - /// - a message with a `map` field or a group field; + /// - a message with a group field; /// - the message type of a group field; /// - a message that uses the `MessageSet` wire format; /// - a message with extension ranges, when JSON code is generated and /// unknown fields are preserved; /// - a message with a field of a non-default string, bytes, or collection - /// type, such as `use_bytes_type`, `string_type`, `bytes_type`, and - /// `repeated_type` select; + /// type, such as `use_bytes_type`, `string_type`, `bytes_type`, + /// `repeated_type`, and a custom `map_type` select. `HashMap` and + /// `BTreeMap` maps are supported; a `map` whose keys or values are of a + /// non-default string or bytes type is not; /// - a message that holds, in a singular, repeated, `oneof`, or map value /// field, a message of the same run that has a `bytes` field of a /// non-default type, or that holds one. The table decodes from one @@ -1109,8 +1111,8 @@ pub enum CodecStrategy { /// unrolled code decoding from a `Bytes` shares with the input. /// /// A table message may hold any other message, including as a member of a - /// `oneof`. It reaches a child that is a table message through the - /// child's table, and any other child, whether it is + /// `oneof` and as a `map` value. It reaches a child that is a table + /// message through the child's table, and any other child, whether it is /// [`Unrolled`](Self::Unrolled), generated by another crate, or a /// well-known type, through the child's `Message` impl, which costs a /// function call per child. Selecting a message does not select the @@ -1124,7 +1126,8 @@ pub enum CodecStrategy { /// different error for the same rejected input; a child reached through its /// `Message` impl is read from the slice of the nearest enclosing table /// message, so it is bounded there. `clear()` resets the message to its - /// default, which releases the capacity of its strings and vectors. + /// default, which releases the capacity of its strings, vectors and maps, + /// where unrolled code keeps it. /// Decoding gathers a `Buf` that is not contiguous into one buffer first. /// Encoding into any sink other than the cursor that `Message::encode` and /// its siblings write a `BufMut` through, meaning a `Rope`, a sink defined @@ -1157,7 +1160,7 @@ pub enum CodecStrategy { #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub struct TableCodecFallbackReason { - /// The reason as a predicate, such as `has a map field`. The wording is for + /// The reason as a predicate, such as `has a group field`. The wording is for /// people and may change between releases. pub reason: String, /// The proto paths of all of them, with a leading dot, in declaration diff --git a/buffa-codegen/src/table_codec.rs b/buffa-codegen/src/table_codec.rs index d66d17f3..5dfb62a9 100644 --- a/buffa-codegen/src/table_codec.rs +++ b/buffa-codegen/src/table_codec.rs @@ -14,7 +14,9 @@ use crate::generated::descriptor::field_descriptor_proto::Type; use crate::generated::descriptor::{DescriptorProto, FieldDescriptorProto}; use crate::idents::rust_path_to_tokens; use crate::message::classify_field; -use crate::table_plan::{table_fields, Card, OneofMembership, TableField}; +use crate::table_plan::{ + table_fields, type_stem, Card, MapField, OneofMembership, PlainField, Shape, TableField, +}; use crate::CodeGenError; /// The name of the static table of the message struct `rust_name`. @@ -77,8 +79,14 @@ pub(crate) fn generate_table_impl( let mut aux: Vec = Vec::new(); let mut oneofs = Oneofs::new(scope, msg, &fields)?; for f in &fields { - if let Some(member) = &f.oneof { - entries.push(oneofs.member_entry(scope, &name, f, member, &mut aux)?); + if let Shape::Plain( + plain @ PlainField { + oneof: Some(member), + .. + }, + ) = &f.shape + { + entries.push(oneofs.member_entry(scope, &name, f, plain, member, &mut aux)?); continue; } let (entry, aux_item) = field_entry(scope, msg, &name, f, aux.len(), resolver)?; @@ -191,7 +199,6 @@ fn field_entry( let field = f.field; let field_name = field.name.as_deref().unwrap_or(""); let ident = ctx.field_ident(field_name, field.number.unwrap_or(0)); - let kind = format_ident!("{}", f.kind); let number = f.number; let aux_u16 = || u16::try_from(aux_index).map_err(|_| too_many_descriptors(scope)); @@ -199,7 +206,23 @@ fn field_entry( let unshortened_path = || unshortened_path(scope, field); let type_name = || field_type_name(field); - match f.ty { + let plain = match &f.shape { + Shape::Map(map) => { + let aux = aux_u16()?; + let map_ty = classify_field(scope, msg, field, resolver)?.rust_type; + let vt = map_descriptor(scope, &map_ty, map)?; + return Ok(( + quote! { + ::buffa::__table_entry!(#name, #ident, Map, #number, aux = #aux, slot = #map_ty) + }, + Some(quote! { ::buffa::table::Aux::Map(&#vt) }), + )); + } + Shape::Plain(plain) => plain, + }; + let kind = format_ident!("{}", plain.kind); + + match plain.ty { Type::TYPE_MESSAGE => { let child = type_path("message")?; let child_ty = rust_path_to_tokens(&child); @@ -210,7 +233,7 @@ fn field_entry( } else { None }; - let (slot, aux_item) = if f.card == Card::Repeated { + let (slot, aux_item) = if plain.card == Card::Repeated { let vt = match &child_table { Some(table) => quote! { ::buffa::table::RepVt::new::<#child_ty>(&#table) }, None => quote! { ::buffa::table::RepVt::new_via_message::<#child_ty>() }, @@ -237,8 +260,8 @@ fn field_entry( } Type::TYPE_ENUM => { let enum_ty = rust_path_to_tokens(&type_path("enum")?); - let repeated = matches!(f.card, Card::Repeated | Card::Packed); - let shape = match (repeated, f.card == Card::Optional, f.closed_enum) { + let repeated = matches!(plain.card, Card::Repeated | Card::Packed); + let shape = match (repeated, plain.card == Card::Optional, plain.closed_enum) { (true, _, true) => quote! { RepeatedClosed }, (true, _, false) => quote! { RepeatedOpen }, (false, true, true) => quote! { OptionalClosed }, @@ -268,6 +291,57 @@ fn field_entry( } } +/// The `MapVt` expression for the map field of Rust type `map_ty`. +fn map_descriptor( + scope: MessageScope<'_>, + map_ty: &TokenStream, + map: &MapField<'_>, +) -> Result { + let marker = |ty: Type, slot: &str| -> Result { + match type_stem(ty, Card::Required) { + Some(stem) if !matches!(ty, Type::TYPE_ENUM | Type::TYPE_MESSAGE) => { + let kind = format_ident!("{}Required", stem); + Ok(quote! { ::buffa::table::kinds::#kind }) + } + _ => Err(CodeGenError::Other(format!( + "table codec: a map {slot} of type {ty:?} has no scalar kind" + ))), + } + }; + let key = marker(map.key_ty, "key")?; + Ok(match map.val_ty { + Type::TYPE_ENUM => { + let enum_ty = rust_path_to_tokens(&type_path(scope, map.val_field, "enum")?); + let shape = if map.closed_enum { + quote! { ImplicitClosed } + } else { + quote! { ImplicitOpen } + }; + quote! { + ::buffa::table::MapVt::with_enum::<#map_ty, #key, ::buffa::table::#shape<#enum_ty>>() + } + } + Type::TYPE_MESSAGE => { + let child = rust_path_to_tokens(&type_path(scope, map.val_field, "message")?); + // A value without a table here is reached through its `Message` + // impl. + let msg_vt = if scope.ctx.uses_table_codec(field_type_name(map.val_field)?) { + let child_table = table_path(&unshortened_path(scope, map.val_field)?)?; + quote! { ::buffa::table::DirectMsgVt::new(&#child_table) } + } else { + quote! { ::buffa::table::DirectMsgVt::<#child>::via_message() } + }; + quote! { + ::buffa::table::MapVt::with_msg::<#map_ty, #key, #child>(&#msg_vt) + } + } + ty => { + let value = marker(ty, "value")?; + quote! { ::buffa::table::MapVt::new::<#map_ty, #key, #value>() } + } + }) +} + /// The proto path of the message or enum type of `field`. fn field_type_name(field: &FieldDescriptorProto) -> Result<&str, CodeGenError> { field @@ -355,13 +429,13 @@ impl Oneofs { ) -> Result { let mut first: HashMap = HashMap::new(); let mut with_messages = HashSet::new(); - for (oneof, f) in fields - .iter() - .filter_map(|f| f.oneof.as_ref().map(|oneof| (oneof, f))) - { + for (oneof, f, plain) in fields.iter().filter_map(|f| match &f.shape { + Shape::Plain(plain) => plain.oneof.as_ref().map(|oneof| (oneof, f, plain)), + Shape::Map(_) => None, + }) { let lowest = first.entry(oneof.index).or_insert(f.number); *lowest = (*lowest).min(f.number); - if f.ty == Type::TYPE_MESSAGE { + if plain.ty == Type::TYPE_MESSAGE { with_messages.insert(oneof.index); } } @@ -399,6 +473,7 @@ impl Oneofs { scope: MessageScope<'_>, message: &proc_macro2::Ident, f: &TableField<'_>, + plain: &PlainField<'_>, member: &OneofMembership<'_>, aux: &mut Vec, ) -> Result { @@ -417,7 +492,7 @@ impl Oneofs { let oneof_field = ctx.oneof_ident(member.name); let variant = crate::oneof::oneof_variant_ident(field_name); let number = f.number; - let payload_kind = format_ident!("{}", f.kind); + let payload_kind = format_ident!("{}", plain.kind); let first = self.first[&member.index]; // The oneof's descriptor, made when its first member is met. @@ -445,7 +520,7 @@ impl Oneofs { // is first set. let variant_fqn = format!(".{}.{}.{field_name}", scope.proto_fqn, member.name); let default = quote! { ::core::default::Default::default() }; - let (slot, value_aux, new) = match f.ty { + let (slot, value_aux, new) = match plain.ty { Type::TYPE_MESSAGE => { let child = rust_path_to_tokens(&type_path(scope, field, "message")?); // A child without a table here is reached through its @@ -456,7 +531,7 @@ impl Oneofs { } else { quote! { ::buffa::table::MsgVt::direct_via_message::<#child>() } }; - let new = if crate::oneof::variant_boxed(ctx, f.ty, &variant_fqn) { + let new = if crate::oneof::variant_boxed(ctx, plain.ty, &variant_fqn) { match ctx.pointer_repr(&variant_fqn) { crate::PointerRepr::Box => quote! { ::buffa::alloc::boxed::Box::default() }, repr => repr.pointer_new(&child, &default)?, @@ -472,7 +547,7 @@ impl Oneofs { } Type::TYPE_ENUM => { let enum_ty = rust_path_to_tokens(&type_path(scope, field, "enum")?); - let shape = if f.closed_enum { + let shape = if plain.closed_enum { quote! { ::buffa::table::ImplicitClosed<#enum_ty> } } else { quote! { ::buffa::table::ImplicitOpen<#enum_ty> } diff --git a/buffa-codegen/src/table_plan.rs b/buffa-codegen/src/table_plan.rs index af87fd16..21963e06 100644 --- a/buffa-codegen/src/table_plan.rs +++ b/buffa-codegen/src/table_plan.rs @@ -16,8 +16,9 @@ use crate::features::ResolvedFeatures; use crate::generated::descriptor::field_descriptor_proto::{Label, Type}; use crate::generated::descriptor::{DescriptorProto, FieldDescriptorProto, FileDescriptorProto}; use crate::impl_message::{ - effective_type, field_bytes_repr, is_explicit_presence_scalar, is_field_packed, - is_real_oneof_member, is_required_field, map_value_bytes_repr, + effective_type, effective_type_in_map_entry, field_bytes_repr, find_map_entry_fields, + is_explicit_presence_scalar, is_field_packed, is_real_oneof_member, is_required_field, + map_string_repr, map_value_bytes_repr, }; use crate::message::{find_map_entry, is_closed_enum, map_entry_key_type, map_entry_value_type}; use crate::{CodeGenError, CodeGenWarning, CodecStrategy, TableCodecFallbackReason}; @@ -48,6 +49,19 @@ impl Card { pub(crate) struct TableField<'a> { pub(crate) field: &'a FieldDescriptorProto, pub(crate) number: u32, + pub(crate) shape: Shape<'a>, +} + +/// What kind of entry a [`TableField`] is. +pub(crate) enum Shape<'a> { + /// A `map` field. + Map(MapField<'a>), + /// Any other field, including a member of a oneof. + Plain(PlainField<'a>), +} + +/// A field that is not a map. +pub(crate) struct PlainField<'a> { pub(crate) ty: Type, pub(crate) card: Card, /// The name of the `buffa::table::Kind` variant of this field's value. For @@ -60,6 +74,17 @@ pub(crate) struct TableField<'a> { pub(crate) oneof: Option>, } +/// The key and value of a map field. +pub(crate) struct MapField<'a> { + pub(crate) key_ty: Type, + pub(crate) val_ty: Type, + /// The value field of the map entry message, which names the value's + /// message or enum type. + pub(crate) val_field: &'a FieldDescriptorProto, + /// Whether the value is an enum and the enum is closed. + pub(crate) closed_enum: bool, +} + /// The oneof that a [`TableField`] is a member of. pub(crate) struct OneofMembership<'a> { /// The index of the oneof in the message's `oneof_decl`. @@ -69,7 +94,7 @@ pub(crate) struct OneofMembership<'a> { /// The `Kind` variant name of a field type, or `None` for a group, which has /// no kind. -fn type_stem(ty: Type, card: Card) -> Option<&'static str> { +pub(crate) fn type_stem(ty: Type, card: Card) -> Option<&'static str> { Some(match ty { Type::TYPE_INT32 => "Int32", Type::TYPE_INT64 => "Int64", @@ -109,6 +134,21 @@ fn same(reason: &str) -> Ineligible { ineligible(reason, format!("it {reason}")) } +/// The fallback reason for a field, or a map, whose string, bytes or collection +/// type is not the default. One text for all of them, so that a schema with +/// several kinds gets one line in the summary. +pub(crate) const CUSTOM_TYPE_REASON: &str = + "has a field with a custom string, bytes or collection type"; + +/// The key and value fields of `f` if it is a map field of `msg`: `None` for +/// any other field, and an error for a map whose entry message is malformed. +fn map_entry_fields<'a>( + msg: &'a DescriptorProto, + f: &FieldDescriptorProto, +) -> Option> { + find_map_entry(msg, f).map(|_| find_map_entry_fields(msg, f)) +} + fn ineligible(reason: impl Into, detail: impl Into) -> Ineligible { Ineligible { reason: reason.into(), @@ -159,11 +199,11 @@ pub(crate) fn table_fields<'a>( } else { None }; - if find_map_entry(msg, f).is_some() { - return Err(ineligible( - "has a map field", - format!("field `{name}` is a map"), - )); + if let Some(entry) = map_entry_fields(msg, f) { + let entry = + entry.map_err(|e| ineligible("has a malformed map field", e.to_string()))?; + fields.push(map_field(ctx, f, entry, fqn, features)?); + continue; } let ty = effective_type(ctx, f, features); let field_fqn = format!("{fqn}.{name}"); @@ -175,7 +215,7 @@ pub(crate) fn table_fields<'a>( } || (repeated && !ctx.repeated_repr(&field_fqn).is_default()); if custom { return Err(ineligible( - "has a field with a custom string, bytes or collection type", + CUSTOM_TYPE_REASON, format!("field `{name}` has a custom string, bytes or collection type"), )); } @@ -213,17 +253,66 @@ pub(crate) fn table_fields<'a>( fields.push(TableField { field: f, number, - ty, - card, - kind, - closed_enum, - oneof: oneof.map(|(index, name)| OneofMembership { index, name }), + shape: Shape::Plain(PlainField { + ty, + card, + kind, + closed_enum, + oneof: oneof.map(|(index, name)| OneofMembership { index, name }), + }), }); } fields.sort_by_key(|f| f.number); Ok(fields) } +/// The table view of the map field `f` of `msg`, or why it cannot use the +/// table. +fn map_field<'a>( + ctx: &CodeGenContext, + f: &'a FieldDescriptorProto, + (key_field, val_field): (&FieldDescriptorProto, &'a FieldDescriptorProto), + fqn: &str, + features: &ResolvedFeatures, +) -> Result, Ineligible> { + let name = f.name.as_deref().unwrap_or(""); + let field_fqn = format!("{fqn}.{name}"); + if matches!(ctx.map_repr(&field_fqn), crate::MapRepr::Custom(_)) { + return Err(ineligible( + CUSTOM_TYPE_REASON, + format!("map field `{name}` has a custom collection type"), + )); + } + let key_ty = effective_type_in_map_entry(ctx, key_field, features); + let val_ty = effective_type_in_map_entry(ctx, val_field, features); + let proto_fqn = fqn.strip_prefix('.').unwrap_or(fqn); + let custom_string = [key_ty, val_ty] + .into_iter() + .any(|ty| !map_string_repr(ctx, ty, proto_fqn, name).is_default()); + let custom_bytes = + !map_value_bytes_repr(ctx, Some(key_ty), Some(val_ty), proto_fqn, name).is_default(); + if custom_string || custom_bytes { + return Err(ineligible( + CUSTOM_TYPE_REASON, + format!("map field `{name}` has a custom string, bytes or collection type"), + )); + } + let number = crate::impl_message::validated_field_number(f) + .map_err(|e| ineligible("has an invalid field number", e.to_string()))?; + let closed_enum = val_ty == Type::TYPE_ENUM + && is_closed_enum(&crate::features::resolve_field(ctx, val_field, features)); + Ok(TableField { + field: f, + number, + shape: Shape::Map(MapField { + key_ty, + val_ty, + val_field, + closed_enum, + }), + }) +} + /// One message of the run and what the plan needs to know about it. struct Candidate<'a> { fqn: String, diff --git a/buffa-codegen/src/tests/table_codec.rs b/buffa-codegen/src/tests/table_codec.rs index 3635a5ef..cc6a98f3 100644 --- a/buffa-codegen/src/tests/table_codec.rs +++ b/buffa-codegen/src/tests/table_codec.rs @@ -629,7 +629,7 @@ fn the_warning_texts_say_what_to_do() { message names" ); let reason = TableCodecFallbackReason { - reason: "has a map field".to_string(), + reason: "has a group field".to_string(), messages: [".t.A", ".t.B", ".t.C", ".t.D", ".t.E"] .map(String::from) .to_vec(), @@ -642,7 +642,7 @@ fn the_warning_texts_say_what_to_do() { let text = summary.to_string(); // Three messages are named, and the rest are counted. assert!( - text.contains("has a map field (5: .t.A, .t.B, .t.C, and 2 more)"), + text.contains("has a group field (5: .t.A, .t.B, .t.C, and 2 more)"), "{text}" ); } @@ -817,11 +817,7 @@ fn a_message_that_holds_a_message_with_a_bytes_type_stays_unrolled() { ); let (counts, reasons) = summary(&warnings); assert_eq!(counts, (8, 12)); - // `MapBlob` falls back for its map, so it is not counted as a holder. - assert_eq!( - reasons, - [(HOLDS_BYTES, 6), (CUSTOM_FIELD, 1), ("has a map field", 1)] - ); + assert_eq!(reasons, [(HOLDS_BYTES, 7), (CUSTOM_FIELD, 1)]); let text = table_warnings(&warnings)[0].to_string(); assert!(text.contains(HOLDS_BYTES), "{text}"); assert!(text.contains(".b.HasBlob"), "{text}"); @@ -861,7 +857,7 @@ fn a_child_set_to_unrolled_that_has_a_bytes_type_keeps_its_holder_unrolled() { }; let (code, warnings) = run_bytes(&config).unwrap(); assert!(!tables(&code).contains(&"HasBlob".to_string()), "{code}"); - assert!(summary(&warnings).1.contains(&(HOLDS_BYTES, 6))); + assert!(summary(&warnings).1.contains(&(HOLDS_BYTES, 7))); } #[test] @@ -1050,16 +1046,15 @@ fn a_message_holding_repeated_map_or_nested_bytes_is_unrolled() { // The holders counted under the one reason are the three above, the chain // and the cycle. assert_eq!(plan.holders, 7); - assert_eq!(plan.counts, (13, 17)); + assert_eq!(plan.counts, (12, 17)); } #[test] fn a_map_with_a_bytes_key_keeps_vec_values_so_its_holder_uses_the_table() { let tables = run_taint().tables; - assert!( - tables.contains(&"HoldsBytesKeyMap".to_string()), - "{tables:?}" - ); + for name in ["BytesKeyMap", "HoldsBytesKeyMap"] { + assert!(tables.contains(&name.to_string()), "{name}: {tables:?}"); + } } /// Package `o` with `WithOneof { oneof choice { int32 a = 1; string b = 2; Leaf leaf = 5; }; int32 c = 3; }`. @@ -1344,3 +1339,277 @@ fn a_oneof_member_with_a_custom_type_keeps_the_message_unrolled() { "{warnings:?}" ); } + +// --------------------------------------------------------------------------- +// Map fields +// --------------------------------------------------------------------------- + +/// A `map` field `name` of message `owner`, and its entry message. +/// `value` is a scalar type, or a message or enum (with `type_name`). +fn map_field( + owner: &str, + name: &str, + number: i32, + key: Type, + value: Type, + type_name: Option<&str>, +) -> (FieldDescriptorProto, DescriptorProto) { + let entry_name = format!("{}Entry", name.replace('_', "")); + let mut value_field = make_field("value", 2, Label::LABEL_OPTIONAL, value); + value_field.type_name = type_name.map(String::from); + let entry = DescriptorProto { + name: Some(entry_name.clone()), + field: vec![ + make_field("key", 1, Label::LABEL_OPTIONAL, key), + value_field, + ], + options: MessageOptions { + map_entry: Some(true), + ..Default::default() + } + .into(), + ..Default::default() + }; + let field = message_field(name, number, &format!(".t.{owner}.{entry_name}")); + ( + FieldDescriptorProto { + label: Some(Label::LABEL_REPEATED), + ..field + }, + entry, + ) +} + +/// `schema()` with `Maps` added, holding one map per shape a table handles. +fn map_schema() -> FileDescriptorProto { + let mut file = schema(); + let maps = [ + map_field( + "Maps", + "counts", + 1, + Type::TYPE_STRING, + Type::TYPE_INT32, + None, + ), + map_field("Maps", "blobs", 2, Type::TYPE_INT64, Type::TYPE_BYTES, None), + map_field( + "Maps", + "leaves", + 3, + Type::TYPE_INT32, + Type::TYPE_MESSAGE, + Some(".t.Leaf"), + ), + map_field( + "Maps", + "shades", + 4, + Type::TYPE_STRING, + Type::TYPE_ENUM, + Some(".t.Shade"), + ), + map_field( + "Maps", + "names", + 5, + Type::TYPE_INT32, + Type::TYPE_STRING, + None, + ), + ]; + let mut holder = message("Maps", maps.iter().map(|(f, _)| f.clone()).collect()); + holder.nested_type = maps.into_iter().map(|(_, e)| e).collect(); + file.message_type.push(holder); + file.enum_type.push(EnumDescriptorProto { + name: Some("Shade".to_string()), + value: vec![enum_value("NONE", 0), enum_value("DARK", 1)], + ..Default::default() + }); + file +} + +fn run_maps(config: &CodeGenConfig) -> (String, Vec) { + let (files, warnings) = + generate_with_diagnostics(&[map_schema()], &["t.proto".to_string()], config).unwrap(); + (joined(&files), warnings) +} + +#[test] +fn a_message_with_maps_gets_a_table_and_a_map_descriptor_per_field() { + let (code, warnings) = run_maps(&table_config(CodecStrategy::Table)); + assert!(tables(&code).contains(&"Maps".to_string()), "{code}"); + assert!(table_warnings(&warnings).is_empty(), "{warnings:?}"); + let code = squashed(&code); + for descriptor in [ + // Scalar and string values name the key and value kinds. + "Aux::Map(&::buffa::table::MapVt::new::<::buffa::__private::HashMap<::buffa::alloc::string::String,i32>,\ + ::buffa::table::kinds::StrRequired,::buffa::table::kinds::Int32Required>())", + "::buffa::table::kinds::Int64Required,::buffa::table::kinds::BytesRequired", + // A message value names the value's table, and an enum its openness. + "MapVt::with_msg::<::buffa::__private::HashMap,::buffa::table::kinds::Int32Required,Leaf>(\ + &::buffa::table::DirectMsgVt::new(&__BUFFA_TABLE_Leaf))", + "MapVt::with_enum::<::buffa::__private::HashMap<::buffa::alloc::string::String,\ + ::buffa::EnumValue>,::buffa::table::kinds::StrRequired,\ + ::buffa::table::ImplicitOpen>()", + ] { + assert!(code.contains(descriptor), "missing {descriptor} in {code}"); + } + assert!( + code.contains("__table_entry!(Maps,counts,Map,1u32,aux=0u16,slot="), + "{code}" + ); +} + +#[test] +fn a_map_uses_the_collection_the_field_is_configured_with() { + let config = CodeGenConfig { + map_fields: vec![(".".to_string(), MapRepr::BTreeMap)], + ..table_config(CodecStrategy::Table) + }; + let (code, warnings) = run_maps(&config); + assert!(tables(&code).contains(&"Maps".to_string())); + assert!(table_warnings(&warnings).is_empty(), "{warnings:?}"); + assert!( + squashed(&code) + .contains("MapVt::new::<::buffa::alloc::collections::BTreeMap<::buffa::alloc::string::String,i32>,"), + "{code}" + ); +} + +#[test] +fn a_map_with_a_custom_collection_or_element_type_falls_back() { + for (config, reason) in [ + ( + CodeGenConfig { + map_fields: vec![( + ".t.Maps.counts".to_string(), + MapRepr::Custom("crate::MyMap".to_string()), + )], + ..table_config(CodecStrategy::Table) + }, + crate::table_plan::CUSTOM_TYPE_REASON, + ), + ( + CodeGenConfig { + string_fields: vec![( + ".t.Maps.counts".to_string(), + StringRepr::Custom("crate::S".to_string()), + )], + ..table_config(CodecStrategy::Table) + }, + crate::table_plan::CUSTOM_TYPE_REASON, + ), + // A string value under an integer key. + ( + CodeGenConfig { + string_fields: vec![( + ".t.Maps.names".to_string(), + StringRepr::Custom("crate::S".to_string()), + )], + ..table_config(CodecStrategy::Table) + }, + crate::table_plan::CUSTOM_TYPE_REASON, + ), + ( + CodeGenConfig { + bytes_fields: vec![(".t.Maps.blobs".to_string(), BytesRepr::Bytes)], + ..table_config(CodecStrategy::Table) + }, + crate::table_plan::CUSTOM_TYPE_REASON, + ), + ] { + let (code, warnings) = run_maps(&config); + assert!(!tables(&code).contains(&"Maps".to_string()), "{reason}"); + let (_, reasons) = summary(&warnings); + assert!(reasons.contains(&(reason, 1)), "{reason}: {reasons:?}"); + } +} + +#[test] +fn a_map_of_messages_without_a_table_is_reached_through_the_message_impl() { + // `Leaf` is unrolled by a rule, so the map's values have no table here. + let config = CodeGenConfig { + codec_strategy_in: vec![(".t.Leaf".to_string(), CodecStrategy::Unrolled)], + ..table_config(CodecStrategy::Table) + }; + let (code, warnings) = run_maps(&config); + assert!(tables(&code).contains(&"Maps".to_string()), "{code}"); + assert!(!tables(&code).contains(&"Leaf".to_string())); + assert!(table_warnings(&warnings).is_empty(), "{warnings:?}"); + assert!( + squashed(&code).contains( + "MapVt::with_msg::<::buffa::__private::HashMap,\ + ::buffa::table::kinds::Int32Required,Leaf>(\ + &::buffa::table::DirectMsgVt::::via_message())" + ), + "{code}" + ); +} + +#[test] +fn a_map_of_messages_from_another_crate_is_reached_through_the_message_impl() { + let mut file = map_schema(); + let (field, entry) = map_field( + "Holds", + "os", + 1, + Type::TYPE_STRING, + Type::TYPE_MESSAGE, + Some(".other.Foreign"), + ); + let mut holds = message("Holds", vec![field]); + holds.nested_type = vec![entry]; + file.message_type.push(holds); + let other = FileDescriptorProto { + package: Some("other".to_string()), + message_type: vec![message("Foreign", vec![])], + ..proto3_file("other.proto") + }; + let config = CodeGenConfig { + extern_paths: vec![(".other".to_string(), "::other_crate".to_string())], + ..table_config(CodecStrategy::Table) + }; + let (files, warnings) = + generate_with_diagnostics(&[file, other], &["t.proto".to_string()], &config).unwrap(); + let code = joined(&files); + assert!(tables(&code).contains(&"Holds".to_string()), "{code}"); + assert!(table_warnings(&warnings).is_empty(), "{warnings:?}"); + assert!( + squashed(&code) + .contains("::buffa::table::DirectMsgVt::<::other_crate::Foreign>::via_message()"), + "{code}" + ); +} + +#[test] +fn a_map_entry_is_not_a_message_of_its_own() { + let (code, _) = run_maps(&table_config(CodecStrategy::Table)); + assert!( + !tables(&code).iter().any(|t| t.ends_with("Entry")), + "{:?}", + tables(&code) + ); +} + +#[test] +fn custom_types_in_maps_and_in_other_fields_share_one_summary_line() { + let config = CodeGenConfig { + map_fields: vec![( + ".t.Maps.counts".to_string(), + MapRepr::Custom("crate::MyMap".to_string()), + )], + string_fields: vec![( + ".t.Plain.s".to_string(), + StringRepr::Custom("crate::S".to_string()), + )], + ..table_config(CodecStrategy::Table) + }; + let (_, warnings) = run_maps(&config); + let (_, reasons) = summary(&warnings); + let custom: Vec<_> = reasons + .iter() + .filter(|(reason, _)| *reason == crate::table_plan::CUSTOM_TYPE_REASON) + .collect(); + assert_eq!(custom, [&(crate::table_plan::CUSTOM_TYPE_REASON, 2)]); +} diff --git a/buffa-test/build.rs b/buffa-test/build.rs index 89281d5c..33a46343 100644 --- a/buffa-test/build.rs +++ b/buffa-test/build.rs @@ -33,15 +33,16 @@ fn compile_both_codecs_with( .iter() .map(|path| format!(".{base}{suffix}.{path}")) .collect(); + // JSON and text first, so that `configure` can turn either off. configure( buffa_build::Config::new() - .files(&[renamed]) - .includes(&[&out]) .generate_json(true) - .generate_text(true) - .codec_strategy(strategy) - .codec_strategy_in(buffa_build::CodecStrategy::Unrolled, &rules), + .generate_text(true), ) + .files(&[renamed]) + .includes(&[&out]) + .codec_strategy(strategy) + .codec_strategy_in(buffa_build::CodecStrategy::Unrolled, &rules) .compile() .unwrap_or_else(|e| panic!("buffa_build failed for {file} ({suffix}): {e}")); } @@ -76,7 +77,8 @@ fn compile_extern_children() { format!( "syntax = \"proto3\";\npackage xf{suffix};\nimport \"xe.proto\";\n\ message Holder {{ xe.Leaf leaf = 1; repeated xe.Leaf leaves = 2; int32 tail = 3;\n\ - oneof pick {{ int32 n = 4; xe.Leaf pl = 5; }} }}\n" + oneof pick {{ int32 n = 4; xe.Leaf pl = 5; }}\n\ + map by_name = 6; }}\n" ), ) .expect("write proto"); @@ -104,7 +106,8 @@ fn compile_cross_package() { "syntax = \"proto3\";\npackage xa{suffix};\n\ message Leaf {{ int32 x = 1; string s = 2; }}\n\ message Wrap {{ Leaf leaf = 1; repeated Leaf leaves = 2; }}\n\ - message Cold {{ int64 c = 1; string s = 2; }}\n" + message Cold {{ int64 c = 1; string s = 2; }}\n\ + enum Mode {{ M0 = 0; M1 = 1; }}\n" ); let user = format!( "syntax = \"proto3\";\npackage xb{suffix};\nimport \"xa{suffix}.proto\";\n\ @@ -115,6 +118,10 @@ fn compile_cross_package() { Sub sub = 4;\n\ xa{suffix}.Cold cold = 5;\n\ repeated xa{suffix}.Cold colds = 6;\n\ + map by_name = 12;\n\ + map subs = 13;\n\ + map modes = 14;\n\ + map cold_map = 15;\n\ message Sub {{ xa{suffix}.Leaf l = 1; }}\n\ oneof pick {{ int32 n = 7; xa{suffix}.Leaf pl = 8; xa{suffix}.Wrap pw = 9; Sub ps = 10; xa{suffix}.Cold pc = 11; }}\n\ }}\n" @@ -259,24 +266,29 @@ fn main() { "tc", &[], ); - compile_both_codecs( + // `BTreeMap` for the maps in the proto2 schemas, so that the `Debug` text + // of a decoded message does not depend on hash order. + compile_both_codecs_with( "table_codec2.proto", &read_proto("table_codec2.proto"), "tc2", &[], + |config| config.map_type(buffa_build::MapRepr::BTreeMap), ); - compile_both_codecs( + compile_both_codecs_with( "table_codec3.proto", &read_proto("table_codec3.proto"), "tc3", &[], + |config| config.map_type(buffa_build::MapRepr::BTreeMap), ); compile_both_codecs("the generated wide schema", &wide_proto(), "wide", &[]); - compile_both_codecs( + compile_both_codecs_with( "table_bridge.proto", &read_proto("table_bridge.proto"), "br", &["Hot"], + |config| config.map_type(buffa_build::MapRepr::BTreeMap), ); compile_both_codecs( "table_codec4.proto", @@ -333,6 +345,59 @@ fn main() { ".tc4x.Twins", ], ); + // The proto2 schema again in a message that drops unknown fields, where + // an entry with an unknown closed-enum number is dropped. + let proto2 = read_proto("table_codec2.proto"); + compile_both_codecs_with( + "table_codec2.proto", + &proto2.replace("package tc2;", "package tcl;"), + "tcl", + &[], + |config| { + config + .preserve_unknown_fields(false) + .map_type(buffa_build::MapRepr::BTreeMap) + }, + ); + // Maps, as `tcm*` and `tcb*`; see protos/table_codec_maps.proto. + let maps = read_proto("table_codec_maps.proto"); + compile_both_codecs("table_codec_maps.proto", &maps, "tcm", &[]); + compile_both_codecs_with( + "table_codec_maps.proto", + &maps.replace("package tcm;", "package tcb;"), + "tcb", + &[], + |config| config.map_type(buffa_build::MapRepr::BTreeMap), + ); + // One message with maps of two collection types. The rule names both + // packages, because the two builds rename the package. + compile_both_codecs_with( + "table_codec_map_reprs.proto", + &read_proto("table_codec_map_reprs.proto"), + "tcr", + &[], + |config| { + config.map_type_in( + buffa_build::MapRepr::BTreeMap, + &[".tcru.MixedMaps.ordered", ".tcrt.MixedMaps.ordered"], + ) + }, + ); + // Maps of strings without UTF-8 validation, of `bytes`, and of another + // crate's messages. + compile_both_codecs_with( + "table_codec_utf8_maps.proto", + &read_proto("table_codec_utf8_maps.proto"), + "tcu8", + &[], + // Text output has no form for a `bytes` map key. + |config| { + config + .generate_text(false) + .strict_utf8_mapping(true) + .use_bytes_type() + }, + ); } // Basic proto — the original test file. Also the codegen target for diff --git a/buffa-test/protos/table_bridge.proto b/buffa-test/protos/table_bridge.proto index 176c8f3d..63b3654b 100644 --- a/buffa-test/protos/table_bridge.proto +++ b/buffa-test/protos/table_bridge.proto @@ -56,6 +56,21 @@ message Wkt { Cold cold = 13; } +// Maps whose values are table messages, a message that `brt` leaves unrolled, +// and well-known types, which a table message reaches through its `Message` +// impl. The maps are `BTreeMap`s, so the two codecs write the same bytes. +message MapBridge { + map leaves = 1; + map hots = 2; + map colds = 3; + map stamps = 4; + map anys = 5; + map values = 6; + map durations = 7; + map wrapped = 8; + int32 tail = 9; +} + // A oneof whose members are table messages, a message that `brt` leaves // unrolled, and well-known types, which a table message reaches through its // `Message` impl. diff --git a/buffa-test/protos/table_bytes.proto b/buffa-test/protos/table_bytes.proto index 97f3e368..4fe538d1 100644 --- a/buffa-test/protos/table_bytes.proto +++ b/buffa-test/protos/table_bytes.proto @@ -35,10 +35,25 @@ message HoldsPick { HoldsBlobInOneof inner = 1; } +// Holds a `Blob` as a map value, so it is unrolled, and so is `HoldsMapHolder`. +message HoldsBlobInMap { + map blobs = 1; + int32 tail = 2; +} + +message HoldsMapHolder { + HoldsBlobInMap inner = 1; +} + message Plain { int32 x = 1; } +// A map of messages that have no `bytes` field: a table. +message HoldsPlainInMap { + map plains = 1; +} + message HoldsPlain { Plain plain = 1; } diff --git a/buffa-test/protos/table_codec.proto b/buffa-test/protos/table_codec.proto index 6fb3898a..b6a81c35 100644 --- a/buffa-test/protos/table_codec.proto +++ b/buffa-test/protos/table_codec.proto @@ -112,14 +112,14 @@ message WithOneof { int32 c = 3; } -// A map, which the table cannot handle, so a message that has one stays -// unrolled when the table is requested. A message that holds one is a table -// message that reaches it through its `Message` impl. +// Two maps, one of messages: a table message. table_codec_maps.proto has maps +// of every type. message WithMap { map m = 1; map inners = 2; } +// Holds a message with a oneof and a message with maps. message HoldsMap { WithOneof o = 1; int32 x = 2; diff --git a/buffa-test/protos/table_codec2.proto b/buffa-test/protos/table_codec2.proto index 02a3ff89..4da09628 100644 --- a/buffa-test/protos/table_codec2.proto +++ b/buffa-test/protos/table_codec2.proto @@ -3,6 +3,13 @@ syntax = "proto2"; // See table_codec.proto: compiled as `tc2u` (unrolled) and `tc2t` (table). package tc2; +// A map value's enum must have a zero, so `ClosedMaps` uses this one. +enum Shade { + NONE = 0; + DARK = 1; + LIGHT = 2; +} + // Closed, and its first value is not zero. enum Color { RED = 1; @@ -89,3 +96,11 @@ message AllRepeated { repeated bytes by = 15; repeated Color color = 16; } + +// Map values that are closed enums, which make an entry with an unknown number +// unknown as a whole, and messages. +message ClosedMaps { + map colors = 1; + map inners = 2; + optional int32 tail = 3; +} diff --git a/buffa-test/protos/table_codec3.proto b/buffa-test/protos/table_codec3.proto index 109a9d4b..418d827d 100644 --- a/buffa-test/protos/table_codec3.proto +++ b/buffa-test/protos/table_codec3.proto @@ -36,3 +36,10 @@ message E { repeated Child children = 14; string implicit_s = 15 [features.field_presence = IMPLICIT]; } + +// Map values whose enums are closed and open by editions features. +message EMaps { + map closed = 1; + map open = 2; + map children = 3; +} diff --git a/buffa-test/protos/table_codec_map_reprs.proto b/buffa-test/protos/table_codec_map_reprs.proto new file mode 100644 index 00000000..8db999db --- /dev/null +++ b/buffa-test/protos/table_codec_map_reprs.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +// Compiled as `tcru`/`tcrt` (see table_codec.proto), with `map_type_in` giving +// `ordered` a `BTreeMap` and leaving `hashed` and `more` the default `HashMap`. +package tcr; + +message MixedMaps { + map hashed = 1; + map ordered = 2; + map more = 3; +} diff --git a/buffa-test/protos/table_codec_maps.proto b/buffa-test/protos/table_codec_maps.proto new file mode 100644 index 00000000..0706b360 --- /dev/null +++ b/buffa-test/protos/table_codec_maps.proto @@ -0,0 +1,84 @@ +syntax = "proto3"; + +// build.rs compiles this schema four times: `tcmu`/`tcmt` (unrolled/table, the +// default `HashMap` for map fields) and `tcbu`/`tcbt` (unrolled/table, with +// `BTreeMap`, whose iteration order makes the encoded bytes and `Debug` text +// comparable). See table_codec.proto. +package tcm; + +enum Color { + COLOR_UNSPECIFIED = 0; + RED = 1; + GREEN = 2; +} + +message Item { + int32 id = 1; + string label = 2; + repeated int32 tags = 3; + Item child = 4; +} + +// Every legal key type, with an `int32` value. +message Keys { + map i32 = 1; + map i64 = 2; + map u32 = 3; + map u64 = 4; + map s32 = 5; + map s64 = 6; + map b = 7; + map f32 = 8; + map f64 = 9; + map sf32 = 10; + map sf64 = 11; + map s = 12; +} + +// Every value type, with a `string` key. +message Values { + map i32 = 1; + map i64 = 2; + map u32 = 3; + map u64 = 4; + map s32 = 5; + map s64 = 6; + map b = 7; + map f32 = 8; + map f64 = 9; + map sf32 = 10; + map sf64 = 11; + map fl = 12; + map db = 13; + map s = 14; + map by = 15; + map color = 16; + map item = 17; +} + +// Maps whose values are the message itself or hold messages that hold maps, +// next to singular and repeated message fields. +message Nesting { + map children = 1; + map items = 2; + Item item = 3; + repeated Item many = 4; + int32 tail = 5; +} + +// Field numbers that miss the dense lookup array, and a message that holds +// maps. +message Sparse { + map a = 1; + int32 x = 2; + map far = 5000; + map last = 536870911; +} + +message Holder { + Values values = 1; + Keys keys = 2; + Nesting nesting = 3; + repeated Sparse sparse = 4; + map by_name = 5; +} diff --git a/buffa-test/protos/table_codec_utf8_maps.proto b/buffa-test/protos/table_codec_utf8_maps.proto new file mode 100644 index 00000000..3d7f42b6 --- /dev/null +++ b/buffa-test/protos/table_codec_utf8_maps.proto @@ -0,0 +1,32 @@ +edition = "2023"; + +// Compiled as `tcu8u`/`tcu8t` (see table_codec.proto), with +// `strict_utf8_mapping` and `use_bytes_type`, under which a string without +// UTF-8 validation is `Vec` and a `bytes` field is `Bytes`. +package tcu8; + +import "google/protobuf/wrappers.proto"; + +// Table messages: without UTF-8 validation a string is `Vec`. The key of +// `carve` is such a string, so the map keeps `Vec` values, though +// `use_bytes_type` would make them `Bytes`. +message Utf8Maps { + map checked = 1; + map raw = 2 [features.utf8_validation = NONE]; + map carve = 3 [features.utf8_validation = NONE]; +} + +// The values are `Bytes`, so a table cannot hold them and these stay unrolled. +message BytesValues { + map blobs = 1; +} + +message RawValues { + map raw_values = 1 [features.utf8_validation = NONE]; +} + +// The value is generated by another crate, which has no table here, so the +// table reaches it through its `Message` impl. +message WrapperValues { + map counts = 1; +} diff --git a/buffa-test/src/lib.rs b/buffa-test/src/lib.rs index a75c3055..36b9f3f7 100644 --- a/buffa-test/src/lib.rs +++ b/buffa-test/src/lib.rs @@ -1172,6 +1172,65 @@ pub mod widet { pub mod tcx { buffa::include_proto!("tcx"); } +// The proto2 schema with unknown fields dropped: `tclu`, `tclt`. +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tclu { + buffa::include_proto!("tclu"); +} +#[forbid(unsafe_code)] +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tclt { + buffa::include_proto!("tclt"); +} +// Maps; see protos/table_codec_maps.proto. +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcmu { + buffa::include_proto!("tcmu"); +} +#[forbid(unsafe_code)] +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcmt { + buffa::include_proto!("tcmt"); +} +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcbu { + buffa::include_proto!("tcbu"); +} +#[forbid(unsafe_code)] +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcbt { + buffa::include_proto!("tcbt"); +} +// Maps of two collection types, and of strings and bytes without UTF-8 +// validation; see the two protos. +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcru { + buffa::include_proto!("tcru"); +} +#[forbid(unsafe_code)] +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcrt { + buffa::include_proto!("tcrt"); +} +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcu8u { + buffa::include_proto!("tcu8u"); +} +#[forbid(unsafe_code)] +#[allow(clippy::derivable_impls, clippy::match_single_binding)] +#[cfg(has_table_codec)] +pub mod tcu8t { + buffa::include_proto!("tcu8t"); +} // `bru` is `table_bridge.proto` unrolled, and `brt` has the table codec except // for `Hot`, so table and unrolled messages hold each other. `xe` is a package @@ -1213,6 +1272,8 @@ pub mod tbz { // Two packages, the second holding messages of the first: `xau`/`xbu` unrolled, // `xat`/`xbt` with the table codec, and `xti` with the table codec and // `file_per_package` with `idiomatic_imports`, which holds `xati` and `xbti`. +// `xa*` holds an enum, whose generated `Default` impl clippy wants derived. +#[allow(clippy::derivable_impls)] #[cfg(has_table_codec)] pub mod xau { buffa::include_proto!("xau"); @@ -1222,6 +1283,7 @@ pub mod xbu { buffa::include_proto!("xbu"); } #[forbid(unsafe_code)] +#[allow(clippy::derivable_impls)] #[cfg(has_table_codec)] pub mod xat { buffa::include_proto!("xat"); @@ -1232,6 +1294,7 @@ pub mod xbt { buffa::include_proto!("xbt"); } #[forbid(unsafe_code)] +#[allow(clippy::derivable_impls)] #[cfg(has_table_codec)] pub mod xti { include!(concat!( diff --git a/buffa-test/src/tests/table_codec.rs b/buffa-test/src/tests/table_codec.rs index c38cdcca..ec8ce88a 100644 --- a/buffa-test/src/tests/table_codec.rs +++ b/buffa-test/src/tests/table_codec.rs @@ -12,6 +12,8 @@ use buffa::{DecodeError, Message}; use super::{length_delimited_field, varint_field}; +mod maps; + /// The error, or the value's re-encoding and `Debug` text. type Outcome = Result<(Vec, String), DecodeError>; @@ -385,10 +387,9 @@ fn invalid_utf8_is_rejected() { } #[test] -fn a_message_with_a_oneof_and_a_holder_of_a_message_with_a_map_round_trip() { - // A message with a oneof is a table. A message with a map is unrolled, and - // the messages that hold it are tables that reach it through their - // `Message` impl. +fn messages_with_a_oneof_or_maps_and_their_holders_round_trip() { + // A message with a oneof, a message with maps and the messages that hold + // them are all tables. let with_oneof = crate::tct::WithOneof { choice: Some(crate::tct::with_oneof::Choice::B("x".into())), c: 4, @@ -413,7 +414,7 @@ fn a_message_with_a_oneof_and_a_holder_of_a_message_with_a_map_round_trip() { } #[test] -fn messages_with_a_map_stay_unrolled_and_work() { +fn a_table_message_with_a_map_round_trips() { let with_map = crate::tct::WithMap { m: [("k".to_string(), 3)].into_iter().collect(), ..Default::default() @@ -939,6 +940,7 @@ fn the_messages_the_table_can_handle_use_it() { crate::tct::__BUFFA_TABLE_HoldsMap, crate::tct::__BUFFA_TABLE_Mixed, crate::tct::__BUFFA_TABLE_WithOneof, + crate::tct::__BUFFA_TABLE_WithMap, crate::tc2t::__BUFFA_TABLE_Req, crate::tc2t::__BUFFA_TABLE_AllRequired, crate::tc2t::__BUFFA_TABLE_AllRepeated, @@ -1030,6 +1032,24 @@ fn messages_held_across_packages_agree_in_every_layout() { cold: MessageField::some(cold(7)), colds: vec![$xa::Cold::default(), cold(8)], pick, + // One entry each, so that the bytes do not depend on the + // iteration order of the `HashMap`s. + by_name: [("n".to_string(), leaf(10))].into_iter().collect(), + subs: [( + 8, + $xb::holder::Sub { + l: MessageField::some(leaf(9)), + ..Default::default() + }, + )] + .into_iter() + .collect(), + modes: [("m".to_string(), buffa::EnumValue::from($xa::Mode::M1))] + .into_iter() + .collect(), + // Unrolled in the table layouts: a map value reached through + // its `Message` impl. + cold_map: [("c".to_string(), cold(11))].into_iter().collect(), ..Default::default() }) .collect::>() @@ -1059,7 +1079,7 @@ macro_rules! bridge_samples { ($name:ident, $m:ident) => { mod $name { use crate::$m::pick::Choice; - use crate::$m::{Cold, Hot, Leaf, Pick, Wkt}; + use crate::$m::{Cold, Hot, Leaf, MapBridge, Pick, Wkt}; use buffa::MessageField; use buffa_types::google::protobuf::{ Any, Duration, Empty, FieldMask, Int32Value, StringValue, Struct, Timestamp, Value, @@ -1132,6 +1152,62 @@ macro_rules! bridge_samples { .collect() } + /// Every kind of map value, with entries that are defaults. + pub fn map_bridge() -> MapBridge { + let mut st = Struct::new(); + st.insert("k", 1.5); + MapBridge { + leaves: [ + ("a".into(), leaf(1, "l", &[2])), + ("b".into(), Leaf::default()), + ] + .into_iter() + .collect(), + hots: [(-1, hot(2, true)), (7, Hot::default())] + .into_iter() + .collect(), + colds: [("c".into(), cold()), ("d".into(), Cold::default())] + .into_iter() + .collect(), + stamps: [ + ("t".into(), Timestamp::from_unix(1_700_000_000, 5)), + ("z".into(), Timestamp::default()), + ] + .into_iter() + .collect(), + anys: [ + ( + 1, + Any::pack( + &Timestamp::from_unix(1, 2), + "type.googleapis.com/google.protobuf.Timestamp", + ), + ), + (2, Any::default()), + ] + .into_iter() + .collect(), + values: [ + ("s".into(), Value::from("v")), + ("n".into(), Value::null()), + ("f".into(), Value::from(2.5)), + ] + .into_iter() + .collect(), + durations: [("d".into(), Duration::from_secs_nanos(-3, -4))] + .into_iter() + .collect(), + wrapped: [ + ("zero".into(), Int32Value::from(0)), + ("one".into(), Int32Value::from(1)), + ] + .into_iter() + .collect(), + tail: 5, + ..Default::default() + } + } + pub fn wkt() -> Wkt { let mut st = Struct::new(); st.insert("k", 1.5); @@ -1175,6 +1251,110 @@ fn messages_that_hold_each_other_across_both_codecs_agree() { assert_same_on_corrupt_input::(&wire, true); } +#[test] +fn map_values_that_have_no_table_agree() { + // `Leaf` and `Cold` are tables in `brt`, `Hot` is not, and the well-known + // types never are, so this map holder reaches each through its `Message` + // impl or its table. + let wire = assert_same_codec(&bru_s::map_bridge(), &brt_s::map_bridge()); + assert_same_chained::(&wire); + assert_same_on_corrupt_input::(&wire, true); + // A value set to its default is still written, as an empty record. + for (field, key) in [ + (1, length_delimited_field(1, b"k")), + (2, varint_field(1, 5)), + (4, length_delimited_field(1, b"k")), + (8, length_delimited_field(1, b"k")), + ] { + let wire = length_delimited_field(field, &[key, length_delimited_field(2, b"")].concat()); + assert_same_decode::(&wire, false); + let decoded = crate::brt::MapBridge::decode_from_slice(&wire).unwrap(); + assert_eq!(decoded.encode_to_vec(), wire, "field {field}"); + } +} + +#[test] +fn a_map_entry_that_fails_leaves_the_message_as_unrolled_code_does() { + let opts = buffa::DecodeOptions::new(); + // Fields 2 and 5 have `int32` keys, the others `string` keys. + let key = |field: u32| match field { + 2 | 5 => varint_field(1, 5), + _ => length_delimited_field(1, b"k"), + }; + let mut wires = Vec::new(); + for field in 1..=8u32 { + // Cut short at the tag, inside the entry's length, and inside its key. + wires.push(tag(field, 2)); + wires.push([tag(field, 2), vec![0x05, 0x0a]].concat()); + wires.push(length_delimited_field(field, &[0x0a, 0x05, b'k'])); + // A value that does not decode, after a key that does: a field that + // is complete, and then a tag with nothing after it. + let value = length_delimited_field(2, &[0x08, 0x05, 0x10]); + let entry = [key(field), value].concat(); + wires.push(length_delimited_field(field, &entry)); + // A value whose length is past the end of the entry. + let entry = [key(field), vec![0x12, 0x7f, 0x08]].concat(); + wires.push(length_delimited_field(field, &entry)); + } + // A string key that is not UTF-8. + wires.push(length_delimited_field( + 1, + &[length_delimited_field(1, &[0xff, 0xfe])].concat(), + )); + let valid_first = length_delimited_field( + 1, + &[ + length_delimited_field(1, b"new"), + length_delimited_field(2, &[0x08, 0x09]), + ] + .concat(), + ); + let mut failed = 0; + for wire in wires { + for prefix in [&[][..], &valid_first[..]] { + let wire = [prefix, &wire[..]].concat(); + failed += usize::from(crate::brt::MapBridge::decode_from_slice(&wire).is_err()); + assert_same_merge(&opts, &bru_s::map_bridge(), &brt_s::map_bridge(), &wire); + assert_same_merge( + &opts, + &crate::bru::MapBridge::default(), + &crate::brt::MapBridge::default(), + &wire, + ); + } + } + // Most of the inputs fail, and so exercise the failure paths. + assert!(failed >= 60, "only {failed} of the inputs fail"); +} + +#[test] +fn a_map_value_that_nests_deeply_hits_the_same_recursion_limit_in_both_codecs() { + // `Cold.hot` (1) holds `Hot.back` (3), which holds a `Cold` again, so each + // level is two messages, and the map's value is the first. + fn cold(levels: usize) -> Vec { + if levels == 0 { + return Vec::new(); + } + length_delimited_field(1, &length_delimited_field(3, &cold(levels - 1))) + } + let mut accepted = Vec::new(); + for levels in [1, 10, 40, 47, 48, 49, 50, 51, 60] { + let entry = [ + length_delimited_field(1, b"k"), + length_delimited_field(2, &cold(levels)), + ] + .concat(); + let wire = length_delimited_field(3, &entry); + assert_same_decode::(&wire, true); + accepted.push(crate::brt::MapBridge::decode_from_slice(&wire).is_ok()); + } + // The depths straddle the limit. + assert!( + accepted.contains(&true) && accepted.contains(&false), + "{accepted:?}" + ); +} + #[test] fn well_known_types_held_by_a_table_message_agree() { let wire = assert_same_codec(&bru_s::wkt(), &brt_s::wkt()); @@ -1350,6 +1530,8 @@ fn messages_from_another_crate_agree() { leaves: vec![leaf(2, "b"), leaf(3, "")], tail: 4, pick, + // A map value from another crate. + by_name: [("k".to_string(), leaf(7, "m"))].into_iter().collect(), ..Default::default() }) .collect::>() @@ -1511,6 +1693,11 @@ fn a_holder_of_a_bytes_typed_message_is_unrolled_and_decodes_without_copying() { // Also the one that holds it in a oneof member, and its own holder. assert!(!generated.contains("__BUFFA_TABLE_HoldsBlobInOneof")); assert!(!generated.contains("__BUFFA_TABLE_HoldsPick")); + // And through a map value, with its own holder. A map of plain messages is + // a table. + assert!(!generated.contains("__BUFFA_TABLE_HoldsBlobInMap")); + assert!(!generated.contains("__BUFFA_TABLE_HoldsMapHolder")); + assert!(generated.contains("static __BUFFA_TABLE_HoldsPlainInMap")); let blob = |fill: u8| Blob { data: Bytes::from(vec![fill; 64]), @@ -1558,6 +1745,28 @@ fn a_holder_of_a_bytes_typed_message_is_unrolled_and_decodes_without_copying() { panic!("expected a Blob"); }; assert!(aliases(&held.data) && aliases(&held.chunks[0])); + + // And the map value does. + let msg = crate::tbz::HoldsMapHolder { + inner: MessageField::some(crate::tbz::HoldsBlobInMap { + blobs: [("a".to_string(), blob(9)), ("b".to_string(), blob(11))] + .into_iter() + .collect(), + tail: 2, + ..Default::default() + }), + ..Default::default() + }; + let src = Bytes::from(msg.encode_to_vec()); + let range = src.as_ptr() as usize..src.as_ptr() as usize + src.len(); + let aliases = |b: &Bytes| range.contains(&(b.as_ptr() as usize)); + let decoded = crate::tbz::HoldsMapHolder::decode(&mut src.clone()).unwrap(); + assert_eq!(decoded, msg); + let held = &decoded.inner.as_option().unwrap().blobs; + assert_eq!(held.len(), 2); + assert!(held + .values() + .all(|b| aliases(&b.data) && aliases(&b.chunks[0]))); } // --------------------------------------------------------------------------- diff --git a/buffa-test/src/tests/table_codec/maps.rs b/buffa-test/src/tests/table_codec/maps.rs new file mode 100644 index 00000000..eebc9549 --- /dev/null +++ b/buffa-test/src/tests/table_codec/maps.rs @@ -0,0 +1,1063 @@ +//! Map fields under the table codec, against the unrolled codec. The packages +//! (`tcbu`, `tcbt`, `tcmu`, `tcmt`) are described in +//! protos/table_codec_maps.proto. + +use core::fmt::Debug; + +use buffa::encoding::{encode_varint, Tag, WireType}; +use buffa::{DecodeError, DecodeOptions, Message}; + +use super::{ + assert_same_chained, assert_same_codec, assert_same_decode, assert_same_on_corrupt_input, + length_delimited_field, varint_field, +}; + +/// The same values in the generated module `$m`. +macro_rules! samples { + ($name:ident, $m:ident) => { + // The `HashMap` variants use only some of the samples. + #[allow(dead_code)] + mod $name { + use crate::$m::{Color, Holder, Item, Keys, Nesting, Sparse, Values}; + use buffa::{EnumValue, MessageField}; + + pub fn item(id: i32, label: &str) -> Item { + Item { + id, + label: label.into(), + tags: vec![id, -id], + ..Default::default() + } + } + + pub fn keys() -> Keys { + Keys { + i32: [(i32::MIN, 1), (0, 0), (i32::MAX, -1)] + .into_iter() + .collect(), + i64: [(i64::MIN, 1), (0, 2), (i64::MAX, 3)].into_iter().collect(), + u32: [(0, 1), (u32::MAX, 2)].into_iter().collect(), + u64: [(0, 1), (u64::MAX, 2)].into_iter().collect(), + s32: [(i32::MIN, 1), (-1, 2), (1, 3)].into_iter().collect(), + s64: [(i64::MIN, 1), (-1, 2), (1, 3)].into_iter().collect(), + b: [(false, 1), (true, 2)].into_iter().collect(), + f32: [(0, 1), (u32::MAX, 2)].into_iter().collect(), + f64: [(0, 1), (u64::MAX, 2)].into_iter().collect(), + sf32: [(i32::MIN, 1), (-1, 2), (i32::MAX, 3)] + .into_iter() + .collect(), + sf64: [(i64::MIN, 1), (0, 2), (i64::MAX, 3)].into_iter().collect(), + s: [ + (String::new(), 1), + ("a".to_string(), 2), + ("b".repeat(200), 3), + ] + .into_iter() + .collect(), + ..Default::default() + } + } + + pub fn values() -> Values { + let key = |n: &str| n.to_string(); + Values { + i32: [(key("a"), i32::MIN), (key("b"), 0), (key("c"), 7)] + .into_iter() + .collect(), + i64: [(key("a"), i64::MIN), (key("b"), i64::MAX)] + .into_iter() + .collect(), + u32: [(key("a"), 0), (key("b"), u32::MAX)].into_iter().collect(), + u64: [(key("a"), 0), (key("b"), u64::MAX)].into_iter().collect(), + s32: [(key("a"), i32::MIN), (key("b"), -1), (key("c"), 1)] + .into_iter() + .collect(), + s64: [(key("a"), i64::MIN), (key("b"), 1)].into_iter().collect(), + b: [(key("f"), false), (key("t"), true)].into_iter().collect(), + f32: [(key("a"), 0), (key("b"), u32::MAX)].into_iter().collect(), + f64: [(key("a"), 0), (key("b"), u64::MAX)].into_iter().collect(), + sf32: [(key("a"), i32::MIN), (key("b"), i32::MAX)] + .into_iter() + .collect(), + sf64: [(key("a"), i64::MIN), (key("b"), i64::MAX)] + .into_iter() + .collect(), + fl: [(key("a"), 0.0), (key("neg"), -0.0), (key("b"), 1.5)] + .into_iter() + .collect(), + db: [(key("a"), f64::MAX), (key("b"), f64::MIN_POSITIVE)] + .into_iter() + .collect(), + s: [ + (key(""), key("")), + (key("k"), key("v")), + (key("wide"), key("h\u{e9}llo \u{1f600}")), + ] + .into_iter() + .collect(), + by: [(key("a"), vec![]), (key("b"), vec![0, 255, 7])] + .into_iter() + .collect(), + color: [ + (key("red"), EnumValue::from(Color::RED)), + (key("zero"), EnumValue::from(Color::COLOR_UNSPECIFIED)), + (key("other"), EnumValue::from(99)), + ] + .into_iter() + .collect(), + item: [ + ( + key("a"), + Item { + child: MessageField::some(item(9, "nine")), + ..item(1, "one") + }, + ), + (key("b"), Item::default()), + ] + .into_iter() + .collect(), + ..Default::default() + } + } + + pub fn nesting() -> Nesting { + let leaf = |tail| Nesting { + tail, + ..Default::default() + }; + let mid = Nesting { + children: [("leaf".to_string(), leaf(1)), ("empty".into(), leaf(0))] + .into_iter() + .collect(), + items: [(2, item(2, "two"))].into_iter().collect(), + ..Default::default() + }; + Nesting { + children: [("mid".to_string(), mid), ("other".into(), leaf(5))] + .into_iter() + .collect(), + items: [ + (-1, item(-1, "m")), + (0, Item::default()), + (70_000, item(3, "")), + ] + .into_iter() + .collect(), + item: MessageField::some(item(4, "four")), + many: vec![item(5, "five"), Item::default()], + tail: 9, + ..Default::default() + } + } + + pub fn sparse() -> Sparse { + Sparse { + a: [("a".to_string(), "b".to_string())].into_iter().collect(), + x: 3, + far: [(1, "one".to_string()), (-1, String::new())] + .into_iter() + .collect(), + last: [("z".to_string(), "y".to_string())].into_iter().collect(), + ..Default::default() + } + } + + pub fn holder() -> Holder { + Holder { + values: MessageField::some(values()), + keys: MessageField::some(keys()), + nesting: MessageField::some(nesting()), + sparse: vec![sparse(), Sparse::default()], + by_name: [ + ( + "self".to_string(), + Holder { + keys: MessageField::some(keys()), + ..Default::default() + }, + ), + ("empty".into(), Holder::default()), + ] + .into_iter() + .collect(), + ..Default::default() + } + } + } + }; +} + +samples!(bu, tcbu); +samples!(bt, tcbt); +samples!(hu, tcmu); +samples!(ht, tcmt); + +// --------------------------------------------------------------------------- +// Both codecs, on values +// --------------------------------------------------------------------------- + +/// Both codecs give the same bytes for the same value, decode each other's +/// bytes, and give the same outcome on the wire cut, chained and corrupted. +macro_rules! agree { + ($test:ident, $ty:ident, $sample:ident) => { + #[test] + fn $test() { + let wire = assert_same_codec(&bu::$sample(), &bt::$sample()); + assert!(!wire.is_empty()); + assert_same_chained::(&wire); + assert_same_on_corrupt_input::(&wire, true); + } + }; +} + +agree!(every_key_type_agrees, Keys, keys); +agree!(every_value_type_agrees, Values, values); +agree!(maps_of_messages_and_nested_messages_agree, Nesting, nesting); +agree!(field_numbers_outside_the_dense_lookup_agree, Sparse, sparse); +agree!(a_message_holding_maps_agrees, Holder, holder); + +#[test] +fn empty_maps_encode_to_nothing() { + assert!(crate::tcbt::Values::default().encode_to_vec().is_empty()); + assert!(crate::tcbt::Holder::default().encode_to_vec().is_empty()); + assert_eq!(crate::tcbt::Keys::default().encoded_len(), 0); +} + +#[test] +fn clear_empties_every_map() { + let mut table = bt::values(); + let mut unrolled = bu::values(); + table.clear(); + unrolled.clear(); + assert_eq!(table, crate::tcbt::Values::default()); + assert_eq!(unrolled, crate::tcbu::Values::default()); + assert!(table.encode_to_vec().is_empty()); +} + +#[test] +fn unknown_fields_next_to_maps_are_kept() { + let mut wire = bu::values().encode_to_vec(); + wire.extend(varint_field(9999, 7)); + wire.extend(length_delimited_field(9998, b"x")); + let table = crate::tcbt::Values::decode_from_slice(&wire).unwrap(); + let unrolled = crate::tcbu::Values::decode_from_slice(&wire).unwrap(); + assert_eq!(format!("{table:?}"), format!("{unrolled:?}")); + assert_eq!(table.encode_to_vec(), unrolled.encode_to_vec()); +} + +/// The entries of a map, sorted, as text. +fn sorted(map: &std::collections::HashMap) -> String { + let mut entries: Vec<_> = map.iter().collect(); + entries.sort_by(|a, b| a.0.cmp(b.0)); + format!("{entries:?}") +} + +/// The maps of a message with `HashMap` fields, each as sorted text. +macro_rules! canonical { + ($msg:expr, [$($field:ident),*]) => { + vec![$(sorted(&$msg.$field)),*] + }; +} + +#[test] +fn hash_maps_agree_through_their_sorted_entries() { + let (unrolled, table) = (hu::keys(), ht::keys()); + assert_eq!(unrolled.encoded_len(), table.encoded_len()); + let from_unrolled = crate::tcmt::Keys::decode_from_slice(&unrolled.encode_to_vec()).unwrap(); + let from_table = crate::tcmu::Keys::decode_from_slice(&table.encode_to_vec()).unwrap(); + let fields = |k: &crate::tcmt::Keys| { + canonical!( + k, + [i32, i64, u32, u64, s32, s64, b, f32, f64, sf32, sf64, s] + ) + }; + assert_eq!(fields(&from_unrolled), fields(&table)); + assert_eq!(from_table, unrolled); + + let (unrolled, table) = (hu::values(), ht::values()); + assert_eq!(unrolled.encoded_len(), table.encoded_len()); + let from_unrolled = crate::tcmt::Values::decode_from_slice(&unrolled.encode_to_vec()).unwrap(); + let from_table = crate::tcmu::Values::decode_from_slice(&table.encode_to_vec()).unwrap(); + assert_eq!(from_unrolled, table); + assert_eq!(from_table, unrolled); +} + +/// The outcome of decoding `wire` into the `HashMap` message `M`, as the text of +/// the same message with `BTreeMap`s, which is independent of iteration order. +fn hash_outcome(wire: &[u8]) -> Result { + M::decode_from_slice(wire) + .map(|m| format!("{:?}", C::decode_from_slice(&m.encode_to_vec()).unwrap())) +} + +#[test] +fn hash_maps_agree_on_corrupt_input() { + macro_rules! check { + ($ty:ident, $wire:expr) => {{ + let wire: Vec = $wire; + // As in `assert_same_decode`, a rejection may differ when the table + // reports the end of a message value's slice first. + let same = |input: &[u8]| { + let table = hash_outcome::(input); + let unrolled = hash_outcome::(input); + assert!( + table == unrolled + || (unrolled.is_err() && table == Err(DecodeError::UnexpectedEof)), + "input {input:02x?}: unrolled {unrolled:?}, table {table:?}" + ); + }; + for end in 0..=wire.len() { + same(&wire[..end]); + } + let mut flipped = wire.clone(); + for i in 0..wire.len() { + for xor in [0x01, 0x80, 0xff] { + flipped[i] = wire[i] ^ xor; + same(&flipped); + } + flipped[i] = wire[i]; + } + }}; + } + check!(Keys, hu::keys().encode_to_vec()); + check!(Values, hu::values().encode_to_vec()); + for wire in value_entries() { + check!(Values, wire); + } +} + +// --------------------------------------------------------------------------- +// Entries built by hand +// --------------------------------------------------------------------------- + +fn field_tag(number: u32, wire_type: WireType) -> Vec { + let mut out = Vec::new(); + Tag::new(number, wire_type).encode(&mut out); + out +} + +/// An entry of field 1 that declares `length` bytes, followed by `tail`. +fn entry_declaring(length: u64, tail: &[u8]) -> Vec { + let mut out = field_tag(1, WireType::LengthDelimited); + encode_varint(length, &mut out); + out.extend_from_slice(tail); + out +} + +/// A map entry of field `number`, with the given contents. +fn entry(number: u32, contents: &[&[u8]]) -> Vec { + length_delimited_field(number, &contents.concat()) +} + +/// Wire inputs for `Values`: entries of unusual shape in every map. +fn value_entries() -> Vec> { + let mut cases: Vec> = Vec::new(); + // An empty entry in every map, and one with only a key or only a value. + for number in 1..=17 { + cases.push(entry(number, &[])); + cases.push(entry(number, &[&length_delimited_field(1, b"key")])); + } + cases.push(entry(1, &[&varint_field(2, 5)])); + cases.push(entry(14, &[&length_delimited_field(2, b"only a value")])); + // Unknown fields of every wire type in an entry. + cases.push(entry( + 1, + &[ + &varint_field(1000, 3), + &length_delimited_field(1, b"k"), + &length_delimited_field(3, b"skipped"), + &varint_field(2, 8), + &[0x2d, 1, 2, 3, 4], + &[0x31, 1, 2, 3, 4, 5, 6, 7, 8], + ], + )); + // A group in an entry, and one nested too deep. + cases.push(entry( + 1, + &[ + &length_delimited_field(1, b"g"), + &[0x1b, 0x08, 0x01, 0x1c], + &varint_field(2, 1), + ], + )); + cases.push(entry(1, &[&[0x1b].repeat(200), &[0x1c].repeat(200)])); + cases.push(entry(1, &[&[0x1b, 0x08, 0x01]])); + // A key or a value more than once, and in either order. + cases.push(entry( + 1, + &[ + &length_delimited_field(1, b"first"), + &varint_field(2, 1), + &length_delimited_field(1, b"second"), + &varint_field(2, 2), + ], + )); + cases.push(entry( + 1, + &[&varint_field(2, 6), &length_delimited_field(1, b"k")], + )); + // The same key in two entries. + cases.push( + [ + entry(1, &[&length_delimited_field(1, b"k"), &varint_field(2, 1)]), + entry(1, &[&length_delimited_field(1, b"k"), &varint_field(2, 2)]), + ] + .concat(), + ); + // A message value that is repeated merges its parts, and one that fails + // is not inserted. + cases.push(entry( + 17, + &[ + &length_delimited_field(1, b"k"), + &length_delimited_field(2, &varint_field(1, 10)), + &length_delimited_field(2, &length_delimited_field(2, b"label")), + &length_delimited_field(2, &length_delimited_field(4, &varint_field(1, 3))), + ], + )); + cases.push(entry( + 17, + &[ + &length_delimited_field(1, b"k"), + &length_delimited_field(2, &length_delimited_field(2, &[0xff])), + ], + )); + cases.push(entry( + 17, + &[ + &length_delimited_field(1, b"k"), + &length_delimited_field(2, &[0x0a]), + ], + )); + // An open enum keeps a number it does not know. + cases.push(entry( + 16, + &[&length_delimited_field(1, b"k"), &varint_field(2, 12345)], + )); + cases.push(entry( + 16, + &[&length_delimited_field(1, b"k"), &varint_field(2, u64::MAX)], + )); + // Wrong wire types for the key and for the value of each kind. + for number in 1..=17 { + cases.push(entry(number, &[&varint_field(1, 1)])); + cases.push(entry(number, &[&[0x0d, 1, 2, 3, 4]])); + cases.push(entry( + number, + &[ + &length_delimited_field(1, b"k"), + &length_delimited_field(2, b"x"), + ], + )); + cases.push(entry( + number, + &[&length_delimited_field(1, b"k"), &[0x15, 1, 2, 3, 4]], + )); + cases.push(entry( + number, + &[ + &length_delimited_field(1, b"k"), + &[0x19, 1, 2, 3, 4, 5, 6, 7, 8], + ], + )); + } + // Invalid UTF-8 in a key and in a value. + cases.push(entry(1, &[&length_delimited_field(1, &[0xff, 0xfe])])); + cases.push(entry( + 14, + &[ + &length_delimited_field(1, b"k"), + &length_delimited_field(2, &[0xc0, 0x80]), + ], + )); + // The entry itself with the wrong wire type. + cases.push(varint_field(1, 1)); + cases.push([field_tag(1, WireType::Fixed32), vec![0; 4]].concat()); + // Lengths that do not fit: an entry longer than the input, a string longer + // than its entry, an entry of length 2^64 - 1. + cases.push(vec![0x0a, 0x09, 0x0a]); + cases.push([&[0x0a, 0x03, 0x0a, 0x05, b'a'][..], b"bcde"].concat()); + cases.push(entry_declaring(u64::MAX, &[])); + cases.push(entry_declaring(1 << 31, b"x")); + // An entry cut anywhere, with trailing bytes. + let whole = entry( + 17, + &[ + &length_delimited_field(1, b"key"), + &length_delimited_field(2, &length_delimited_field(2, b"label")), + ], + ); + for end in 0..whole.len() { + cases.push(whole[..end].to_vec()); + } + cases.push([whole.clone(), vec![0x08]].concat()); + cases +} + +#[test] +fn entries_of_unusual_shape_decode_the_same() { + for wire in value_entries() { + assert_same_decode::(&wire, true); + assert_same_chained::(&wire); + } +} + +#[test] +fn key_entries_of_unusual_shape_decode_the_same() { + let mut cases = Vec::new(); + for number in 1..=12 { + cases.push(entry(number, &[])); + cases.push(entry(number, &[&varint_field(2, 4)])); + cases.push(entry(number, &[&varint_field(1, 1), &varint_field(2, 4)])); + cases.push(entry( + number, + &[&length_delimited_field(1, b"x"), &varint_field(2, 4)], + )); + cases.push(entry(number, &[&[0x0d, 9, 9, 9, 9], &varint_field(2, 4)])); + cases.push(entry( + number, + &[&[0x09, 1, 2, 3, 4, 5, 6, 7, 8], &varint_field(2, 4)], + )); + cases.push(entry( + number, + &[&varint_field(1, u64::MAX), &varint_field(2, 4)], + )); + cases.push(entry( + number, + &[&varint_field(1, 1 << 40), &varint_field(2, 4)], + )); + cases.push(entry( + number, + &[&varint_field(1, 1), &length_delimited_field(2, b"x")], + )); + } + for wire in cases { + assert_same_decode::(&wire, true); + } +} + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +fn outcome_with( + options: &DecodeOptions, + wire: &[u8], +) -> Result { + options + .decode_from_slice::(wire) + .map(|m| format!("{m:?}")) +} + +#[test] +fn the_element_memory_limit_counts_each_entry_in_both_codecs() { + // A thousand empty entries of a scalar map, a string map, a bytes map and + // a message map: each still creates a key and a value. + for number in [1, 14, 15, 17] { + let wire: Vec = (0..1000).flat_map(|_| entry(number, &[])).collect(); + for limit in [50, 100, 4000, 64_000, 1_000_000] { + let options = DecodeOptions::new().with_element_memory_limit(limit); + assert_eq!( + outcome_with::(&options, &wire), + outcome_with::(&options, &wire), + "field {number}, limit {limit}" + ); + } + let options = DecodeOptions::new().with_element_memory_limit(100); + assert_eq!( + outcome_with::(&options, &wire), + Err(DecodeError::ElementMemoryLimitExceeded) + ); + } +} + +#[test] +fn the_recursion_limit_counts_message_values_in_both_codecs() { + // children (1): key "k", value: the same message, `depth` deep. + fn nested(depth: usize) -> Vec { + let mut wire = Vec::new(); + for _ in 0..depth { + wire = entry( + 1, + &[ + &length_delimited_field(1, b"k"), + &length_delimited_field(2, &wire), + ], + ); + } + wire + } + for depth in [1, 50, 90, 95, 98, 99, 100, 101, 102, 110, 200] { + let wire = nested(depth); + for limit in [5, 20, 100] { + let options = DecodeOptions::new().with_recursion_limit(limit); + assert_eq!( + outcome_with::(&options, &wire), + outcome_with::(&options, &wire), + "depth {depth}, limit {limit}" + ); + } + } + assert_eq!( + crate::tcbt::Nesting::decode_from_slice(&nested(200)), + Err(DecodeError::RecursionLimitExceeded) + ); +} + +// --------------------------------------------------------------------------- +// Closed enums +// --------------------------------------------------------------------------- + +#[test] +fn an_entry_with_an_unknown_closed_enum_number_is_kept_as_unknown() { + // proto2: colors[k] = 7, which `Color` does not have. + let unknown = entry(1, &[&length_delimited_field(1, b"k"), &varint_field(2, 7)]); + let known = entry(1, &[&length_delimited_field(1, b"j"), &varint_field(2, 2)]); + for wire in [ + unknown.clone(), + [known.clone(), unknown.clone()].concat(), + entry( + 1, + &[ + &length_delimited_field(1, b"k"), + &varint_field(2, 7), + &varint_field(2, 2), + ], + ), + entry( + 1, + &[ + &length_delimited_field(1, b"k"), + &varint_field(2, 2), + &varint_field(2, 7), + ], + ), + entry(1, &[&varint_field(2, 7)]), + entry( + 1, + &[&length_delimited_field(1, b"k"), &varint_field(2, u64::MAX)], + ), + ] { + assert_same_decode::(&wire, true); + let decoded = crate::tc2t::ClosedMaps::decode_from_slice(&wire).unwrap(); + assert_eq!( + decoded.encode_to_vec().len(), + crate::tc2u::ClosedMaps::decode_from_slice(&wire) + .unwrap() + .encode_to_vec() + .len() + ); + } + let decoded = crate::tc2t::ClosedMaps::decode_from_slice(&unknown).unwrap(); + assert!(decoded.colors.is_empty()); + assert_eq!(decoded.encode_to_vec(), unknown, "the whole entry is kept"); +} + +#[test] +fn an_entry_with_an_unknown_closed_enum_number_is_dropped_without_unknown_fields() { + let unknown = entry(1, &[&length_delimited_field(1, b"k"), &varint_field(2, 7)]); + let known = entry(1, &[&length_delimited_field(1, b"j"), &varint_field(2, 2)]); + for wire in [ + unknown.clone(), + [known.clone(), unknown.clone()].concat(), + entry( + 1, + &[ + &length_delimited_field(1, b"k"), + &varint_field(2, 7), + &varint_field(2, 2), + ], + ), + entry( + 1, + &[ + &length_delimited_field(1, b"k"), + &varint_field(2, 2), + &varint_field(2, 7), + ], + ), + ] { + assert_same_decode::(&wire, true); + } + let decoded = crate::tclt::ClosedMaps::decode_from_slice(&unknown).unwrap(); + assert!(decoded.colors.is_empty()); + assert!(decoded.encode_to_vec().is_empty()); +} + +#[test] +fn the_unknown_field_limit_counts_kept_entries_in_both_codecs() { + let wire: Vec = (0..100) + .flat_map(|_| entry(1, &[&length_delimited_field(1, b"k"), &varint_field(2, 7)])) + .collect(); + for limit in [1, 10, 99, 100, 101, 1000] { + let options = DecodeOptions::new().with_unknown_field_limit(limit); + assert_eq!( + outcome_with::(&options, &wire), + outcome_with::(&options, &wire), + "limit {limit}" + ); + } + let options = DecodeOptions::new().with_unknown_field_limit(10); + assert_eq!( + outcome_with::(&options, &wire), + Err(DecodeError::UnknownFieldLimitExceeded) + ); + // Dropped entries are not counted. + assert!(outcome_with::(&options, &wire).is_ok()); +} + +#[test] +fn closed_enum_maps_agree_on_corrupt_input() { + let mut wire = entry(1, &[&length_delimited_field(1, b"a"), &varint_field(2, 1)]); + wire.extend(entry( + 1, + &[&length_delimited_field(1, b"b"), &varint_field(2, 7)], + )); + wire.extend(entry( + 2, + &[ + &varint_field(1, 3), + &length_delimited_field(2, &varint_field(1, 4)), + ], + )); + wire.extend(varint_field(3, 9)); + assert_same_on_corrupt_input::(&wire, true); + assert_same_on_corrupt_input::(&wire, true); +} + +#[test] +fn editions_enum_openness_applies_to_map_values() { + use crate::{tc3t, tc3u}; + // closed = { "k": 7 } (C_A = 0, C_B = 1 only), open = { 1: 7 }. + let wire = [ + entry(1, &[&length_delimited_field(1, b"k"), &varint_field(2, 7)]), + entry(2, &[&varint_field(1, 1), &varint_field(2, 7)]), + entry(1, &[&length_delimited_field(1, b"j"), &varint_field(2, 1)]), + entry( + 3, + &[ + &length_delimited_field(1, b"c"), + &length_delimited_field(2, &varint_field(1, 5)), + ], + ), + ] + .concat(); + assert_same_decode::(&wire, true); + let table = tc3t::EMaps::decode_from_slice(&wire).unwrap(); + assert_eq!(table.closed.len(), 1); + assert_eq!(table.open.len(), 1); + assert_same_on_corrupt_input::(&wire, true); +} + +// --------------------------------------------------------------------------- +// Which messages use the table +// --------------------------------------------------------------------------- + +/// Referencing a message's table is a compile error when it fell back to +/// unrolled code. +#[test] +fn messages_with_maps_and_the_messages_that_hold_them_use_the_table() { + fn is_table(_: &'static buffa::table::Table) {} + macro_rules! tables { + ($($path:path),* $(,)?) => { $( is_table(&$path); )* }; + } + tables!( + crate::tcbt::__BUFFA_TABLE_Keys, + crate::tcbt::__BUFFA_TABLE_Values, + crate::tcbt::__BUFFA_TABLE_Nesting, + crate::tcbt::__BUFFA_TABLE_Sparse, + crate::tcbt::__BUFFA_TABLE_Holder, + crate::tcmt::__BUFFA_TABLE_Keys, + crate::tcmt::__BUFFA_TABLE_Values, + crate::tcmt::__BUFFA_TABLE_Holder, + crate::tc2t::__BUFFA_TABLE_ClosedMaps, + crate::tclt::__BUFFA_TABLE_ClosedMaps, + crate::tc3t::__BUFFA_TABLE_EMaps, + crate::tcrt::__BUFFA_TABLE_MixedMaps, + crate::tcu8t::__BUFFA_TABLE_Utf8Maps, + // Its values are a message from another crate, reached through its + // `Message` impl. + crate::tcu8t::__BUFFA_TABLE_WrapperValues, + ); +} + +// --------------------------------------------------------------------------- +// Duplicate keys, footprint and registration order +// --------------------------------------------------------------------------- + +#[test] +fn a_repeated_key_replaces_a_message_value_instead_of_merging_into_it() { + let key = length_delimited_field(1, b"k"); + let item = |contents: &[u8]| length_delimited_field(2, contents); + let first = [&varint_field(1, 1)[..], &length_delimited_field(2, b"a")].concat(); + let wire = [ + entry(17, &[&key, &item(&first)]), + entry(17, &[&key, &item(&varint_field(3, 9))]), + ] + .concat(); + assert_same_decode::(&wire, true); + assert_same_chained::(&wire); + let table = crate::tcbt::Values::decode_from_slice(&wire).unwrap(); + let unrolled = crate::tcbu::Values::decode_from_slice(&wire).unwrap(); + assert_eq!( + format!("{:?}", table.item["k"]), + format!("{:?}", unrolled.item["k"]) + ); + let value = &table.item["k"]; + assert_eq!((value.id, value.label.as_str()), (0, "")); + assert_eq!(value.tags, [9]); +} + +/// Entries of field `number` with keys `k0`, `k1`, … and no value. +fn keyed_entries(number: u32, count: usize) -> Vec { + (0..count) + .flat_map(|i| { + entry( + number, + &[&length_delimited_field(1, format!("k{i}").as_bytes())], + ) + }) + .collect() +} + +#[test] +fn an_entry_counts_the_size_of_its_key_and_value_against_the_element_memory_limit() { + use core::mem::size_of; + let string = size_of::(); + // The fields of `Values` with a `string` key, and the size of the value. + let fields = [ + (1, size_of::()), + (4, size_of::()), + (7, size_of::()), + (13, size_of::()), + (14, string), + (15, size_of::>()), + ]; + for (number, value) in fields { + let wire = keyed_entries(number, 5); + let limit = 5 * (string + value); + for (limit, accepted) in [(limit, true), (limit - 1, false)] { + let options = DecodeOptions::new().with_element_memory_limit(limit); + let (table, unrolled) = ( + outcome_with::(&options, &wire), + outcome_with::(&options, &wire), + ); + assert_eq!(table, unrolled, "field {number}, limit {limit}"); + assert_eq!(table.is_ok(), accepted, "field {number}, limit {limit}"); + if !accepted { + assert_eq!(table, Err(DecodeError::ElementMemoryLimitExceeded)); + } + } + } +} + +#[test] +fn an_entry_is_counted_before_it_is_decoded() { + // Under a limit too small for one entry, an entry that is also malformed + // is rejected for the limit or for the malformation in the same order in + // both codecs. + for wire in value_entries() { + for limit in [0, 1, 20, 28, 40, 100, 4000] { + let options = DecodeOptions::new().with_element_memory_limit(limit); + assert_eq!( + outcome_with::(&options, &wire), + outcome_with::(&options, &wire), + "limit {limit}, input {wire:02x?}" + ); + } + } +} + +// --------------------------------------------------------------------------- +// Collection types and string and bytes representations +// --------------------------------------------------------------------------- + +#[test] +fn map_type_in_can_mix_collections_within_one_message() { + use buffa::__private::HashMap; + use std::collections::BTreeMap; + macro_rules! sample { + ($m:ident) => { + crate::$m::MixedMaps { + // One entry in each `HashMap`, so that the bytes do not depend + // on iteration order. + hashed: [("h".to_string(), 1)].into_iter().collect(), + ordered: [ + ("c".to_string(), 3), + ("a".to_string(), 1), + ("b".to_string(), 2), + ] + .into_iter() + .collect(), + more: [(5, "five".to_string())].into_iter().collect(), + ..Default::default() + } + }; + } + let (unrolled, table) = (sample!(tcru), sample!(tcrt)); + let _: &HashMap = &table.hashed; + let _: &BTreeMap = &table.ordered; + let _: &HashMap = &table.more; + let wire = assert_same_codec(&unrolled, &table); + assert_same_chained::(&wire); + assert_same_on_corrupt_input::(&wire, true); +} + +#[test] +fn strings_without_utf8_validation_are_byte_vectors_in_a_table_map() { + use buffa::__private::HashMap; + macro_rules! sample { + ($m:ident) => { + crate::$m::Utf8Maps { + checked: [("a".to_string(), "h\u{e9}llo".to_string())] + .into_iter() + .collect(), + raw: [(vec![0xff, 0xfe], vec![0x80])].into_iter().collect(), + carve: [(vec![1, 2], vec![3])].into_iter().collect(), + ..Default::default() + } + }; + } + let (unrolled, table) = (sample!(tcu8u), sample!(tcu8t)); + let _: &HashMap, Vec> = &table.raw; + let _: &HashMap, Vec> = &table.carve; + let wire = assert_same_codec(&unrolled, &table); + assert_same_chained::(&wire); + assert_same_on_corrupt_input::(&wire, true); + + // Invalid UTF-8 is accepted in `raw` and rejected in `checked`. + let invalid = |number| { + entry( + number, + &[ + &length_delimited_field(1, &[0xff]), + &length_delimited_field(2, &[0xff]), + ], + ) + }; + for number in [2, 3] { + assert!(crate::tcu8t::Utf8Maps::decode_from_slice(&invalid(number)).is_ok()); + } + assert_same_decode::(&invalid(1), false); + assert!(crate::tcu8t::Utf8Maps::decode_from_slice(&invalid(1)).is_err()); +} + +#[test] +fn maps_of_custom_string_or_bytes_types_agree_in_the_unrolled_codec() { + // `Bytes` values are not representable in a table, so these messages fall + // back, and must still round-trip through the same API. + macro_rules! sample { + ($m:ident) => { + ( + crate::$m::BytesValues { + blobs: [( + "b".to_string(), + buffa::bytes::Bytes::from_static(b"\x00\xff"), + )] + .into_iter() + .collect(), + ..Default::default() + }, + crate::$m::RawValues { + raw_values: [(1, buffa::bytes::Bytes::from_static(b"\xff"))] + .into_iter() + .collect(), + ..Default::default() + }, + ) + }; + } + let ((bu, ru), (bt, rt)) = (sample!(tcu8u), sample!(tcu8t)); + assert_same_codec(&bu, &bt); + assert_same_codec(&ru, &rt); +} + +/// A `WrapperValues` decoded from `wire` under `options`, as the sorted entries +/// of its map, which do not depend on the iteration order of a `HashMap`. +macro_rules! wrapper_outcome { + ($m:ident, $options:expr, $wire:expr) => { + $options + .decode_from_slice::($wire) + .map(|m| sorted(&m.counts)) + }; +} + +#[test] +fn a_map_of_messages_from_another_crate_round_trips_and_agrees_on_corrupt_input() { + use buffa_types::google::protobuf::Int32Value; + // One entry, so that the bytes do not depend on the iteration order of the + // `HashMap`. + macro_rules! sample { + ($m:ident) => { + crate::$m::WrapperValues { + counts: [("c".to_string(), Int32Value::from(3))] + .into_iter() + .collect(), + ..Default::default() + } + }; + } + assert_same_codec(&sample!(tcu8u), &sample!(tcu8t)); + + let entries = |count: u64| -> Vec { + (0..count) + .flat_map(|i| { + entry( + 1, + &[ + &length_delimited_field(1, format!("k{i}").as_bytes()), + &length_delimited_field(2, &varint_field(1, i + 1)), + ], + ) + }) + .collect() + }; + let wire = entries(4); + let options = DecodeOptions::new(); + // As in `assert_same_decode`, a rejection may differ when the table + // reports the end of a message value's slice first. + let same = |input: &[u8]| { + let unrolled = wrapper_outcome!(tcu8u, options, input); + let table = wrapper_outcome!(tcu8t, options, input); + assert!( + table == unrolled || (unrolled.is_err() && table == Err(DecodeError::UnexpectedEof)), + "input {input:02x?}: unrolled {unrolled:?}, table {table:?}" + ); + }; + for end in 0..=wire.len() { + same(&wire[..end]); + } + let mut flipped = wire.clone(); + for i in 0..wire.len() { + for xor in [0x01, 0x80, 0xff] { + flipped[i] = wire[i] ^ xor; + same(&flipped); + } + flipped[i] = wire[i]; + } + + // The element-memory limit counts each entry, and the recursion limit the + // message value. + let wire = entries(200); + for limit in [0, 1, 50, 100, 4000, 1_000_000] { + let options = DecodeOptions::new().with_element_memory_limit(limit); + assert_eq!( + wrapper_outcome!(tcu8t, options, &wire), + wrapper_outcome!(tcu8u, options, &wire), + "element memory limit {limit}" + ); + } + for limit in [0, 1, 2, 100] { + let options = DecodeOptions::new().with_recursion_limit(limit); + assert_eq!( + wrapper_outcome!(tcu8t, options, &wire), + wrapper_outcome!(tcu8u, options, &wire), + "recursion limit {limit}" + ); + } + let options = DecodeOptions::new().with_element_memory_limit(100); + assert_eq!( + wrapper_outcome!(tcu8t, options, &wire), + Err(DecodeError::ElementMemoryLimitExceeded) + ); +} diff --git a/buffa/src/table/decode.rs b/buffa/src/table/decode.rs index 1802ba11..629c0c7d 100644 --- a/buffa/src/table/decode.rs +++ b/buffa/src/table/decode.rs @@ -3,6 +3,7 @@ //! Decoding runs over one contiguous `&[u8]`, so every arm reads through a //! non-generic function compiled in this crate. +use super::map::merge_map; use super::scalar::Sc; use super::{ Bool, Double, Entry, EnumVt, Fixed32, Fixed64, Float, Int32, Int64, Kind, MessageTable, @@ -158,7 +159,15 @@ unsafe fn merge_one( return unsafe { merge_unknown(table, base, tag, buf, ctx) }; }; // SAFETY: the offset is within the message, per the table's contract. - unsafe { merge_kind(table, e, base, base.add(e.offset as usize), tag, buf, ctx) } + let slot = unsafe { base.add(e.offset as usize) }; + // SAFETY: `slot` is the field `e` describes. + unsafe { + if e.kind == Kind::Map { + merge_map(table, e, base, slot, tag, buf, ctx) + } else { + merge_kind(table, e, base, slot, tag, buf, ctx) + } + } } /// # Safety @@ -181,14 +190,20 @@ unsafe fn merge_unknown( Ok(()) } +// The arm for a map is unreachable: `merge_one`, which `merge_slice` calls for +// each field, tests for a map before it calls this. `map::merge_entry` also +// calls this function, for a key and a value, which are never maps. The +// reason is in the module documentation of `map.rs`. macro_rules! merge_dispatch { ($($name:ident: $fam:ident $ty:ident $card:ident;)*) => { /// # Safety /// /// `slot` points to the field `e` describes, inside the live message - /// at `base` of the type `table` describes. + /// at `base` of the type `table` describes. `base` may be null if + /// `table` has no unknown fields, which is how a map entry, whose + /// fields are not in a message, calls this. #[inline] - unsafe fn merge_kind( + pub(super) unsafe fn merge_kind( table: &MessageTable, e: &Entry, base: *mut u8, @@ -223,6 +238,9 @@ macro_rules! merge_dispatch { (@arm Oneof $ty:ident $card:ident $table:ident $e:ident $base:ident $slot:ident $tag:ident $buf:ident $ctx:ident) => { merge_oneof($table, $e, $base, $slot, $tag, $buf, $ctx) }; + (@arm Map $ty:ident $card:ident $table:ident $e:ident $base:ident $slot:ident $tag:ident $buf:ident $ctx:ident) => { + unreachable!("a map field is merged by `merge_one`") + }; } kind_table!(merge_dispatch); @@ -500,7 +518,7 @@ unsafe fn merge_scalar( /// Split a length-prefixed payload off the front of `buf`. #[inline] -fn take_len_delimited<'a>(buf: &mut &'a [u8]) -> Result<&'a [u8], DecodeError> { +pub(super) fn take_len_delimited<'a>(buf: &mut &'a [u8]) -> Result<&'a [u8], DecodeError> { let len = decode_varint(buf)?; let len = usize::try_from(len).map_err(|_| DecodeError::MessageTooLarge)?; if buf.len() < len { diff --git a/buffa/src/table/encode.rs b/buffa/src/table/encode.rs index ce91f74c..c3d10b5e 100644 --- a/buffa/src/table/encode.rs +++ b/buffa/src/table/encode.rs @@ -1,6 +1,7 @@ //! The write pass: [`write_to`] and its per-kind arms. use super::bridge::write_field_value; +use super::map::write_map; use super::scalar::Sc; use super::{ Bool, Double, Entry, Fixed32, Fixed64, Float, Int32, Int64, Kind, MessageTable, Sfixed32, @@ -64,7 +65,15 @@ pub(super) unsafe fn write_message( ) { for e in table.entries { // SAFETY: the offset is within the message, per the table's contract. - unsafe { write_kind(table, e, base.add(e.offset as usize), cache, buf) }; + let slot = unsafe { base.add(e.offset as usize) }; + // SAFETY: `slot` is the field `e` describes. + unsafe { + if e.kind == Kind::Map { + write_map(table, e, slot, cache, buf); + } else { + write_kind(table, e, slot, cache, buf); + } + } } if table.unknown != NO_UNKNOWN { // SAFETY: `unknown` is the offset of the message's `UnknownFields`. @@ -73,7 +82,7 @@ pub(super) unsafe fn write_message( } #[inline(always)] -fn put_tag(e: &Entry, buf: &mut K) { +pub(super) fn put_tag(e: &Entry, buf: &mut K) { if e.tag_len == 1 { buf.put_u8(e.tag as u8); } else { @@ -83,6 +92,8 @@ fn put_tag(e: &Entry, buf: &mut K) { /// Defines `$fname`, the write of one field by kind, for the kinds listed, as /// `size_dispatch!` does. +// The arm for a map is unreachable: `write_message` tests for a map before it +// calls this. The reason is in the module documentation of `map.rs`. macro_rules! write_dispatch { ($fname:ident; $($name:ident: $fam:ident $ty:ident $card:ident;)*) => { /// # Safety @@ -90,7 +101,7 @@ macro_rules! write_dispatch { /// `slot` points to the field `e` describes, in a live message of the /// type `table` describes. #[inline] - unsafe fn $fname( + pub(super) unsafe fn $fname( table: &MessageTable, e: &Entry, slot: *const u8, @@ -129,6 +140,9 @@ macro_rules! write_dispatch { (@arm Oneof $ty:ident ONEOF $table:ident $e:ident $slot:ident $cache:ident $buf:ident) => { () }; + (@arm Map $ty:ident $card:ident $table:ident $e:ident $slot:ident $cache:ident $buf:ident) => { + unreachable!("a map field is written by `write_message`") + }; } kind_table!(write_dispatch, write_kind); diff --git a/buffa/src/table/map.rs b/buffa/src/table/map.rs new file mode 100644 index 00000000..e57afcf9 --- /dev/null +++ b/buffa/src/table/map.rs @@ -0,0 +1,466 @@ +//! Map fields: `map` stored as a [`MapStorage`] collection. +//! +//! A map is written as one length-delimited record per entry, each holding +//! the key as field 1 and the value as field 2. The interpreters treat the +//! key and the value as two fields of a message with two entries and reach +//! them through pointers to the collection's storage, so every key and value +//! kind is handled by the arms that handle it as an ordinary field. Only +//! what depends on the collection's type is generic: iterating it, and +//! inserting an entry that has been decoded into locals. +//! +//! # Why the loops test for a map +//! +//! The dispatch functions `size_kind`, `write_kind` and `merge_kind` have an +//! arm for every kind, and the arm for [`Kind::Map`] is `unreachable!`. +//! `compute_size`, `write_message` and `merge_one` (which `merge_slice` calls +//! for each field) test for a map first and call [`size_map`], [`write_map`] +//! or `merge_map` themselves. `merge_kind` is also called by `merge_entry`, +//! for a key and a value, which are never maps. +//! +//! Calling the map interpreters from the arms made the dispatch functions +//! large enough that, at `opt-level = 3`, the compiler stopped inlining them +//! into the loops and called them once per field. With fat LTO, one codegen +//! unit and Rust 1.95, the `whatsapp.proto` of `waproto` was 4% larger at +//! `opt-level = 3`, and a schema of table messages without maps 2.6% larger; +//! at `opt-level = "z"` the two layouts were within 0.1% of each other. This +//! is an inlining threshold, so it depends on the compiler and the size of the +//! dispatch functions, and any change to them or a new toolchain can bring it +//! back. To check, build a schema of table messages without maps at +//! `opt-level = 3` (fat LTO, one codegen unit) and compare the binary's size +//! with a build before a change to the dispatch functions: a jump of a few +//! percent is this. + +use super::decode::{merge_kind, take_len_delimited}; +use super::encode::{put_tag, write_kind}; +use super::shape::{DirectMsgVt, EnumVtOf}; +use super::size::size_kind; +use super::{Aux, Entry, EnumShape, Kind, KindSlot, MessageTable, IMPLICIT, NO_UNKNOWN, REQUIRED}; +use crate::encoding::{ + check_wire_type, encode_varint, skip_field_depth, varint_len, Tag, WireType, +}; +use crate::map_codec::MapStorage; +use crate::{ + types, DecodeContext, DecodeError, EncodeSink, SizeCache, UnknownField, UnknownFieldData, + UnknownFields, +}; + +/// Descriptor of a map field: the kinds of its key and value, and functions +/// that iterate the collection and insert a decoded entry, instantiated for +/// its type. +pub struct MapVt { + /// The key (field 1) and the value (field 2), as the entries of a message + /// whose fields are reached by pointer, so their offsets are unused. + entries: [Entry; 2], + /// The descriptor of the value, if its kind has one, which its entry + /// indexes at 0. + value_aux: Option, + /// The size in bytes of a key and a value together, which an entry read + /// from the wire counts towards the element-memory limit. + footprint: usize, + /// Call the function with a pointer to each key and its value. + /// + /// # Safety + /// + /// The first argument points to a live collection of the type the + /// descriptor was built for, which nothing mutates during the call. + for_each: unsafe fn(*const u8, &mut dyn FnMut(*const u8, *const u8)), + /// Create a key and a value with their defaults, pass pointers to them to + /// the function, and insert them into the collection if it returns + /// `true`. Returns what the function returned. + /// + /// # Safety + /// + /// The first argument points to a live collection of the type the + /// descriptor was built for, to which the caller has exclusive access for + /// the whole call, as a `&mut` would give it: nothing else reads or + /// writes the collection while it runs, including through the function. + decode_entry: unsafe fn(*mut u8, &mut EntryReader<'_>) -> Result, +} + +/// Reads the key and value of one entry through pointers to them and returns +/// whether to insert the entry. +type EntryReader<'a> = dyn FnMut(*mut u8, *mut u8) -> Result + 'a; + +/// # Safety +/// +/// `map` points to a live `Mp`, which nothing mutates during the call. +unsafe fn for_each_impl(map: *const u8, f: &mut dyn FnMut(*const u8, *const u8)) { + // SAFETY: the caller passes a pointer to a live `Mp`. + let map = unsafe { &*map.cast::() }; + for (key, value) in map.storage_iter() { + f( + (key as *const Mp::Key).cast(), + (value as *const Mp::Value).cast(), + ); + } +} + +/// # Safety +/// +/// `map` points to a live `Mp` to which the caller has exclusive access for +/// the call. +unsafe fn decode_entry_impl( + map: *mut u8, + f: &mut EntryReader<'_>, +) -> Result +where + Mp::Key: Default, + Mp::Value: Default, +{ + let mut key = Mp::Key::default(); + let mut value = Mp::Value::default(); + let keep = f( + (&mut key as *mut Mp::Key).cast(), + (&mut value as *mut Mp::Value).cast(), + )?; + if keep { + // SAFETY: the caller passes a pointer to a live `Mp` that it has + // exclusive access to. + unsafe { (*map.cast::()).storage_insert(key, value) }; + } + Ok(keep) +} + +impl MapVt { + /// Describe a map of type `Mp` whose keys are of the kind `KK` names and + /// whose values are of the string, bytes or scalar kind `VK` names. + /// + /// # Panics + /// + /// Panics, at compile time when used to initialise a `static`, if `KK` or + /// `VK` does not have required cardinality (`Int32Required`, not + /// `Int32Optional` or `Int32Repeated`). The key is not restricted to the + /// types protobuf allows for one (integers, `bool` and `string`): any + /// scalar, string or bytes kind with required cardinality is accepted, and + /// the collection type decides whether it can be keyed by that type. + #[must_use] + pub const fn new() -> Self + where + KK: KindSlot, + VK: KindSlot, + Mp: MapStorage, + KK::Slot: Default, + VK::Slot: Default, + { + Self::build::(KK::KIND, VK::KIND, None) + } + + /// Describe a map of type `Mp` whose keys are of the kind `KK` names and + /// whose values are enums stored as `S`, which must have implicit + /// presence: [`ImplicitOpen`](super::ImplicitOpen) or + /// [`ImplicitClosed`](super::ImplicitClosed). + /// + /// A value that a closed enum does not know makes the whole entry + /// unknown, as in unrolled code. + /// + /// # Panics + /// + /// As for [`new`](Self::new), and if `S` is not an implicit shape. + #[must_use] + pub const fn with_enum() -> Self + where + KK: KindSlot, + S: EnumShape, + Mp: MapStorage, + KK::Slot: Default, + S::Slot: Default, + { + assert!( + S::CARD == IMPLICIT, + "buffa table: a map's enum values must have an implicit shape" + ); + Self::build::( + KK::KIND, + Kind::EnumRequired, + Some(Aux::Enum(&EnumVtOf::::VT)), + ) + } + + /// Describe a map of type `Mp` whose keys are of the kind `KK` names and + /// whose values are messages of type `M`, which `vt` describes: a + /// [`DirectMsgVt::new`] for a message that has a table, or + /// [`DirectMsgVt::via_message`] for one that has not, which is reached + /// through its [`Message`](crate::Message) impl. Its type ties `vt` to the + /// map's value type, so a descriptor of another message is a type error: + /// + /// ``` + /// use buffa::table::{kinds, DirectMsgVt, MapVt, Table}; + /// use std::collections::BTreeMap; + /// + /// #[derive(Default)] + /// struct Point { + /// x: i32, + /// } + /// static POINT: Table = buffa::__table!( + /// Point, + /// abi = buffa::table::ABI, + /// entries = [buffa::__table_entry!(Point, x, Int32Implicit, 1)], + /// dense = &[0, 1], + /// aux = [], + /// unknown = none, + /// ); + /// static VT: DirectMsgVt = DirectMsgVt::new(&POINT); + /// let _ = MapVt::with_msg::, kinds::StrRequired, Point>(&VT); + /// ``` + /// + /// ```compile_fail,E0308 + /// use buffa::table::{kinds, DirectMsgVt, MapVt, Table}; + /// use std::collections::BTreeMap; + /// + /// #[derive(Default)] + /// struct Point { + /// x: i32, + /// } + /// #[derive(Default)] + /// struct Other; + /// static POINT: Table = buffa::__table!( + /// Point, + /// abi = buffa::table::ABI, + /// entries = [buffa::__table_entry!(Point, x, Int32Implicit, 1)], + /// dense = &[0, 1], + /// aux = [], + /// unknown = none, + /// ); + /// static VT: DirectMsgVt = DirectMsgVt::new(&POINT); + /// // The values are `Other`s, but the descriptor is a `Point`'s. + /// let _ = MapVt::with_msg::, kinds::StrRequired, Other>(&VT); + /// ``` + /// + /// # Panics + /// + /// As for [`new`](Self::new). + #[must_use] + pub const fn with_msg(vt: &'static DirectMsgVt) -> Self + where + KK: KindSlot, + Mp: MapStorage, + KK::Slot: Default, + M: Default, + { + Self::build::(KK::KIND, Kind::MsgSingular, Some(Aux::Msg(&vt.vt))) + } + + const fn build(key: Kind, value: Kind, value_aux: Option) -> Self + where + Mp::Key: Default, + Mp::Value: Default, + { + assert!( + key.card() == REQUIRED, + "buffa table: a map key must be of a required scalar or string kind" + ); + assert!( + value as u8 == Kind::MsgSingular as u8 || value.card() == REQUIRED, + "buffa table: a map value must be of a required kind or a singular message" + ); + Self { + entries: [Entry::new(key, 1, 0, 0), Entry::new(value, 2, 0, 0)], + value_aux, + footprint: core::mem::size_of::() + core::mem::size_of::(), + for_each: for_each_impl::, + decode_entry: decode_entry_impl::, + } + } + + /// The key and value as a two-field message, for the interpreters. + #[inline] + fn entry_table(&'static self) -> MessageTable { + MessageTable { + entries: &self.entries, + dense: &[], + aux: match &self.value_aux { + Some(aux) => core::slice::from_ref(aux), + None => &[], + }, + unknown: NO_UNKNOWN, + } + } +} + +/// The encoded size of the map at `slot`, whose field is `e`, recording the +/// sizes of message values in `cache`. +/// +/// # Safety +/// +/// `slot` points to the map field `e` describes. +#[inline(never)] +pub(super) unsafe fn size_map( + table: &MessageTable, + e: &Entry, + slot: *const u8, + cache: &mut SizeCache, +) -> u64 { + let tl = u64::from(e.tag_len); + let vt = table.map_vt(e); + let entry_table = vt.entry_table(); + let mut size = 0u64; + // SAFETY: the descriptor was built for the slot's type, and the entry + // table describes what it passes to the closure. + unsafe { + (vt.for_each)(slot, &mut |key, value| { + let entry = size_kind(&entry_table, &vt.entries[0], key, cache) + + size_kind(&entry_table, &vt.entries[1], value, cache); + size += tl + varint_len(entry) as u64 + entry; + }); + } + size +} + +/// Write the map at `slot`, whose field is `e`, taking the sizes of message +/// values from `cache` in the order `size_map` recorded them. +/// +/// # Safety +/// +/// As for [`size_map`]. +#[inline(never)] +pub(super) unsafe fn write_map( + table: &MessageTable, + e: &Entry, + slot: *const u8, + cache: &mut SizeCache, + buf: &mut K, +) { + let vt = table.map_vt(e); + let entry_table = vt.entry_table(); + let [key_entry, value_entry] = &vt.entries; + // SAFETY: as for `size_map`. + unsafe { + (vt.for_each)(slot, &mut |key, value| { + let key_len = size_kind(&entry_table, key_entry, key, cache); + if value_entry.kind == Kind::MsgSingular { + // The size pass recorded the value's size, and the value's own + // write takes the sizes of what it holds after it. + let len = cache.consume_next(); + let inner = u64::from(len); + let entry = + key_len + u64::from(value_entry.tag_len) + varint_len(inner) as u64 + inner; + put_tag(e, buf); + encode_varint(entry, buf); + write_kind(&entry_table, key_entry, key, cache, buf); + put_tag(value_entry, buf); + encode_varint(inner, buf); + let msg = entry_table.msg_vt(value_entry); + msg.child.write_to((msg.get)(value), len, cache, buf); + } else { + let entry = key_len + size_kind(&entry_table, value_entry, value, cache); + put_tag(e, buf); + encode_varint(entry, buf); + write_kind(&entry_table, key_entry, key, cache, buf); + write_kind(&entry_table, value_entry, value, cache, buf); + } + }); + } +} + +/// Decode one entry of the map at `slot`, whose field is `e`, from the front +/// of `buf`, after its `tag`. +/// +/// An entry whose value is a closed enum's unknown number is dropped, or kept +/// whole as an unknown field of the message at `base` if it keeps any. +/// +/// # Safety +/// +/// `slot` points to the map field `e` describes, inside the live message at +/// `base` of the type `table` describes. +#[inline(never)] +pub(super) unsafe fn merge_map( + table: &MessageTable, + e: &Entry, + base: *mut u8, + slot: *mut u8, + tag: Tag, + buf: &mut &[u8], + ctx: DecodeContext<'_>, +) -> Result<(), DecodeError> { + check_wire_type(tag, WireType::LengthDelimited)?; + let vt = table.map_vt(e); + let payload = take_len_delimited(buf)?; + // An entry amplifies as a repeated element does: an omitted message value + // still materialises a whole value in the map, and a few bytes of key buy + // a slot. Count both before decoding either. + ctx.register_element_memory(vt.footprint)?; + let entry_table = vt.entry_table(); + // SAFETY: the descriptor was built for the slot's type, and the entry + // table describes what it passes to the closure. + let inserted = unsafe { + (vt.decode_entry)(slot, &mut |key, value| { + merge_entry(&entry_table, key, value, payload, ctx) + })? + }; + if !inserted && table.unknown != NO_UNKNOWN { + ctx.register_unknown_field()?; + // SAFETY: `unknown` is the offset of the message's `UnknownFields`. + unsafe { + (*base.add(table.unknown as usize).cast::()).push(UnknownField { + number: e.tag >> 3, + data: UnknownFieldData::LengthDelimited(payload.to_vec()), + }); + } + } + Ok(()) +} + +/// Decode the fields of one entry, `payload`, into the key at `key` and the +/// value at `value`. Returns `false` if the last occurrence of the value is +/// an enum number that a closed enum does not know. +/// +/// # Safety +/// +/// `key` and `value` point to live values of the types the two entries of +/// `table`, the entry table of a map's descriptor, describe. +unsafe fn merge_entry( + table: &MessageTable, + key: *mut u8, + value: *mut u8, + mut payload: &[u8], + ctx: DecodeContext<'_>, +) -> Result { + let [key_entry, value_entry] = table.entries else { + unreachable!("an entry table has a key and a value") + }; + let mut known = true; + while !payload.is_empty() { + let tag = Tag::decode(&mut payload)?; + match tag.field_number() { + 1 => { + // SAFETY: `key` points to the value the key entry describes, + // and the entry table has no unknown fields, so `merge_kind` + // does not read the base pointer. + unsafe { + merge_kind( + table, + key_entry, + core::ptr::null_mut(), + key, + tag, + &mut payload, + ctx, + ) + }?; + } + 2 if value_entry.kind == Kind::EnumRequired => { + check_wire_type(tag, WireType::Varint)?; + let raw = types::decode_int32(&mut payload)?; + // SAFETY: `value` points to the enum the value entry's + // descriptor was built for. + known = unsafe { (table.enum_vt(value_entry).set)(value, raw) }; + } + 2 => { + // SAFETY: as for the key. + unsafe { + merge_kind( + table, + value_entry, + core::ptr::null_mut(), + value, + tag, + &mut payload, + ctx, + ) + }?; + } + _ => skip_field_depth(tag, &mut payload, ctx.depth())?, + } + } + Ok(known) +} diff --git a/buffa/src/table/map_tests.rs b/buffa/src/table/map_tests.rs new file mode 100644 index 00000000..8e718ac7 --- /dev/null +++ b/buffa/src/table/map_tests.rs @@ -0,0 +1,1023 @@ +// Tests of the map arms over hand-written table messages, against a reference +// message that reads and writes the same fields through `map_codec`, as +// generated unrolled code does. + +use super::*; +use crate::alloc::{collections::BTreeMap, string::String, vec, vec::Vec}; +use crate::bytes::Buf; +use crate::encoding::{ + check_wire_type, decode_unknown_field, encode_varint, skip_field_depth, Tag, WireType, +}; +use crate::map_codec::{ + self, BytesVec, ClosedEnum, Double, Fixed32, Fixed64, Int32, Int64, OpenEnum, Sint32, Sint64, + Str, Uint32, Uint64, +}; +use crate::{ + DecodeOptions, EnumValue, Enumeration, Message, MessageField, Rope, UnknownFieldData, UnknownFields, +}; + +type Hash = crate::__private::HashMap; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +enum Color { + #[default] + Red = 0, + Green = 1, + Blue = 2, +} + +impl Enumeration for Color { + fn from_i32(value: i32) -> Option { + match value { + 0 => Some(Self::Red), + 1 => Some(Self::Green), + 2 => Some(Self::Blue), + _ => None, + } + } + + fn to_i32(&self) -> i32 { + *self as i32 + } + + fn proto_name(&self) -> &'static str { + match self { + Self::Red => "RED", + Self::Green => "GREEN", + Self::Blue => "BLUE", + } + } +} + +/// `int32 id = 1; string tag = 2; Item child = 3;`, keeping unknown fields. +#[derive(Clone, Debug, Default, PartialEq)] +struct Item { + id: i32, + tag: String, + child: MessageField, + unknown: UnknownFields, +} + +/// One map of each shape, in field number order. +#[derive(Clone, Debug, Default, PartialEq)] +struct Maps { + by_name: Hash, + items: BTreeMap, + closed: BTreeMap, + open: BTreeMap>, + blobs: BTreeMap>, + names: BTreeMap, + floats: BTreeMap, + zigzag: BTreeMap, + fixed: BTreeMap, + high: BTreeMap, + unknown: UnknownFields, +} + +/// The closed-enum map of `Maps`, in a message that drops unknown fields. +#[derive(Clone, Debug, Default, PartialEq)] +struct Lossy { + closed: BTreeMap, +} + +/// A map whose values are the message itself. +#[derive(Clone, Debug, Default, PartialEq)] +struct Tree { + children: BTreeMap, +} + +static ITEM: Table = crate::__table!( + Item, + abi = ABI, + entries = [ + crate::__table_entry!(Item, id, Int32Implicit, 1), + crate::__table_entry!(Item, tag, StrImplicit, 2), + crate::__table_entry!( + Item, + child, + MsgSingular, + 3, + aux = 0, + slot = MessageField + ), + ], + dense = &[0, 1, 2, 3], + aux = [Aux::Msg(&MsgVt::new::>(&ITEM))], + unknown = unknown, +); + +static MAPS: Table = crate::__table!( + Maps, + abi = ABI, + entries = [ + crate::__table_entry!(Maps, by_name, Map, 1, aux = 0, slot = Hash), + crate::__table_entry!(Maps, items, Map, 2, aux = 1, slot = BTreeMap), + crate::__table_entry!(Maps, closed, Map, 3, aux = 2, slot = BTreeMap), + crate::__table_entry!( + Maps, + open, + Map, + 4, + aux = 3, + slot = BTreeMap> + ), + crate::__table_entry!(Maps, blobs, Map, 5, aux = 4, slot = BTreeMap>), + crate::__table_entry!(Maps, names, Map, 6, aux = 5, slot = BTreeMap), + crate::__table_entry!(Maps, floats, Map, 7, aux = 6, slot = BTreeMap), + crate::__table_entry!(Maps, zigzag, Map, 8, aux = 7, slot = BTreeMap), + crate::__table_entry!(Maps, fixed, Map, 9, aux = 8, slot = BTreeMap), + crate::__table_entry!(Maps, high, Map, 300, aux = 9, slot = BTreeMap), + ], + dense = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + aux = [ + Aux::Map(&MapVt::new::, kinds::StrRequired, kinds::Int32Required>()), + Aux::Map(&MapVt::with_msg::, kinds::Int32Required, Item>( + &DirectMsgVt::new(&ITEM) + )), + Aux::Map(&MapVt::with_enum::, kinds::Uint64Required, ImplicitClosed>()), + Aux::Map(&MapVt::with_enum::< + BTreeMap>, + kinds::BoolRequired, + ImplicitOpen, + >()), + Aux::Map(&MapVt::new::>, kinds::StrRequired, kinds::BytesRequired>()), + Aux::Map(&MapVt::new::, kinds::Int64Required, kinds::StrRequired>()), + Aux::Map(&MapVt::new::, kinds::Uint32Required, kinds::DoubleRequired>()), + Aux::Map(&MapVt::new::, kinds::Sint32Required, kinds::Sint64Required>()), + Aux::Map(&MapVt::new::, kinds::Fixed64Required, kinds::Fixed32Required>()), + Aux::Map(&MapVt::new::, kinds::StrRequired, kinds::StrRequired>()), + ], + unknown = unknown, +); + +static LOSSY: Table = crate::__table!( + Lossy, + abi = ABI, + entries = [crate::__table_entry!(Lossy, closed, Map, 3, aux = 0, slot = BTreeMap)], + dense = &[0, 0, 0, 1], + aux = [Aux::Map( + &MapVt::with_enum::, kinds::Uint64Required, ImplicitClosed>() + )], + unknown = none, +); + +static TREE: Table = crate::__table!( + Tree, + abi = ABI, + entries = [crate::__table_entry!(Tree, children, Map, 1, aux = 0, slot = BTreeMap)], + dense = &[0, 1], + aux = [Aux::Map(&MapVt::with_msg::, kinds::StrRequired, Tree>( + &DirectMsgVt::new(&TREE) + ))], + unknown = none, +); + +macro_rules! table_message { + ($ty:ty, $table:ident) => { + crate::impl_default_instance!($ty); + + impl Message for $ty { + fn compute_size(&self, cache: &mut SizeCache) -> u32 { + $table.compute_size(self, cache) + } + + fn write_to(&self, cache: &mut SizeCache, buf: &mut impl EncodeSink) { + $table.write_to(self, cache, buf); + } + + fn merge_field( + &mut self, + tag: Tag, + buf: &mut impl Buf, + ctx: DecodeContext<'_>, + ) -> Result<(), DecodeError> { + $table.merge_field(self, tag, buf, ctx) + } + + fn merge_to_limit( + &mut self, + buf: &mut impl Buf, + ctx: DecodeContext<'_>, + limit: usize, + ) -> Result<(), DecodeError> { + $table.merge_to_limit(self, buf, ctx, limit) + } + + fn merge_length_delimited( + &mut self, + buf: &mut impl Buf, + ctx: DecodeContext<'_>, + ) -> Result<(), DecodeError> { + $table.merge_length_delimited(self, buf, ctx) + } + + fn clear(&mut self) { + *self = Self::default(); + } + } + }; +} + +table_message!(Item, ITEM); +table_message!(Maps, MAPS); +table_message!(Lossy, LOSSY); +table_message!(Tree, TREE); + +/// `Maps` written and read as unrolled generated code does: through the +/// generic helpers of `map_codec`, one call per field. +#[derive(Clone, Debug, Default, PartialEq)] +struct Reference(Maps); +crate::impl_default_instance!(Reference); + +impl Message for Reference { + fn compute_size(&self, cache: &mut SizeCache) -> u32 { + let m = &self.0; + let mut size = 0u64; + size += map_codec::field_len::(&m.by_name, 1); + size += map_codec::message_field_len::(&m.items, 1, cache); + size += map_codec::field_len::, _>(&m.closed, 1); + size += map_codec::field_len::, _>(&m.open, 1); + size += map_codec::field_len::(&m.blobs, 1); + size += map_codec::field_len::(&m.names, 1); + size += map_codec::field_len::(&m.floats, 1); + size += map_codec::field_len::(&m.zigzag, 1); + size += map_codec::field_len::(&m.fixed, 1); + size += map_codec::field_len::(&m.high, 2); + size += m.unknown.encoded_len() as u64; + crate::saturate_size(size) + } + + fn write_to(&self, cache: &mut SizeCache, buf: &mut impl EncodeSink) { + let m = &self.0; + map_codec::write_field::(&m.by_name, 1, buf); + map_codec::write_message_field::(&m.items, 2, cache, buf); + map_codec::write_field::, _>(&m.closed, 3, buf); + map_codec::write_field::, _>(&m.open, 4, buf); + map_codec::write_field::(&m.blobs, 5, buf); + map_codec::write_field::(&m.names, 6, buf); + map_codec::write_field::(&m.floats, 7, buf); + map_codec::write_field::(&m.zigzag, 8, buf); + map_codec::write_field::(&m.fixed, 9, buf); + map_codec::write_field::(&m.high, 300, buf); + m.unknown.write_to(buf); + } + + fn merge_field( + &mut self, + tag: Tag, + buf: &mut impl Buf, + ctx: DecodeContext<'_>, + ) -> Result<(), DecodeError> { + let m = &mut self.0; + let number = tag.field_number(); + if (1..=9).contains(&number) || number == 300 { + check_wire_type(tag, WireType::LengthDelimited)?; + } + match number { + 1 => map_codec::merge_entry::(&mut m.by_name, buf, ctx), + 2 => map_codec::merge_entry::, _>(&mut m.items, buf, ctx), + 3 => map_codec::merge_entry_with_unknowns::, _>( + &mut m.closed, + buf, + ctx, + Some((3, &mut m.unknown)), + ), + 4 => map_codec::merge_entry::, _>( + &mut m.open, + buf, + ctx, + ), + 5 => map_codec::merge_entry::(&mut m.blobs, buf, ctx), + 6 => map_codec::merge_entry::(&mut m.names, buf, ctx), + 7 => map_codec::merge_entry::(&mut m.floats, buf, ctx), + 8 => map_codec::merge_entry::(&mut m.zigzag, buf, ctx), + 9 => map_codec::merge_entry::(&mut m.fixed, buf, ctx), + 300 => map_codec::merge_entry::(&mut m.high, buf, ctx), + _ => { + let field = decode_unknown_field(tag, buf, ctx)?; + m.unknown.push(field); + Ok(()) + } + } + } + + fn clear(&mut self) { + self.0 = Maps::default(); + } +} + +/// `Lossy` the same way, which drops an entry with an unknown enum value. +#[derive(Clone, Debug, Default, PartialEq)] +struct ReferenceLossy(Lossy); +crate::impl_default_instance!(ReferenceLossy); + +impl Message for ReferenceLossy { + fn compute_size(&self, _: &mut SizeCache) -> u32 { + crate::saturate_size(map_codec::field_len::, _>( + &self.0.closed, + 1, + )) + } + + fn write_to(&self, _: &mut SizeCache, buf: &mut impl EncodeSink) { + map_codec::write_field::, _>(&self.0.closed, 3, buf); + } + + fn merge_field( + &mut self, + tag: Tag, + buf: &mut impl Buf, + ctx: DecodeContext<'_>, + ) -> Result<(), DecodeError> { + if tag.field_number() == 3 { + check_wire_type(tag, WireType::LengthDelimited)?; + map_codec::merge_entry::, _>(&mut self.0.closed, buf, ctx) + } else { + skip_field_depth(tag, buf, ctx.depth()) + } + } + + fn clear(&mut self) { + self.0 = Lossy::default(); + } +} + +fn item(id: i32, tag: &str) -> Item { + Item { + id, + tag: tag.into(), + ..Item::default() + } +} + +fn populated() -> Maps { + Maps { + by_name: [("a".to_string(), 1), (String::new(), 0), ("ccc".into(), -3)] + .into_iter() + .collect(), + items: BTreeMap::from([ + ( + -1, + Item { + child: MessageField::some(item(9, "deep")), + ..item(1, "one") + }, + ), + (0, Item::default()), + (70_000, item(2, "")), + ]), + closed: BTreeMap::from([(0, Color::Red), (u64::MAX, Color::Blue)]), + open: BTreeMap::from([(false, EnumValue::from(9)), (true, EnumValue::from(0))]), + blobs: BTreeMap::from([("k".to_string(), vec![1, 2, 3]), (String::new(), Vec::new())]), + names: BTreeMap::from([(i64::MIN, "min".to_string()), (0, String::new())]), + floats: BTreeMap::from([(0, 0.0), (7, -1.5), (u32::MAX, f64::MAX)]), + zigzag: BTreeMap::from([(i32::MIN, i64::MIN), (-1, 1), (0, 0)]), + fixed: BTreeMap::from([(0, 0), (u64::MAX, u32::MAX)]), + high: BTreeMap::from([("h".to_string(), "i".to_string())]), + unknown: UnknownFields::new(), + } +} + +// --------------------------------------------------------------------------- +// Wire helpers +// --------------------------------------------------------------------------- + +fn varint(v: u64) -> Vec { + let mut out = Vec::new(); + encode_varint(v, &mut out); + out +} + +/// A length-delimited field `number`. +fn ld(number: u32, payload: &[u8]) -> Vec { + let mut out = varint(u64::from(number) << 3 | 2); + out.extend(varint(payload.len() as u64)); + out.extend_from_slice(payload); + out +} + +/// A varint field `number`. +fn vf(number: u32, v: u64) -> Vec { + let mut out = varint(u64::from(number) << 3); + out.extend(varint(v)); + out +} + +fn cat(parts: &[&[u8]]) -> Vec { + parts.concat() +} + +// --------------------------------------------------------------------------- +// Comparison with the reference +// --------------------------------------------------------------------------- + +/// Decode `wire` with the table and with the reference, and require the same +/// value or, from two rejections, the same error, except that the table may +/// report `UnexpectedEof` where the reference read on past the end of an +/// entry and found another error. +#[track_caller] +fn assert_same(wire: &[u8]) -> Result { + let table = Maps::decode_from_slice(wire); + let reference = Reference::decode_from_slice(wire).map(|r| r.0); + match (&table, &reference) { + (Ok(t), Ok(r)) => assert_eq!(t, r, "values differ on {wire:02x?}"), + (Err(t), Err(r)) => assert!( + t == r || *t == DecodeError::UnexpectedEof, + "errors differ on {wire:02x?}: table {t:?}, reference {r:?}" + ), + _ => panic!("outcomes differ on {wire:02x?}: table {table:?}, reference {reference:?}"), + } + table +} + +#[test] +fn encodes_the_same_bytes_as_the_reference() { + let msg = populated(); + let reference = Reference(msg.clone()); + let wire = reference.encode_to_vec(); + assert_eq!(msg.encode_to_vec(), wire); + assert_eq!(msg.encoded_len(), reference.encoded_len()); + assert_eq!(msg.encoded_len() as usize, wire.len()); + assert_eq!(Maps::decode_from_slice(&wire).unwrap(), msg); + + let mut framed = Vec::new(); + msg.encode_length_delimited(&mut framed); + let mut reference_framed = Vec::new(); + reference.encode_length_delimited(&mut reference_framed); + assert_eq!(framed, reference_framed); +} + +#[test] +fn an_empty_map_encodes_to_nothing() { + assert_eq!(Maps::default().encode_to_vec(), Vec::::new()); + assert_eq!(Maps::default().encoded_len(), 0); + let empty = Maps::decode_from_slice(&[]).unwrap(); + assert_eq!(empty, Maps::default()); +} + +#[test] +fn every_sink_receives_the_same_bytes() { + let msg = populated(); + let expected = msg.encode_to_vec(); + let mut rope = Rope::new(); + msg.encode(&mut rope); + assert_eq!(&rope.to_contiguous_bytes()[..], &expected[..]); + assert_eq!(&msg.encode_to_bytes()[..], &expected[..]); + let mut vec = vec![0xee]; + msg.encode(&mut vec); + assert_eq!(&vec[1..], &expected[..]); +} + +#[test] +fn known_wire_bytes() { + let mut msg = Maps::default(); + msg.names.insert(-1, "x".into()); + msg.zigzag.insert(-1, 1); + // names (6): an entry of key = -1 (ten bytes) and value = "x"; zigzag (8): + // key -1 is 1, value 1 is 2. + let mut names = vec![0x08]; + names.extend([0xff; 9]); + names.push(0x01); + names.extend([0x12, 0x01, b'x']); + let expected = cat(&[&ld(6, &names), &ld(8, &[0x08, 0x01, 0x10, 0x02])]); + assert_eq!(msg.encode_to_vec(), expected); + assert_eq!(Maps::decode_from_slice(&expected).unwrap(), msg); +} + +#[test] +fn a_high_field_number_takes_a_two_byte_tag() { + let mut msg = Maps::default(); + msg.high.insert("k".into(), "v".into()); + // 300 << 3 | 2 = 2402 = 0xe2 0x12. + assert_eq!( + msg.encode_to_vec(), + [0xe2, 0x12, 0x06, 0x0a, 0x01, b'k', 0x12, 0x01, b'v'] + ); + assert_eq!(Maps::decode_from_slice(&msg.encode_to_vec()).unwrap(), msg); +} + +#[test] +fn a_map_of_messages_records_and_consumes_sizes_in_order() { + // Nested messages inside map values reserve slots in the same cache. + let mut msg = Maps::default(); + for i in 0..5 { + msg.items.insert( + i, + Item { + child: MessageField::some(Item { + child: MessageField::some(item(i, "leaf")), + ..item(i * 10, "mid") + }), + ..item(i * 100, "top") + }, + ); + } + msg.high.insert("after".into(), "the maps".into()); + let reference = Reference(msg.clone()).encode_to_vec(); + assert_eq!(msg.encode_to_vec(), reference); + assert_eq!(Maps::decode_from_slice(&reference).unwrap(), msg); +} + +#[test] +fn a_map_of_the_message_itself_round_trips() { + let mut tree = Tree::default(); + let mut child = Tree::default(); + child.children.insert("grandchild".into(), Tree::default()); + tree.children.insert("child".into(), child); + tree.children.insert("empty".into(), Tree::default()); + let wire = tree.encode_to_vec(); + assert_eq!(Tree::decode_from_slice(&wire).unwrap(), tree); +} + +// --------------------------------------------------------------------------- +// Entry semantics +// --------------------------------------------------------------------------- + +#[test] +fn a_missing_key_or_value_takes_its_default() { + // An empty entry of each map: the default key with the default value. + for number in [1, 2, 3, 4, 5, 6, 7, 8, 9, 300] { + let decoded = assert_same(&ld(number, &[])).unwrap(); + assert!(!decoded.encode_to_vec().is_empty(), "field {number}"); + } + // Key only, and value only. + let decoded = assert_same(&ld(1, &[0x0a, 0x01, b'k'])).unwrap(); + assert_eq!(decoded.by_name, Hash::from_iter([("k".to_string(), 0)])); + let decoded = assert_same(&ld(1, &[0x10, 0x07])).unwrap(); + assert_eq!(decoded.by_name, Hash::from_iter([(String::new(), 7)])); +} + +#[test] +fn the_last_occurrence_of_a_key_or_value_in_an_entry_wins() { + let entry = cat(&[ + &ld(1, b"first"), + &vf(2, 1), + &ld(1, b"second"), + &vf(2, 2), + ]); + let decoded = assert_same(&ld(1, &entry)).unwrap(); + assert_eq!(decoded.by_name, Hash::from_iter([("second".to_string(), 2)])); +} + +#[test] +fn the_value_may_precede_the_key() { + let entry = cat(&[&vf(2, 5), &ld(1, b"k")]); + let decoded = assert_same(&ld(1, &entry)).unwrap(); + assert_eq!(decoded.by_name, Hash::from_iter([("k".to_string(), 5)])); +} + +#[test] +fn unknown_fields_in_an_entry_are_skipped() { + let entry = cat(&[&vf(1000, 3), &ld(1, b"k"), &ld(3, b"ignored"), &vf(2, 8)]); + let decoded = assert_same(&ld(1, &entry)).unwrap(); + assert_eq!(decoded.by_name, Hash::from_iter([("k".to_string(), 8)])); + assert!(decoded.unknown.is_empty()); +} + +#[test] +fn the_same_key_in_two_entries_keeps_the_later_value() { + let wire = cat(&[ + &ld(1, &cat(&[&ld(1, b"k"), &vf(2, 1)])), + &ld(1, &cat(&[&ld(1, b"k"), &vf(2, 2)])), + ]); + let decoded = assert_same(&wire).unwrap(); + assert_eq!(decoded.by_name, Hash::from_iter([("k".to_string(), 2)])); +} + +#[test] +fn a_message_value_repeated_in_an_entry_merges() { + let entry = cat(&[ + &vf(1, 4), + &ld(2, &vf(1, 10)), + &ld(2, &ld(2, b"tag")), + ]); + let decoded = assert_same(&ld(2, &entry)).unwrap(); + assert_eq!(decoded.items[&4], item(10, "tag")); +} + +#[test] +fn a_message_value_with_nested_messages_decodes() { + let value = cat(&[&vf(1, 3), &ld(3, &vf(1, 4))]); + let decoded = assert_same(&ld(2, &cat(&[&vf(1, 1), &ld(2, &value)]))).unwrap(); + assert_eq!(decoded.items[&1].child.as_option().unwrap().id, 4); +} + +#[test] +fn a_wrong_wire_type_inside_an_entry_is_an_error() { + // Key of a string map as a varint; value of an int map as length-delimited. + for entry in [vf(1, 1), ld(2, b"x"), cat(&[&ld(1, b"k"), &ld(2, b"x")])] { + assert!(assert_same(&ld(1, &entry)).is_err()); + } + // The entry itself as a varint. + assert!(assert_same(&vf(1, 1)).is_err()); +} + +#[test] +fn an_entry_that_runs_past_its_length_is_an_error() { + // A string of five bytes in an entry of length three. + let wire = cat(&[&[0x0a, 0x03, 0x0a, 0x05, b'a'], b"bcde"]); + assert!(assert_same(&wire).is_err()); + // An entry longer than the message. + assert_eq!( + Maps::decode_from_slice(&[0x0a, 0x09, 0x0a]), + Err(DecodeError::UnexpectedEof) + ); + // An entry of an impossible length. + assert!(assert_same(&cat(&[&[0x0a], &varint(u64::MAX)])).is_err()); +} + +#[test] +fn an_invalid_utf8_key_or_value_is_an_error() { + assert!(assert_same(&ld(1, &ld(1, &[0xff, 0xfe]))).is_err()); + assert!(assert_same(&ld(6, &ld(2, &[0xc0]))).is_err()); +} + +#[test] +fn a_closed_enum_value_that_is_not_known_makes_the_entry_unknown() { + // closed (3): key 5, value 7, which Color does not have. + let entry = cat(&[&vf(1, 5), &vf(2, 7)]); + let wire = ld(3, &entry); + let decoded = assert_same(&wire).unwrap(); + assert!(decoded.closed.is_empty()); + // The whole entry is kept, under the map's field number. + let kept: Vec<_> = decoded.unknown.iter().collect(); + assert_eq!(kept.len(), 1); + assert_eq!(kept[0].number, 3); + assert_eq!(kept[0].data, UnknownFieldData::LengthDelimited(entry.clone())); + assert_eq!(decoded.encode_to_vec(), wire); + + // A message that keeps no unknown fields drops it. + let lossy = Lossy::decode_from_slice(&wire).unwrap(); + assert!(lossy.closed.is_empty()); + assert_eq!( + ReferenceLossy::decode_from_slice(&wire).unwrap().0, + lossy, + "the reference drops it too" + ); +} + +#[test] +fn only_the_last_value_in_an_entry_decides_whether_a_closed_enum_is_known() { + let known_last = cat(&[&vf(1, 5), &vf(2, 7), &vf(2, 2)]); + let decoded = assert_same(&ld(3, &known_last)).unwrap(); + assert_eq!(decoded.closed, BTreeMap::from([(5, Color::Blue)])); + assert!(decoded.unknown.is_empty()); + + let unknown_last = cat(&[&vf(1, 5), &vf(2, 2), &vf(2, 7)]); + let decoded = assert_same(&ld(3, &unknown_last)).unwrap(); + assert!(decoded.closed.is_empty()); + assert_eq!(decoded.unknown.iter().count(), 1); +} + +#[test] +fn an_open_enum_value_that_is_not_known_is_kept() { + let decoded = assert_same(&ld(4, &cat(&[&vf(1, 1), &vf(2, 42)]))).unwrap(); + assert_eq!(decoded.open, BTreeMap::from([(true, EnumValue::from(42))])); +} + +#[test] +fn every_prefix_and_bit_flip_and_noise_input_matches_the_reference() { + let wire = Reference(populated()).encode_to_vec(); + // Miri is about a hundred times slower, so it takes every eleventh offset. + let stride = if cfg!(miri) { 11 } else { 1 }; + for end in (0..=wire.len()).step_by(stride) { + let _ = assert_same(&wire[..end]); + } + let mut flipped = wire.clone(); + for i in (0..wire.len()).step_by(stride) { + let original = flipped[i]; + for xor in [0x01, 0x07, 0x80, 0xff] { + flipped[i] = original ^ xor; + let _ = assert_same(&flipped); + } + flipped[i] = original; + } + let mut state = 0x9e37_79b9_7f4a_7c15_u64; + let mut next = || { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + state + }; + let rounds = if cfg!(miri) { 10 } else { 2000 }; + for _ in 0..rounds { + let len = (next() % 48) as usize; + let mut noise: Vec = (0..len).map(|_| next() as u8).collect(); + let _ = assert_same(&noise); + let keep = (next() as usize) % (wire.len() + 1); + noise.splice(0..0, wire[..keep].iter().copied()); + let _ = assert_same(&noise); + } +} + +#[test] +fn a_non_contiguous_buffer_is_gathered() { + let msg = populated(); + let wire = msg.encode_to_vec(); + let step = if cfg!(miri) { 17 } else { 1 }; + for split in (0..=wire.len()).step_by(step) { + let (head, tail) = wire.split_at(split); + let decoded = Maps::decode(&mut head.chain(tail)).unwrap(); + assert_eq!(decoded, msg, "split at {split}"); + } +} + +#[test] +fn a_map_field_decodes_from_a_contiguous_buffer_through_merge_field() { + let wire = ld(6, &cat(&[&vf(1, 3), &ld(2, b"x")])); + let mut msg = Maps::default(); + let mut buf = &wire[..]; + let tag = Tag::decode(&mut buf).unwrap(); + let limit = core::cell::Cell::new(1000); + let ctx = DecodeContext::new(crate::RECURSION_LIMIT, &limit); + msg.merge_field(tag, &mut buf, ctx).unwrap(); + assert_eq!(msg.names, BTreeMap::from([(3, "x".to_string())])); + assert!(buf.is_empty()); +} + +// --------------------------------------------------------------------------- +// Limits +// --------------------------------------------------------------------------- + +#[test] +fn the_element_memory_limit_counts_each_entry() { + // A thousand entries (under Miri, a tenth of that) of a scalar map, of a + // string map, and of a message map, each an empty entry that still + // materialises a key and a value. + let count = if cfg!(miri) { 100 } else { 1000 }; + for number in [1, 2, 6] { + let wire: Vec = (0..count).flat_map(|_| ld(number, &[])).collect(); + assert!(Maps::decode_from_slice(&wire).is_ok()); + let limited = DecodeOptions::new().with_element_memory_limit(100); + assert_eq!( + limited.decode_from_slice::(&wire), + Err(DecodeError::ElementMemoryLimitExceeded), + "field {number}" + ); + assert_eq!( + limited.decode_from_slice::(&wire).map(|r| r.0), + Err(DecodeError::ElementMemoryLimitExceeded), + "the reference agrees on field {number}" + ); + } +} + +#[test] +fn the_unknown_field_limit_counts_preserved_entries() { + let wire: Vec = (0..100).flat_map(|_| ld(3, &vf(2, 7))).collect(); + assert!(Maps::decode_from_slice(&wire).is_ok()); + let limited = DecodeOptions::new().with_unknown_field_limit(10); + assert_eq!( + limited.decode_from_slice::(&wire), + Err(DecodeError::UnknownFieldLimitExceeded) + ); + assert_eq!( + limited.decode_from_slice::(&wire).map(|r| r.0), + Err(DecodeError::UnknownFieldLimitExceeded) + ); + // Dropped entries do not count. + assert!(limited.decode_from_slice::(&wire).is_ok()); +} + +#[test] +fn the_recursion_limit_applies_to_message_values() { + // children (1): key "k", value: the same, `depth` levels deep. + fn nested(depth: usize) -> Vec { + let mut wire = Vec::new(); + for _ in 0..depth { + wire = ld(1, &cat(&[&ld(1, b"k"), &ld(2, &wire)])); + } + wire + } + assert!(Tree::decode_from_slice(&nested(50)).is_ok()); + assert_eq!( + Tree::decode_from_slice(&nested(150)), + Err(DecodeError::RecursionLimitExceeded) + ); + assert_eq!( + DecodeOptions::new() + .with_recursion_limit(10) + .decode_from_slice::(&nested(50)), + Err(DecodeError::RecursionLimitExceeded) + ); +} + +#[test] +fn a_failed_message_value_is_not_inserted() { + // The value's string is invalid UTF-8. + let entry = cat(&[&vf(1, 1), &ld(2, &ld(2, &[0xff]))]); + assert!(Maps::decode_from_slice(&ld(2, &entry)).is_err()); + let mut msg = Maps::default(); + let result = msg.merge_from_slice(&ld(2, &entry)); + assert!(result.is_err()); + assert!(msg.items.is_empty()); +} + +// --------------------------------------------------------------------------- +// Descriptors +// --------------------------------------------------------------------------- + +// --------------------------------------------------------------------------- +// Values reached through their `Message` impl +// --------------------------------------------------------------------------- + +/// A map whose values are `Reference`s, a hand-written `Message` with no +/// table, which the map reaches through its `Message` impl. +#[derive(Clone, Debug, Default, PartialEq)] +struct HoldsRefs { + refs: BTreeMap, +} + +static HOLDS_REFS: Table = crate::__table!( + HoldsRefs, + abi = ABI, + entries = [crate::__table_entry!( + HoldsRefs, + refs, + Map, + 1, + aux = 0, + slot = BTreeMap + )], + dense = &[0, 1], + aux = [Aux::Map(&MapVt::with_msg::< + BTreeMap, + kinds::StrRequired, + Reference, + >(&DirectMsgVt::via_message()))], + unknown = none, +); +table_message!(HoldsRefs, HOLDS_REFS); + +/// Declares `size` bytes and writes `writes` of them. +#[derive(Clone, Debug, Default, PartialEq)] +struct Liar { + size: u32, + writes: u8, +} +crate::impl_default_instance!(Liar); + +impl Message for Liar { + fn compute_size(&self, _: &mut SizeCache) -> u32 { + self.size + } + + fn write_to(&self, _: &mut SizeCache, buf: &mut impl EncodeSink) { + for _ in 0..self.writes { + buf.put_u8(1); + } + } + + fn merge_field( + &mut self, + _: Tag, + _: &mut impl Buf, + _: DecodeContext<'_>, + ) -> Result<(), DecodeError> { + unreachable!() + } + + fn clear(&mut self) {} +} + +#[derive(Clone, Debug, Default, PartialEq)] +struct HoldsLiars { + liars: BTreeMap, +} + +static HOLDS_LIARS: Table = crate::__table!( + HoldsLiars, + abi = ABI, + entries = [crate::__table_entry!( + HoldsLiars, + liars, + Map, + 1, + aux = 0, + slot = BTreeMap + )], + dense = &[0, 1], + aux = [Aux::Map(&MapVt::with_msg::< + BTreeMap, + kinds::StrRequired, + Liar, + >(&DirectMsgVt::via_message()))], + unknown = none, +); +table_message!(HoldsLiars, HOLDS_LIARS); + +fn holds_liars(size: u32, writes: u8) -> HoldsLiars { + HoldsLiars { + liars: BTreeMap::from([("k".to_string(), Liar { size, writes })]), + } +} + +fn refs() -> HoldsRefs { + let nested = Maps { + items: BTreeMap::from([( + 3, + Item { + child: MessageField::some(item(4, "nested")), + ..item(3, "three") + }, + )]), + ..Maps::default() + }; + HoldsRefs { + refs: BTreeMap::from([ + ("a".to_string(), Reference(populated())), + (String::new(), Reference::default()), + ("z".to_string(), Reference(nested)), + ]), + } +} + +#[test] +fn a_map_of_hand_written_messages_writes_the_same_bytes_to_every_sink() { + let msg = refs(); + let expected: Vec = msg + .refs + .iter() + .flat_map(|(key, value)| { + ld( + 1, + &cat(&[&ld(1, key.as_bytes()), &ld(2, &value.encode_to_vec())]), + ) + }) + .collect(); + // Through the cursor that `encode_to_vec` writes, which each value is + // written to directly. + assert_eq!(msg.encode_to_vec(), expected); + assert_eq!(msg.encoded_len() as usize, expected.len()); + // A `Rope` is not written through the cursor, so each value is staged in + // a buffer of the size it declared. + let mut rope = Rope::new(); + msg.encode(&mut rope); + assert_eq!(&rope.to_contiguous_bytes()[..], &expected[..]); + // Room for one byte at a time. + let mut chunked = crate::bytes::BytesMut::with_capacity(1); + msg.encode_length_delimited(&mut chunked); + assert_eq!(&chunked[..], &cat(&[&varint(expected.len() as u64), &expected])[..]); + // `write_to` on a `Vec` is not the cursor either. + let mut cache = SizeCache::new(); + msg.compute_size(&mut cache); + let mut out = Vec::new(); + msg.write_to(&mut cache, &mut out); + assert_eq!(out, expected); + // Decoding reads each value, and the maps and messages nested in it, + // through its `Message` impl. + assert_eq!(HoldsRefs::decode_from_slice(&expected).unwrap(), msg); +} + +#[test] +#[should_panic(expected = "more bytes than compute_size declared")] +fn a_map_value_that_writes_more_than_it_sized_panics_in_the_scratch_buffer() { + // The value is staged in a buffer of the size `compute_size` gave. + holds_liars(0, 1).encode(&mut Rope::new()); +} + +#[test] +#[cfg(debug_assertions)] +#[should_panic(expected = "different byte count than compute_size declared")] +fn a_map_value_that_writes_less_than_it_sized_panics_in_debug_builds() { + holds_liars(3, 1).encode(&mut Rope::new()); +} + +mod invalid_descriptors { + use super::*; + + static ENUM_VT: EnumVt = EnumVt::new::>(); + static ENUM_AUX: [Aux; 1] = [Aux::Enum(&ENUM_VT)]; + + #[test] + #[should_panic(expected = "a map key must be of a required scalar or string kind")] + fn a_key_must_have_required_cardinality() { + let _ = + MapVt::new::, i32>, kinds::Int32Optional, kinds::Int32Required>(); + } + + #[test] + #[should_panic(expected = "a map value must be of a required kind or a singular message")] + fn a_value_must_have_required_cardinality() { + let _ = + MapVt::new::>, kinds::Int32Required, kinds::Int32Repeated>(); + } + + #[test] + #[should_panic(expected = "a map's enum values must have an implicit shape")] + fn an_enum_value_must_have_an_implicit_shape() { + let _ = MapVt::with_enum::< + BTreeMap>, + kinds::Int32Required, + OptionalClosed, + >(); + } + + #[test] + #[should_panic(expected = "wrong variant")] + fn a_map_entry_needs_a_map_descriptor() { + const E: Entry = Entry::new(Kind::Map, 1, 0, 0); + // SAFETY: the table is dropped without being used. + let _: Table = unsafe { Table::new(ABI, &[E], &[], &ENUM_AUX, None) }; + } +} diff --git a/buffa/src/table/mod.rs b/buffa/src/table/mod.rs index b23f084c..4f26b273 100644 --- a/buffa/src/table/mod.rs +++ b/buffa/src/table/mod.rs @@ -9,6 +9,14 @@ //! cardinality). The `Message` methods forward to the interpreters here, //! which every message shares. //! +//! A map field is one entry of kind [`Kind::Map`] whose [`MapVt`] describes +//! the key and the value and how to iterate and insert into the collection. +//! The key and value are then sized, written and decoded by the same +//! interpreters as any other field. No size, write or decode logic is +//! instantiated per key or value type; only iterating and inserting into the +//! collection are instantiated per collection type, in the crate that owns +//! the message and at its optimisation level. +//! //! This module is support code for generated code, is not meant to be called //! directly, and may change in any release, so generated code must be //! regenerated with the `buffa-codegen` that matches the `buffa` it builds @@ -42,8 +50,8 @@ //! chunk. Only a caller that drives `merge_field` itself is affected, such //! as the default `Message::merge_group`, so a message that is the type of a //! group field must not use the table strategy. -//! - [`clear`](crate::Message::clear) resets to `Default`, which releases -//! allocations that unrolled code keeps. +//! - [`clear`](crate::Message::clear) resets to `Default`, which releases the +//! allocations of strings, vectors and maps that unrolled code keeps. //! - A child reached through its [`Message`](crate::Message) impl (see //! [`MsgVt::new_via_message`]) has the same wire format and accepts the same //! input, but it is staged in a scratch buffer and copied when it is written @@ -67,7 +75,9 @@ //! this crate for speed and its own generated code for size. Encoding into //! any other sink, such as a [`Rope`](crate::Rope) or a `BufMut` passed //! straight to `Message::write_to`, and the generic wrappers around -//! decoding are instantiated in the crate that calls them. +//! decoding are instantiated in the crate that calls them. Iterating and +//! inserting into a map's collection is also compiled there, so raising this +//! crate's optimisation level does not speed it up. use core::marker::PhantomData; @@ -76,10 +86,11 @@ use crate::bytes::Buf; use crate::encoding::{Tag, WireType}; use crate::{DecodeContext, DecodeError, EncodeSink, SizeCache, UnknownFields}; +pub use map::MapVt; pub use oneof::{Member, OneofEnum, OneofVt}; pub use shape::{ - EnumShape, EnumVt, ImplicitClosed, ImplicitOpen, MsgSlot, MsgVt, OptionalClosed, OptionalOpen, - RepVt, RepeatedClosed, RepeatedOpen, + DirectMsgVt, EnumShape, EnumVt, ImplicitClosed, ImplicitOpen, MsgSlot, MsgVt, OptionalClosed, + OptionalOpen, RepVt, RepeatedClosed, RepeatedOpen, }; use scalar::{ @@ -227,6 +238,7 @@ macro_rules! kind_table { MsgRepeated: Msg Msg REPEATED; OneofLeader: Oneof Oneof LEADER; OneofFollower: Oneof Oneof ONEOF; + Map: Map Map IMPLICIT; } }; } @@ -261,7 +273,10 @@ macro_rules! payload_kind_table { macro_rules! define_kind { ($($name:ident: $fam:ident $ty:ident $card:ident;)*) => { - /// The type and cardinality of a field: one interpreter arm each. + /// The type and cardinality of a field. The dispatch functions have an + /// arm for each kind, except that the one for `Map` is unreachable: + /// the loops test for a map first, for a reason that + /// `buffa/src/table/map.rs` gives. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[repr(u8)] pub enum Kind { @@ -283,6 +298,14 @@ macro_rules! define_kind { } } + /// The cardinality of the kind: one of `IMPLICIT`, `REQUIRED`, + /// `OPTIONAL`, `REPEATED` and `PACKED`. + const fn card(self) -> u8 { + match self { + $(Kind::$name => define_kind!(@card $card),)* + } + } + /// The cardinality a field of this kind has in an enum shape: /// `IMPLICIT` (also for a required field), `OPTIONAL` or `REPEATED` /// (also for a packed field). @@ -302,6 +325,7 @@ macro_rules! define_kind { $( #[doc = concat!("The type-level name of [`Kind::", stringify!($name), "`](super::Kind::", stringify!($name), ").")] pub struct $name; + impl KindMarker for $name { const KIND: Kind = Kind::$name; } define_kind!(@slot $name $fam $ty $card); )* } @@ -326,16 +350,24 @@ macro_rules! define_kind { // A oneof member's field is the `Option` of the oneof's enum, which its // aux descriptors check. (@slot $name:ident Oneof $ty:ident $card:ident) => {}; + // A map is checked against its aux descriptor, which is built for the map's + // type. + (@slot $name:ident Map $ty:ident $card:ident) => {}; (@wire Scalar $ty:ident PACKED) => { WireType::LengthDelimited as u32 }; (@wire Scalar $ty:ident $card:ident) => { <$ty as Sc>::WIRE as u32 }; (@wire Str $ty:ident $card:ident) => { WireType::LengthDelimited as u32 }; (@wire Bytes $ty:ident $card:ident) => { WireType::LengthDelimited as u32 }; (@wire Msg $ty:ident $card:ident) => { WireType::LengthDelimited as u32 }; + (@wire Map $ty:ident $card:ident) => { WireType::LengthDelimited as u32 }; (@wire Enum $ty:ident PACKED) => { WireType::LengthDelimited as u32 }; (@wire Enum $ty:ident $card:ident) => { WireType::Varint as u32 }; (@wire Oneof $ty:ident $card:ident) => { panic!("a oneof member's wire type is its payload kind's, so build its entry with `Entry::oneof_member`") }; + // The members of a oneof have no cardinality of their own. + (@card ONEOF) => { IMPLICIT }; + (@card LEADER) => { IMPLICIT }; + (@card $card:ident) => { $card }; (@shape IMPLICIT) => { IMPLICIT }; (@shape REQUIRED) => { IMPLICIT }; (@shape OPTIONAL) => { OPTIONAL }; @@ -348,6 +380,7 @@ macro_rules! define_kind { (@aux Bytes $card:ident) => { None }; (@aux Enum $card:ident) => { Some(AuxKind::Enum) }; (@aux Oneof $card:ident) => { Some(AuxKind::Member) }; + (@aux Map $card:ident) => { Some(AuxKind::Map) }; (@aux Msg REPEATED) => { Some(AuxKind::Rep) }; (@aux Msg $card:ident) => { Some(AuxKind::Msg) }; } @@ -355,11 +388,17 @@ macro_rules! define_kind { /// The type of the field that an entry of a [`kinds`] type describes, for the /// scalar, string and bytes kinds. Enum and message kinds have none, because /// their field type depends on the enum or message. -pub trait KindSlot { +pub trait KindSlot: KindMarker { /// The type of the field. type Slot; } +/// The [`Kind`] that a type of the [`kinds`] module names. +pub trait KindMarker { + /// The kind. + const KIND: Kind; +} + kind_table!(define_kind); macro_rules! define_payload_check { @@ -379,6 +418,7 @@ payload_kind_table!(define_payload_check); mod bridge; mod decode; mod encode; +mod map; mod oneof; mod scalar; mod shape; @@ -392,6 +432,7 @@ enum AuxKind { Enum, Group, Member, + Map, } /// Per-field data that a kind needs beyond the field's offset. @@ -407,6 +448,8 @@ pub enum Aux { Group(&'static OneofVt), /// One member of a oneof ([`Kind::OneofLeader`] or [`Kind::OneofFollower`]). Member(Member), + /// The descriptor of a map field ([`Kind::Map`]). + Map(&'static MapVt), } impl Aux { @@ -417,6 +460,7 @@ impl Aux { Aux::Enum(_) => AuxKind::Enum, Aux::Group(_) => AuxKind::Group, Aux::Member(_) => AuxKind::Member, + Aux::Map(_) => AuxKind::Map, } } } @@ -551,6 +595,14 @@ impl MessageTable { } } + #[inline] + fn map_vt(&self, e: &Entry) -> &'static MapVt { + match &self.aux[usize::from(e.aux)] { + Aux::Map(vt) => vt, + _ => unreachable!("`Table::new` checked that map entries index `MapVt`s"), + } + } + #[inline] fn enum_vt(&self, e: &Entry) -> &'static EnumVt { match &self.aux[usize::from(e.aux)] { @@ -652,7 +704,9 @@ impl Table { /// [`OneofEnum`] implementation gives, for the member's number, a pointer /// to a value of the member's payload kind, under the same rules as the /// kinds above (for `MsgSingular`, a [`MsgVt::direct`] or - /// [`MsgVt::direct_via_message`] descriptor of the message). + /// [`MsgVt::direct_via_message`] descriptor of the message); + /// - `Map`: exactly the collection type the entry's [`MapVt`] was built + /// for. /// /// `unknown`, if present, must be the offset of a field of type /// `UnknownFields`. The `__table_entry!` macro checks the field types @@ -1003,9 +1057,10 @@ macro_rules! __table { /// let _ = buffa::__table_entry!(Point, x, StrImplicit, 1); /// ``` /// -/// The scalar, string and bytes kinds have a fixed field type. The enum and -/// message kinds take the type explicitly, as `aux = , slot = `, -/// where the type is the one their aux descriptor was built for. A oneof +/// The scalar, string and bytes kinds have a fixed field type. The enum, +/// message and map kinds take the type explicitly, as +/// `aux = , slot = `, where the type is the one their aux +/// descriptor was built for (for a map, the collection). A oneof /// member is written `oneof(, )`, where `` says /// whether it is the oneof's lowest-numbered member, with the `Option` of the /// oneof's enum as its slot type and the index of its [`Member`] as `aux`. @@ -1058,3 +1113,9 @@ macro_rules! __table_entry { mod tests { include!("tests.rs"); } + +#[cfg(test)] +#[rustversion::since(1.77)] +mod map_tests { + include!("map_tests.rs"); +} diff --git a/buffa/src/table/shape.rs b/buffa/src/table/shape.rs index e3017a33..a105b58f 100644 --- a/buffa/src/table/shape.rs +++ b/buffa/src/table/shape.rs @@ -193,6 +193,38 @@ impl MsgVt { } } +/// A [`MsgVt`] for a slot that holds an `M` itself, as a map's value does, +/// whose type records that its message is an `M`, so that a map of `M` can be +/// built only from a descriptor of an `M`. +pub struct DirectMsgVt { + pub(super) vt: MsgVt, + _marker: PhantomData, +} + +impl DirectMsgVt { + /// Describe a message whose table `table` is; see [`MsgVt::direct`]. + #[must_use] + pub const fn new(table: &'static Table) -> Self { + Self { + vt: MsgVt::direct(table), + _marker: PhantomData, + } + } + + /// Describe a message whose table is not visible here, which is reached + /// through its [`Message`] impl; see [`MsgVt::direct_via_message`]. + #[must_use] + pub const fn via_message() -> Self + where + M: Message, + { + Self { + vt: MsgVt::direct_via_message::(), + _marker: PhantomData, + } + } +} + // --------------------------------------------------------------------------- // Repeated message fields // --------------------------------------------------------------------------- @@ -378,6 +410,14 @@ impl EnumVt { } } +/// `EnumVt::new::()` as an associated constant, so that a reference to it +/// has the `'static` lifetime a descriptor needs. +pub(super) struct EnumVtOf(PhantomData); + +impl EnumVtOf { + pub(super) const VT: EnumVt = EnumVt::new::(); +} + macro_rules! enum_shape { ($(#[$m:meta])* $name:ident, $card:ident, $slot:ty, $set:expr, $get:expr, $len:expr, $accepts:expr) => { $(#[$m])* diff --git a/buffa/src/table/size.rs b/buffa/src/table/size.rs index 15bcf038..25ef5ed6 100644 --- a/buffa/src/table/size.rs +++ b/buffa/src/table/size.rs @@ -1,5 +1,6 @@ //! The size pass: [`compute_size`] and its per-kind arms. +use super::map::size_map; use super::scalar::Sc; use super::{ Bool, Double, Entry, Fixed32, Fixed64, Float, Int32, Int64, Kind, MessageTable, Sfixed32, @@ -24,7 +25,15 @@ pub(super) unsafe fn compute_size( let mut size = 0u64; for e in table.entries { // SAFETY: the offset is within the message, per the table's contract. - size += unsafe { size_kind(table, e, base.add(e.offset as usize), cache) }; + let slot = unsafe { base.add(e.offset as usize) }; + // SAFETY: `slot` is the field `e` describes. + size += unsafe { + if e.kind == Kind::Map { + size_map(table, e, slot, cache) + } else { + size_kind(table, e, slot, cache) + } + }; } if table.unknown != NO_UNKNOWN { // SAFETY: `unknown` is the offset of the message's `UnknownFields`. @@ -37,6 +46,8 @@ pub(super) unsafe fn compute_size( /// Defines `$fname`, the size of one field by kind, for the kinds listed. Two /// lists are used, all kinds for `size_kind` and the payload kinds of a oneof /// member for `size_payload`. +// The arm for a map is unreachable: `compute_size` tests for a map before it +// calls this. The reason is in the module documentation of `map.rs`. macro_rules! size_dispatch { ($fname:ident; $($name:ident: $fam:ident $ty:ident $card:ident;)*) => { /// # Safety @@ -44,7 +55,7 @@ macro_rules! size_dispatch { /// `slot` points to the field `e` describes, in a live message of the /// type `table` describes. #[inline] - unsafe fn $fname( + pub(super) unsafe fn $fname( table: &MessageTable, e: &Entry, slot: *const u8, @@ -85,6 +96,9 @@ macro_rules! size_dispatch { (@arm Oneof $ty:ident ONEOF $table:ident $e:ident $tl:ident $slot:ident $cache:ident) => { 0 }; + (@arm Map $ty:ident $card:ident $table:ident $e:ident $tl:ident $slot:ident $cache:ident) => { + unreachable!("a map field is sized by `compute_size`") + }; } kind_table!(size_dispatch, size_kind); diff --git a/docs/guide.md b/docs/guide.md index 564b771f..6ed9d978 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -1191,7 +1191,7 @@ Buffa uses a two-pass model to avoid the exponential-time size computation that ### Smaller generated code: `codec_strategy` -By default every generated message contains its own size, write, and merge code, specialised to its fields. `CodecStrategy::Table` replaces it with one static table per message and interpreters in `buffa` that every message shares. On the WhatsApp schema (`whatsapp.proto` from `waproto`: 334 top-level messages, 752 with the nested ones, 3,477 fields), built with `Box` message fields, no unknown-field preservation, fat LTO, and `panic = "abort"`, the text section of the binary at `opt-level = "z"` went from 1,669 KB to 961 KB (−42%). 3 of its 752 messages (2 of 334 top-level) stay unrolled because they have a `map`. [#463](https://github.com/anthropics/buffa/issues/463) describes the method. The cost is speed on messages made of many small fields, where encoding takes up to about 3.5 times as long as with the default `CodecStrategy::Unrolled` and decoding up to 1.6 times. Messages dominated by bulk data, such as large strings, bytes, and packed arrays, show no difference. +By default every generated message contains its own size, write, and merge code, specialised to its fields. `CodecStrategy::Table` replaces it with one static table per message and interpreters in `buffa` that every message shares. On the WhatsApp schema (`whatsapp.proto` from `waproto`: 334 top-level messages, 752 with the nested ones, 3,477 fields), built with `Box` message fields, no unknown-field preservation, fat LTO, and `panic = "abort"`, the text section of the binary at `opt-level = "z"` went from 1,669 KB to 968 KB (−42%). All 752 of its messages use the table. [#463](https://github.com/anthropics/buffa/issues/463) describes the method. The cost is speed on messages made of many small fields, where encoding takes up to about 3.5 times as long as with the default `CodecStrategy::Unrolled` and decoding up to 1.6 times. Messages dominated by bulk data, such as large strings, bytes, and packed arrays, show no difference. ```rust,ignore // build.rs @@ -1204,30 +1204,30 @@ buffa_build::Config::new() .compile()?; ``` -Apart from the holders listed below, a table message may hold any message, including as a member of a `oneof`. It reaches a child that has a table through it, and any other child (a message you set to `Unrolled`, one generated by another crate, or a well-known type such as `Timestamp`) through its `Message` impl, which costs a function call per child. A `codec_strategy_in` rule selects the message it names and the messages nested in it, and does not extend to the messages it holds. In the example, `.wa.Message` and `.wa.Receipt` stay specialised and the messages that hold them use the table. Setting a message to `Unrolled` therefore does not keep the messages that hold it unrolled: to keep a whole path specialised, set its holders to `Unrolled` too. +Apart from the holders listed below, a table message may hold any message, including as a member of a `oneof` and as a `map` value. It reaches a child that has a table through it, and any other child (a message you set to `Unrolled`, one generated by another crate, or a well-known type such as `Timestamp`) through its `Message` impl, which costs a function call per child. A `codec_strategy_in` rule selects the message it names and the messages nested in it, and does not extend to the messages it holds. In the example, `.wa.Message` and `.wa.Receipt` stay specialised and the messages that hold them use the table. Setting a message to `Unrolled` therefore does not keep the messages that hold it unrolled: to keep a whole path specialised, set its holders to `Unrolled` too. The option changes only the binary `Message` implementation. The wire format and the JSON, text, view, and reflection code are the same under both strategies, and a table message encodes to the same bytes and decodes the same accepted input as its unrolled twin. It differs in these ways: - A field that declares a length past the end of its enclosing message fails at once with `DecodeError::UnexpectedEof`, where unrolled code reads on into the enclosing message and can report a different error for the same rejected input. A child reached through its `Message` impl is read from the slice of the nearest enclosing table message, so it is bounded there. - The table decodes from one contiguous slice, so a `Buf` that is not contiguous is gathered into one buffer first. `Message::merge_field` on a table message cannot gather, and returns `UnexpectedEof` for such a buffer; only code that calls it directly is affected, such as the default `merge_group`. A message that another crate or another codegen run uses as the type of a group or `DELIMITED` field must therefore stay `Unrolled`. Within one run, codegen keeps the type of a group field unrolled itself. -- `clear()` resets a table message to `Default`, so it releases the capacity of its strings and vectors instead of keeping it. +- `clear()` resets a table message to `Default`, so it releases the capacity of its strings, vectors, and maps, where unrolled code keeps it. - Encoding into any sink other than the cursor that `Message::encode` and its siblings write a `BufMut` through stages each child reached through its `Message` impl in a scratch buffer first. Those sinks are a `Rope`, a sink defined outside `buffa`, and a `BufMut` passed straight to `Message::write_to`. A `Rope` copies the child again and cannot share the `bytes` fields inside it by reference count. - Codegen cannot see the fields of a message from another crate, so a table message that holds one with a `bytes` field of a non-default type copies it on decode. The well-known type `google.protobuf.Any` is one, because its `value` is `bytes::Bytes`. To keep the payload shared with a `Bytes` input, set the holder to `Unrolled` with `codec_strategy_in`. These stay unrolled, whatever the setting: -- a message with a `map` or a group field; +- a message with a group field; - the message type of a group field; - a message that uses the `MessageSet` wire format; - a message with extension ranges, when JSON code is generated and unknown fields are preserved; -- a message with a field of a non-default string, bytes, or collection type, which `use_bytes_type`, `string_type`, `bytes_type`, and `repeated_type` select; +- a message with a field of a non-default string, bytes, or collection type, which `use_bytes_type`, `string_type`, `bytes_type`, `repeated_type`, and a custom map type (`map_type_custom`) select. `HashMap` and `BTreeMap` maps are supported; a `map` whose keys or values are of a non-default string or bytes type is not; - a message that holds, in a singular, repeated, `oneof`, or map value field, a message of the same run that has a `bytes` field of a non-default type, or that holds one. The table decodes from one contiguous slice, so it would copy the `bytes::Bytes` fields that unrolled code decoding from a `Bytes` shares with the input. Codegen prints one warning per run that counts the messages that fell back, groups them by reason, and names a few of each. Setting the messages that cause a fallback to `Unrolled` with `codec_strategy_in` silences it. A `codec_strategy_in` rule that selects the table for a message by its exact path, when the message cannot use it, is an error, because the rule asked for something impossible. The table code needs Rust 1.77 or later; `buffa-build` returns an error on an older compiler, and the plugin's output does not compile on one. It contains `unsafe` code, in macros inside `buffa`, so the generated code compiles in a crate with `#![forbid(unsafe_code)]`. The `buffa::table` module the code calls may change in any release, so regenerate the code whenever you update `buffa`; a mismatch is a compile error. -`compute_size`, decoding from a contiguous buffer, and encoding into a `BufMut` are compiled in `buffa`, at the `opt-level` `buffa` is built with. A build that sets `opt-level = "z"` for everything can spend a little size to recover speed with `[profile.release.package.buffa] opt-level = 3`. Encoding into any other sink, such as a `Rope`, and the generic wrappers around decoding are compiled in your crate. +`compute_size`, decoding from a contiguous buffer, and encoding into a `BufMut` are compiled in `buffa`, at the `opt-level` `buffa` is built with. A build that sets `opt-level = "z"` for everything can spend a little size to recover speed with `[profile.release.package.buffa] opt-level = 3`. Encoding into any other sink, such as a `Rope`, and the generic wrappers around decoding are compiled in your crate. A map field has no size, write, or decode logic per key or value type; only iterating the map and inserting into it are instantiated per collection type, in the crate that owns the message, at its `opt-level`. Raising `buffa`'s `opt-level` therefore speeds the interpreters but not the hash map's insertion and iteration. ### Error handling