NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
crates.io · #3865 most downloaded on crates.io
Protobuf well-known types for buffa
Last release 1 months ago
03 Sep 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 15 of 15 stable releases
Nothing withdrawn
no release was ever pulled
6 months old
15 releases · first in 2026
One column per month.
json: reject nonempty Any objects without @type by @fallintoplace in #334
Full Changelog: v0.9.1...v0.9.2
A patch release, and a large one: 55 pull requests from twelve contributors, most of them closing gaps in how buffa bounds untrusted input. 0.9.0 introduced the element-memory budget for the binary decoder; an audit of everything downstream of that decoder found the same fail-open shape repeated — the view and lazy-view entry points, the reflective decoder's packed scalars and enums, the textproto parser, extension records, nested unknown groups, descriptor-pool construction, and serialization of nested google.protobuf.Any — and each is now bounded. That includes the CVE-2026-55407 follow-up reported in #357, where the extension-merge path gave every wire record its own full allowance. The two entries under Security are the stack overflow in reflective Any serialization and the map-entry parser differential between view and owned decoding. If your service decodes protobuf from the network, this is the release to take.
Regenerate your code with a version-matched buffa-codegen. Several fixes live on the generated path: decode_view now attaches the element budget, the view decoders merge split map values the way the owned decoder does, to_dynamic no longer rejects large messages the process built itself, generated enums carry #[allow(non_camel_case_types)], and generated code reaches core::fmt and serde through absolute paths so a proto package named core or serde compiles. buffa-build users pick these up on the next build; protoc-gen-buffa and buf generate users must reinstall the plugin and regenerate — code generated by 0.9.1 still builds and runs against this runtime and gets every runtime-side fix, but keeps the old view decoders until it is regenerated.
The API is additive and MSRV is unchanged at 1.75; the migration surface is behavioural. Every hardening entry turns input that was previously unbounded into a limit error, and beyond those, five changes tighten what ordinary use could previously rely on, each called out in its own entry: the textproto parser applies an element budget by default, so a very large checked-in fixture can now fail to parse until the bound is raised; DynamicMessage charges packed scalars, so a reflective decode of a columnar payload above roughly half a million elements needs with_element_memory_limit raised; DescriptorPool rejects descriptor sets that reuse a reserved name or number, place a field in the 19000–19999 band, start an open enum at a non-zero value, or alias enum values without allow_alias — protoc never emits those, so only hand-built sets are affected; PartialEq for DynamicMessage compares unknown fields, so two messages that differ only in unknown content no longer compare equal; and the ProtoJSON FieldMask helpers now round-trip a leading underscore and reject malformed path components on both codecs. One addition can break a build rather than a decode: buffa-types gains google::protobuf::Option, which shadows the prelude Option in any module that glob-imports google::protobuf::*.
Beyond the hardening: SharedCorpusContext and shared_descriptor_pool cut codegen time and duplicated descriptor bytes for workspaces that generate one crate per package; DynamicMessage::try_from_message is a fallible bridge from a generated message; buffa-types now ships the complete well-known set, so googleapis compiles; exclude_package drops packages from a buffa-build run; the plugin gains unbox_oneof and shared_descriptor_pool options; and SizeCache panic sites are gated on debug_assertions, shrinking generated compute_size.
CodeGenConfig::shared_descriptor_pool emits the reflection descriptor set once at the module-tree root instead of once per package (#311). When enabled, per-package __buffa::reflect modules delegate to a single shared __buffa_fds module; every consumer path (pkg::descriptor_pool(), pkg::FILE_DESCRIPTOR_SET_BYTES) keeps resolving as an alias, and all packages observe one DescriptorPool. Adds the encode_descriptor_set, shared_descriptor_root_module, FdsEmbedding, and CodeGenConfig::reflect_feature_gate surface that front-ends use to emit the shared module.
shared_descriptor_pool=true plugin option on protoc-gen-buffa and protoc-gen-buffa-packaging (#370) — enables the shared descriptor pool (see CodeGenConfig::shared_descriptor_pool) on the plugin path. Set it on both plugins (same name on both, like exclude_package). Rejected together with feature overrides, feature-gating, or file_per_package, which the plugin path cannot support.
buffa_build::Config::shared_descriptor_pool enables the shared descriptor pool from build.rs (#369). The descriptor set is written once as a binary sidecar next to the generated tree and include_bytes!-d by the shared __buffa_fds module, removing both the per-package duplication and the byte-literal source expansion. Requires reflection and .include_file(...). With a checked-in out_dir, commit the emitted *.descriptor_set.binpb sidecar alongside the generated .rs.
DynamicMessage::decode_with_options and merge_with_options decode under caller-supplied limits (#341), mirroring DescriptorPool::decode_with_options.
TextDecoder::with_element_memory_limit and buffa::text::decode_from_str_with_element_memory_limit raise the textproto parser bound above the default (#344), reported past the limit as the new ParseErrorKind::ElementMemoryLimitExceeded variant.
A defaulted SingularCodec::decode_one_ctx lets repeated extension elements share one decode budget (#345). Existing implementations need no change; the default forwards to decode_one, which is correct for any codec with a fixed per-record cost. A codec whose value can materialize an unbounded subtree must override it.
buffa_descriptor::MAX_SYMBOL_LEN and the PoolError::NameTooLong variant (#347). PoolError is #[non_exhaustive], so the variant is additive.
generate_with_diagnostics warns when a kept file references a type from an excluded or unmapped package (#352, closes #292).
generate_with_diagnostics now emits a CodeGenWarning::ExcludedPackageFieldRef when a field in a kept file references a type that is neither being generated nor covered by an extern_path. The warning precedes the consumer's build, so the dangling super::…::Type compile error is accompanied by an actionable diagnostic that names the file, message, and field where the reference appears, the missing package, and the exact extern_path or .files() fix. Detection is type-granular: references to a specific type from a partially-generated package are correctly flagged even when other types in the same package are being generated.
buffa-build: Config::exclude_package knob (#383, closes #293). Config::exclude_package("buf.validate") (or .exclude_package(".buf.validate")) drops all .proto files in that package — and any sub-packages — from code generation. Multiple calls accumulate. The same knob is exposed as exclude_package=<pkg> in the protoc-gen-buffa plugin option string and as CodeGenConfig::exclude_packages for direct codegen users. An entry that matches no package in the input now raises CodeGenWarning::ExcludePackageMatchedNothing (a cargo:warning from buffa-build).
protoc-gen-buffa gains unbox_oneof=true and repeatable unbox_oneof_in=<path> options (#392), the plugin equivalents of buffa_build::Config::unbox_oneof() / unbox_oneof_in(&[..]), opting non-recursive message/group oneof variants into inline storage. Paths are normalized like the other path-scoped options (leading dot optional, whitespace and trailing dots stripped); an empty path is rejected rather than treated as the blanket rule.
buffa-types ships google.protobuf.Api, Type, Enum, and SourceContext (#394, closes #382). Codegen auto-maps the whole .google.protobuf package to buffa-types, so compiling googleapis (or any schema importing api.proto / type.proto / source_context.proto) failed with cannot find type Api / TypeView / EnumView. The three files are the rest of the official well-known set; they are vendored at protobuf v33.5 and generated next to Timestamp/Any, with binary, view, and text codecs and textproto Any expansion. They have no Serialize/Deserialize impls, so a json = true message that embeds one of them does not compile; map the type to a locally generated copy with extern_path if JSON is needed. One of the new types is google::protobuf::Option, which is not re-exported at the crate root: a module that glob-imports buffa_types::google::protobuf::* now has it shadowing the prelude Option, and Option<T> there fails with E0107 — import the types you use by name.
SharedCorpusContext lets a workspace that generates one crate per proto package pay the corpus-wide codegen analysis once (#401, closes #400). generate() re-derives which oneof variants are unboxed, which message fields are stored inline, and the per-symbol comment map from the whole FileDescriptorSet on every call, although none of that depends on the per-package extern_paths; a ~2900-package workspace paid that walk 2900 times. Build a SharedCorpusContext::new(&files, &config) once and set CodeGenConfig::shared_corpus_context on each per-package config; the output is byte-identical, and a context built from a different corpus or different oneof/pointer-repr rules is refused with CodeGenError::SharedCorpusContextMismatch rather than silently resolving against the wrong one. The corpus comparison is structural, not by file name, so a regenerated FileDescriptorSet with edited content is refused too; it costs O(corpus) per call, measured at roughly a quarter of the name-only comparison's saving on that workspace and still well ahead of not sharing. Off by default.
CodeGenConfig::shared_descriptor_pool_root (#399): override the computed super::-relative path to the shared __buffa_fds root in shared-pool mode (shared_descriptor_pool) with a caller-supplied path, for workspaces where one crate does not host the whole package tree as nested modules (e.g. one independent Cargo crate per proto package). None (the default) keeps the existing super::-relative behaviour — purely additive, no change to any existing consumer. Reachable only by direct buffa-codegen callers this release; buffa-build and the protoc-gen-buffa* plugins do not expose it yet.
DynamicMessage::try_from_message: a fallible, name-resolving bridge from a generated message (#414, refs #413). Resolves the descriptor from the type's MessageName and returns a new BridgeError (MessageNotFound / MessageTooLarge / Decode, re-exported from buffa_descriptor::reflect and the crate root) instead of panicking when the pool lacks the type or the encoded bytes fail to decode against its descriptor. DynamicMessage::try_from_message_with_index is the fallible form for callers that already hold a MessageIndex; from_message now shares its decode step. The bridge uses the same encoded-length-scaled memory budgets as generated ReflectMessage::to_dynamic, so large generated messages avoid false ElementMemoryLimitExceeded failures without making the second representation unbounded.
Two decode-limit documentation claims corrected (#349). The unknown-field count limit was described as capping overhead at roughly limit x 40 bytes; that holds for flat input only, and nested groups are now charged separately (see the Fixed entry for #349). And the Map hasher docs described the foldhash process-wide seed as mixing ASLR addresses with process start time; that holds on std, but no_std builds drop the clock and allocator entropy, and on a bare-metal target with no ASLR the seed is a link-time constant, so a colliding key set can be precomputed from the firmware image. These two corrections are documentation-only.
SizeCache panic locations are now gated on debug_assertions, shrinking generated code (#366).
Generated compute_size calls SizeCache::set once per length-delimited sub-message field, and generated write_to calls consume_next once as well, so a large schema produces hundreds to thousands of call sites for each. Both carried #[track_caller], which materializes a Location record at every one of them, and set inlined a formatted assert! panic block on top of that. The attribute is now gated on debug_assertions and set's panic joins consume_next's out of line.
Measured on a ~1000-message schema linked into a 13.2 MiB binary (fat LTO, codegen-units = 1, panic = "abort", stripped): 1068 Location records removed, and 106 KiB (0.79%) off the linked binary.
Both bound checks still run, with the same conditions and messages as before. What changes is that a violation reports a location inside buffa rather than in your compute_size / write_to. The gate follows the profile buffa itself was compiled with; to get caller locations back in a release build, set [profile.release.package.buffa] debug-assertions = true (which also enables buffa's other debug assertions, so treat it as a diagnostic setting).
Extension registry lookups by (extendee, number) no longer allocate, in either the JSON or the text registry (#393). Internal representation only; no API, wire, or behaviour change.
Same-module AIP-192 rustdoc links are emitted bare (#394). A [Mixin][] reference to a type in the same generated module now renders as [Mixin] rather than [Mixin](crate::…::Mixin), which rustdoc::redundant_explicit_links rejects under -D warnings; cross-module references, fully-qualified display names, and keyword-named types keep the explicit target.
A nonempty JSON object for google.protobuf.Any without the required @type discriminator is now rejected (#334) instead of silently decoding as an empty message.
Generated decode_view now applies DEFAULT_ELEMENT_MEMORY_LIMIT, matching Message::decode (#338). The view field arms already charged the budget, but the default entry point never attached one, and register_element_memory is a no-op without it — so FooView::decode_view and OwnedView::decode accepted repeated-field payloads that the owned decoder rejects. DecodeOptions::decode_view was unaffected. Callers that legitimately decode views of very large repeated fields can raise the ceiling with DecodeOptions::with_element_memory_limit(n).decode_view(..).
google.protobuf.Any expansion depth is capped at type_registry::MAX_ANY_EXPANSION_DEPTH (100, matching RECURSION_LIMIT) in the hand-written Any impls in buffa-types, for both JSON and textproto (#339). Expanding an Any decodes its payload and serializes the result, so an Any holding an Any recursed once per level with no bound; the decode-time recursion limit cannot see that nesting, because Any is a flat two-field message whose chain lives inside its opaque value bytes. Past the cap, JSON serialization returns an error and textproto emits the unexpanded type_url/value form. The reflective DynamicMessage JSON codec gained the equivalent bound in #364.
ReflectMessage::to_dynamic no longer panics on views that decoded successfully (#341). Two causes: DynamicMessage charged two recursion levels for a message-valued map entry where the owned and view decoders each charge one, so a band of nesting depths decoded cleanly as a view and then panicked on conversion (437 bytes of google.protobuf.Struct was enough); and the view re-decode applied the fixed untrusted-input limits to bytes the library had just encoded. The map accounting now matches the other two decoders, and the view re-decode scales the element-memory and unknown-field bounds to the encoded length, floored at the defaults.
The reflective decoder now charges element memory for packed repeated scalars, packed and unpacked closed-enum elements, and closed-enum-valued map entries (#342). Generated code exempts packed scalars because a 1-byte varint becomes a 4-byte i32; DynamicMessage stores each element as a 64-byte Value instead, so the same wire bytes bought roughly 64x the memory with the 32 MiB bound attached but never consulted — 4 MB of wire produced 262 MB resident. Consequence for callers: a reflective decode of a large columnar payload (above ~524k packed elements under the default) now returns ElementMemoryLimitExceeded where it previously succeeded, and needs DecodeOptions::with_element_memory_limit raised.
The lazy view family now applies the element-memory budget (#343). Four gaps composed: generated decode_lazy built its context without one, neither the repeated nor the singular message arm charged, and each deferred access rebuilt a context that dropped it — so DecodeOptions::with_element_memory_limit had no effect on this family at all. Recording a deferred element or fragment is now charged for the byte-range slot it retains, and the budget remaining at the record site is replayed when the subtree is accessed. Like the recursion and unknown-field limits on this path, the element budget is a per-subtree bound rather than the eager decoder global cap; the rustdoc and guide say so, and the three affected # Errors lists now name ElementMemoryLimitExceeded.
The textproto parser now applies an element-memory budget (#344). DecodeContext never reaches this parser, so repeated and map fields were unbounded there while the identical binary payload was rejected — and the amplification is very nearly as large, an empty repeated element costing three input bytes ({},) against two encoded for the same footprint. This may reject input that parsed in earlier releases: textproto is a format people hand-write and check in, so a large fixture can now fail with element memory limit exceeded. Raise the bound to accept it.
An extension field decoded through MessageCodec or GroupCodec now shares one decode budget across all of its wire records, in either the singular or repeated form (#345, closes #357). Message::merge_from_slice mints fresh budget cells per call, so merging records one at a time gave every record its own full unknown-field and element-memory allowance — turning documented per-decode absolutes into per-record ones, multiplied by however many records the sender chose to send. The carrier sees only opaque records and charges one slot each, so nothing upstream noticed. Reachable from ExtensionSet::extension and, with no explicit call, from serde_json::to_string on a message carrying a registered extension. New failure mode: an extension whose records together exhaust the budget now reads as absent (or yields a short list), which the getter rustdoc now says. The budget is always the crate default — ExtensionSet::extension takes no context, so a DecodeOptions used for the carrier does not reach it.
The reflective base64 decoder's 32-bit size overflow and the any_encode_text debug assertion no longer panic on untrusted input (#346); on both paths the non-panicking behaviour was already correct. The base64 decoder sized its buffer with len * 3 / 4, which overflows usize on 32-bit targets (supported, with their own CI job) above ~1.33 GiB of attacker-supplied JSON; it now divides first, which cannot overflow at any length. And any_encode_text carried a debug_assert! that the stored Any.value bytes decode, so a single malformed byte panicked wherever debug assertions are on; that field is a raw bytes field that decode never validates, making undecodable content ordinary untrusted input rather than a broken invariant.
Reflective map decode is no longer quadratic in the number of wire entries (#348). MapValue is a sorted Vec and the binary decode path called its O(n) sorted insert once per entry — its own doc already said decode paths should batch instead, which the JSON path did and the binary path did not. Entries arriving in descending key order landed at index 0 every time, so a few megabytes of wire turned into minutes of memmove. Entries are now appended during the decode and sorted once at the end, including on the error path so a partially-decoded message never escapes with a broken sort invariant.
A fully-qualified symbol name is now capped at MAX_SYMBOL_LEN (512 bytes, matching protoc) (#347), which bounds two things in DescriptorPool construction — work that happens after decode and that none of the decode budgets cover. Names are built by concatenating the parent name, and every descendant stores its own copy four times over, so a deep prefix is paid once on the wire and once per descendant; a message name is a singular field, so the element budget never charged for it. The cap also bounds nesting: construction walks nested messages recursively in four places with no depth counter, each level adds at least one byte to the name, and the cap is reached in pass 1 before the other three walks run. That closes the unbounded recursion reachable through the public DescriptorPool::new, which takes an already-decoded set and so is not covered by the decoder recursion limit. The cap applies to registered symbol names — messages, enums, enum values, services, methods, extensions — not to leaf field or oneof names, whose amplification is linear rather than quadratic.
Nested unknown groups now charge element memory for the per-level Vec slot each child occupies (#349). The unknown-field count limit prices a flat field at its ~40-byte UnknownField, but a group allocates a Vec per nesting level on top of that, and Vec's minimum non-zero capacity is four elements — so a group holding one child costs ~160 bytes against one slot, and a deep chain spent several times what the count implied. The doc on DEFAULT_UNKNOWN_FIELD_LIMIT said the count caps overhead at roughly limit x 40 bytes; that holds for flat input and now says so, with nesting bounded by the two limits together.
ProtoJSON parsing for bytes extensions now accepts standard and URL-safe base64 with or without padding (#350), matching ordinary bytes fields and the protobuf JSON specification.
Replacing a JSON or text extension registry entry now removes the superseded name and number mappings (#351), keeping serialization and parsing lookups consistent.
Reflective DynamicMessage JSON integer decoding now matches generated messages (#353). Quoted decimal and exponent forms parse exactly across the full i64/u64 range (a quoted "9007199254740993.0" was previously rejected after a lossy f64 round trip), and unquoted integer-valued floats above 2^52 - 1 are now rejected, as the generated decoders have done since 0.9.0, because serde_json can round such tokens to an adjacent integer before the decoder sees them. Reflective decodes that relied on an unquoted value in that range must quote it. The reflective path now routes through the same json_helpers with-modules as generated code, so the two cannot drift apart again.
JSON null on a registered extension key unsets the extension (#355), for both singular and repeated extensions. Singular google.protobuf.Value and google.protobuf.NullValue extensions are the exception: null is a present value there. A NullValue extension now also serializes as null rather than the enum name "NULL_VALUE" ([null, …] when repeated), matching the reflective codec and ProtoJSON; the parse side accepts null, "NULL_VALUE", and 0. Generated extension registrations reach that behaviour through four new extension_registry::helpers functions (null_value_to_json, null_value_from_json, and their repeated_ forms), so code generated by this version needs a matching buffa.
Integer extensions now use the same ProtoJSON decoders as generated fields (#356). They accept exact quoted decimal and exponent forms across every integer wire type while rejecting unsafe high-magnitude unquoted floats.
Strict reflective Any JSON decoding now rejects unknown members alongside the value of a wrapped well-known type (#354); from_json_ignoring_unknown continues to discard those members.
Generated descriptor_pool() no longer rejects small schemas built from short type names (#360, closes #336). The scaled element-memory bound (len * 64) could come out tighter than the untrusted-input default it replaced: descriptor sets whose bulk is one-character type names amplify past 64x (the issue's shape measures ~71.5x on 64-bit), so a few-kilobyte schema was rejected even though the flat 32 MiB default before 0.9.1 accepted it. The bound is now floored at DEFAULT_ELEMENT_MEMORY_LIMIT, so it can only ever be looser than that default, never tighter.
Reflective-package codegen (ReflectMode::Bridge/ReflectMode::VTable) no longer scales one token per descriptor byte (#361). The embedded FILE_DESCRIPTOR_SET_BYTES constant is now written as a single byte-string literal instead of one token per byte, so its codegen cost no longer scales one token per descriptor byte (which can reach tens of megabytes for a large codegen run). Measured at roughly 20x less build time and 15x less peak memory generating one package against a ~22 MB encoded descriptor set. No change to the constant's type, name, or contents.
Reflective ProtoJSON parsing now honours ignore_unknown_fields for google.protobuf.Empty (#367), including Empty values packed in google.protobuf.Any.
Generated JSON serialization for message-valued oneof variants now dereferences the configured ProtoBox before passing the value to serde (#372). Custom ProtoBox types no longer need to implement serde::Serialize just because they are used in a oneof, and the custom-types example now exercises the ProtoBox contract without that workaround.
Generated Deserialize impls reach core::fmt through an absolute ::core:: path (#375). It was the one unqualified path the codegen emitted (on messages with a oneof, or with extension ranges under preserve_unknown_fields), so a proto package named core produced a module that shadowed the core crate and the generated code failed to compile with E0433: cannot find fmt in core.
OwnedView::into_bytes could drop the wrapped view twice if its destructor panicked (#379, closes #377). The method ran the view's destructor while self was still a live OwnedView, so a panic inside V::drop unwound into OwnedView's own Drop impl and dropped the view a second time. Generated view types have no Drop impl, so only a hand-written MessageView whose destructor can panic was exposed. OwnedView now has no Drop impl at all: it stores the view before the buffer, in a private MaybeDangling wrapper modelled on the yoke crate, and relies on declaration-order drop glue, so into_bytes is a plain field move and a panicking view destructor cannot be re-entered. The wrapper also hides the view's forged 'static borrows from the aliasing model, so dropping an OwnedView received by value — which Miri reported as undefined behaviour under field retagging — is now well-defined. Reported by HackerOne researcher waynezinn.
merge_bytes / merge_string no longer publish a length before the bytes behind it are written (#380, closes #378).
The non-contiguous copy path of merge_bytes, merge_string, decode_bytes, and decode_string called set_len(len) on the destination and then let Buf::copy_to_slice fill it. chunk() and advance() are safe, user-implementable methods, so a Buf that panicked mid-copy (or whose chunk() ran dry before remaining() said it would) left the caller-owned Vec<u8> / String with a length covering bytes that were never written — stale or uninitialised heap, and for String possibly non-UTF-8 — readable by safe code that caught the unwind. All four sites now copy into the spare capacity chunk by chunk and extend the length only once every byte is in place, so an unwind leaves the merged value empty and a Buf that runs dry panics with a message instead of spinning. Not reachable with the stock bytes implementations. Reported by HackerOne researcher bezumiya.
Generated JSON code uses absolute ::serde paths (#384). A proto package or message named serde put a mod serde in scope that shadowed the crate inside the generated custom message and oneof Serialize/Deserialize impls, so the generated code failed to compile.
DynamicMessage equality compares unknown fields by value, in arrival order, rather than by count alone (#388).
DescriptorPool rejects field and extension numbers in the implementation-reserved band 19000–19999, as protoc does (#389); only hand-built or synthesized descriptor sets can reach this. The band is named by the new buffa::encoding::FIRST_RESERVED_FIELD_NUMBER / LAST_RESERVED_FIELD_NUMBER constants and the rejection surfaces as the new PoolError::ReservedFieldNumber variant. Extension ranges that span the band load unchanged, matching protoc and protobuf-go.
ProtoJSON FieldMask conversion preserves leading underscores (#390). A path component such as _foo maps to Foo and back, matching the C++ and Go implementations; on the parse side a leading uppercase letter, previously rejected as non-canonical, now yields a leading underscore.
ProtoJSON FieldMask paths are validated the same way on the typed and reflective codecs (#391). Both now reject a component that is empty or contains a character outside [a-z0-9_] (whitespace, -, /), on parse and on serialize, as C++ and protobuf-go do; previously the reflective parser trimmed whitespace and the typed one kept it. Surrounding whitespace in a path is therefore an error where the reflective codec used to accept "foo, barBaz". buffa::json_helpers::wkt::field_mask_path_round_trips carries the new check for anyone calling it directly. The exact path * stays accepted as the full-mask wildcard.
ProtoJSON Any rejects a @type whose type name is empty (#402). "type.googleapis.com/" passed the "contains a /" check and went on to a registry lookup of the empty name; it is now rejected with the other malformed type URLs.
DescriptorPool rejects enum values that reuse a reserved name or number (#405), as protoc does. Only hand-built or synthesized descriptor sets can reach this; the rejection is the new PoolError::ReservedEnumValueName / ReservedEnumValueNumber variants, naming the enum and the value.
Textproto accepts a comment between - and a special float literal (#404). The tokenizer already allowed whitespace and comments between the sign and inf/infinity/nan, as the spec and C++ do, but the decoder only trimmed whitespace from the raw span, so f: - # note\n inf failed to parse.
Generated enums carry #[allow(non_camel_case_types)] (#409, closes #408). The module-tree wrapper emitted by buffa-build and protoc-gen-buffa-packaging already suppresses the lint, but a per-proto content file consumed on its own (mod/include! without the packaging plugin) warned on every SCREAMING_CASE variant. The attribute is now on each enum, as the scoped non_snake_case allow already is on messages.
DescriptorPool rejects two enum values with the same number unless allow_alias is set (#410), as protoc does; with allow_alias, lookup by number keeps returning the first declaration. Only hand-built or synthesized descriptor sets can reach this; the rejection is the new PoolError::DuplicateEnumValueNumber variant.
DescriptorPool rejects an open enum whose first declared value is not zero (#411), as protoc does for proto3 and editions enum_type = OPEN; closed enums (proto2, or editions CLOSED) may still start anywhere. Only hand-built or synthesized descriptor sets can reach this; the rejection is the new PoolError::OpenEnumFirstValueNotZero variant.
DescriptorPool rejects message fields that reuse a reserved name or number, and extension ranges that overlap a reserved range (#412), as protoc does. Only hand-built or synthesized descriptor sets can reach this; the new PoolError::ReservedMessageFieldName, ReservedMessageFieldNumber, and ReservedExtensionRange variants name the message and the offending field or range.
DynamicMessage::from_message, to_message, generated owned-mode to_dynamic, and bridge-mode reflect() no longer reject large messages the process built itself (#414, refs #413). Their internal re-decode applied the fixed untrusted-input element-memory and unknown-field budgets to bytes the library had just encoded, so a valid message a few megabytes on the wire panicked with ElementMemoryLimitExceeded; the budgets now scale with the encoded length, floored at the defaults, as the generated view conversion already did (#341). The recursion limit and the 2 GiB protobuf size limit still apply.
Reflective JSON serialization (DynamicMessage::to_json / serde_json::to_string(&dyn_msg)) now bounds nesting across google.protobuf.Any payloads (#364). DynamicMessage's Serialize impl decodes Any.value at serialize time and previously gave each layer a fresh recursion budget, so a sub-megabyte input of deeply nested Any messages — which the binary decoder accepts, since Any.value is opaque bytes to it — overflowed the stack (an uncatchable process abort). It now threads one RECURSION_LIMIT (100) count through nested messages and Any payloads alike, and the inner Any decode continues that count rather than restarting it, so over-deep input is a serde error (text contains exceeds buffa::RECURSION_LIMIT). A DynamicMessage whose nesting counted through Any exceeds 100 levels now fails to serialize; the cap is not configurable in this release. The generated-code buffa_types::Any serializer is covered by #339.
View and reflective decoders now merge a repeated message value within one map entry, matching the owned decoder (#365). For a map<K, SomeMessage> field, protobuf merge semantics say that if a single map entry carries its value field more than once, the occurrences merge into one message. The owned decoder did that, but generated *View types (including lazy views and buffa_types::StructView) and DynamicMessage kept only the last occurrence — a parser differential that let a crafted message show different map contents to a view-based check than to owned decoding or JSON transcoding of the same bytes. All decoders now merge. No conforming encoder emits a split value (message concatenation produces separate entries, which are unaffected), so only hand-crafted input decodes differently, and it now decodes the same way everywhere.
ci: publish every publishable crate, and fail when one is missed by @iainmcgin in #323
Full Changelog: v0.9.0...v0.9.1
A patch release, driven by a regression in 0.9.0. The element-memory bound that release introduced is sized for untrusted wire input, and buffa's own tooling applied it to the descriptor sets it reads — so codegen began rejecting schemas of a few hundred .proto files with element memory limit exceeded. Descriptor types are wide structs, giving a measured element footprint of roughly 6.3x the encoded size, so the 32 MiB default worked out to about 5 MB of descriptors. Tooling paths now decode under a 1 GiB bound. DEFAULT_ELEMENT_MEMORY_LIMIT is unchanged, so the protection for untrusted input is exactly as it was.
Regenerate your code with a version-matched buffa-codegen. The generated descriptor_pool() accessor now scales its decode bound to the descriptor bytes embedded in it; code generated by 0.9.0 keeps the old bound and the failure that comes with it. The buffa-build and protoc plugin fixes take effect on rebuild without regenerating.
API changes are additive only — DescriptorPool::decode_with_options and the new buffa-codegen decode helpers — so this is a drop-in upgrade from 0.9.0. MSRV is unchanged at 1.75.
UnknownFieldsView holds its records behind a pointer, so a view carrying none costs a null (#329).
Every generated view embeds this type by value. Holding the records inline made each view an owning aggregate, which the compiler moves with out-of-line memcpy calls rather than inline vector stores, and that cost view-decode throughput even on payloads that never carry an unknown field. The state now lives behind a Box, allocated when the first unknown record arrives.
Measured on google_message1_proto3/decode_view on quiesced bare metal, as the mean of four code-alignment layouts and reproduced on two CPU generations: a payload with no unknown fields decodes ~10% faster, and one carrying them at every nesting level ~2% slower, since each such view pays an allocation. A payload whose unknown fields all sit in one view falls between the two. Public API, wire behaviour and Debug output are unchanged.Field-scoped open-enum overrides now flow through FieldDescriptor into reflective binary and JSON decoding, so DynamicMessage preserves unknown enum values in singular, repeated, oneof, and map fields exactly like generated decoders. (#316)
Descriptor decoding in buffa's own tooling no longer applies the untrusted-input element-memory bound, which rejected schemas of a few hundred .proto files with element memory limit exceeded. buffa-build and the protoc plugins now decode under a 1 GiB bound, overridable with the element_memory_limit plugin option or the BUFFA_ELEMENT_MEMORY_LIMIT environment variable; generated descriptor_pool() scales its bound to the descriptor bytes compiled into it. New DescriptorPool::decode_with_options lets any caller supply decode limits; DescriptorPool::decode stays bounded, since it may be handed an untrusted descriptor set. (#331)
Documentation: DecodeOptions bounds the binary codec only, and now says so (#330).
The 0.9.0 notes for with_element_memory_limit said "the owned, view and reflective (DynamicMessage) decoders are all bounded", which is true and was easy to read as codec-independent. It is not: JSON decoding runs serde_json straight into the generated Deserialize impls, which never receive a DecodeOptions, so a message parsed from JSON is bounded by none of the limits that bound the same message parsed from protobuf — and the element amplification is nearly as large there, {} being three JSON bytes for the footprint that costs two on the wire. The rustdoc on DecodeOptions and with_element_memory_limit, and the guide's decode-options section, now state the scope; the guide's table also lists with_element_memory_limit, which it had omitted. Bounding the JSON path is tracked in #330; no behaviour changes in this release.
changelog: adopt changie fragment files to end CHANGELOG merge conflicts by @iainmcgin in #230
jiff feature (Timestamp/Duration conversions) by @macalinao in #264Full Changelog: v0.8.0...v0.9.0
This release adds explicit limits on what encoding and decoding may produce and allocate, makes decoding substantially faster, and fixes a range of reflection, JSON, and codegen bugs. There are breaking changes in both the API surface and in code generation — when upgrading, regenerate all code with a version-matched buffa-codegen.
Encoding now enforces the protobuf specification's own 2 GiB ceiling on every entry point, where it previously wrapped silently past 4 GiB and produced a corrupt message. On top of that default, try_encode_bounded takes a budget of your choosing and answers "will this message fit my frame" before writing a byte, in a single size pass rather than two. Together these are the encode twin of DecodeOptions::with_max_message_size, which has always bounded the size of a message coming off the wire.
For decoding, a further memory-amplification attack was reported — #301, a variant of the CVE-2026-55407 advisory but on expected fields rather than unknown ones. It is now mitigated through DecodeOptions::with_element_memory_limit, defaulting to 32 MiB. This will break your existing workloads if you expect and accept large numbers of absent messages within repeated fields. If a child message's struct is ~200 bytes, an absent instance of it inside a repeated field costs 2 bytes on the wire and expands to that ~200-byte allocation on decode — a 100x amplification. Millions of those 2-byte entries fit inside a perfectly reasonable 4 MiB encoded message: 4 MiB of them is 2,097,152 elements and roughly 400 MiB of allocation. with_element_memory_limit bounds the memory such fields may consume, charging each element's own footprint so that a 20-byte struct and a 200-byte struct are each handled on their merits. It complements with_unknown_field_limit rather than subsuming it — the two limits are applied separately, and map entries and view decoding are charged the same way. Adjust either default if it does not meet your needs.
Multiple changes in this release improve decode performance:
Together the packed-decode changes cut decode latency by 36% on a 1024-element columnar batch and 16% on shorter packed arrays, primarily through fewer allocation calls and better inlining; restoring view inlining is worth up to 32% on view decoding. Results depend heavily on code layout. Our benchmarks build with lto = true, codegen-units = 1, and -Cllvm-args=-align-all-nofallthru-blocks=6 -Cllvm-args=-align-loops=64 in an attempt to get consistent results, and even then there is a reproducibility floor of roughly ±5%. We have watched a hot loop move ~20% with byte-identical machine code, purely from where it landed relative to a cache line — so keep that in mind for your own benchmarking and production builds.
Each of these has fuller migration notes in the section below.
WirePayload is now opaque — a struct with private fields and WirePayload::borrowed(..) / ::owned(..) constructors, in place of the 0.8.0 Borrowed(&[u8]) / Owned(Bytes) enum. The reason is that a variant holding exactly the field's bytes cannot see the wire buffer around them, which is what the slack-aware UTF-8 validator needs to take its fast path; the opaque payload carries that tail, so to_str now reaches the validator on the custom-ProtoString decode path — the one string site the 0.8.0 UTF-8 work missed. Code that only calls the accessors is unaffected. Constructing a payload lowercases to ::borrowed(..) / ::owned(..), and matching on the variants moves to the accessors, with is_owned() covering the take-the-Bytes-only-when-free pattern.MessageField<T, ::buffa::Inline<T>>, laid out as Option<T>, so a singular submessage no longer costs a heap allocation per field. Recursive fields are detected and stay boxed automatically. Reading and writing fields through the MessageField API is unchanged, so most code that touches the structs needs no edit at all; what breaks is an explicit MessageField<Foo> type annotation, which still names the boxed form and will now mismatch the field's declared type. Drop the annotation and let the representation infer. Note the tradeoff this makes: an unset inline field costs size_of::<T>() where a boxed one cost a pointer, so for a large submessage that is usually absent, box_type_in(PointerRepr::Box, &[".pkg.Msg.field"]) restores the old behaviour per field.OwnedView::to_owned_message is now infallible. It became fallible in 0.8.0 as a deliberate part of the CVE-2026-55407 fix, which put view-to-owned conversion under the decode-time limit; 0.8.1's accounting fix then made that error unreachable for any wire-decoded view, so the Result is now dead weight. Delete the ? / .unwrap() at call sites whose receiver is an OwnedView or a generated FooOwnedView. Plain view types (FooView::to_owned_message) stay fallible, because hand-written impls and push_raw-built views can still legitimately fail.&mut impl EncodeSink instead of &mut impl BufMut, so that a sink can flush segments without copying them. Callers passing Vec<u8>, BytesMut, or any other BufMut are source-compatible through the blanket impl and need no change; a hand-written Message / ViewEncode impl updates its signature, and one that reached for BufMut methods beyond the encoders' own subset (put_u8, put_slice, the little-endian fixed-width writers) must assemble into a concrete buffer first.u64 — types::put_len_delimited_header, and map_codec::field_len / message_field_len. This is what makes the 2 GiB ceiling enforceable: sizes have to accumulate in a type that cannot wrap before anything can check them against a limit. Bare integer literals still infer, and a u32 variable widens with u64::from(..), so hand-written call sites are usually a small edit or none. Checked-in generated code will not compile until it is regenerated — code from earlier buffa-codegen passes a u32 into put_len_delimited_header and accumulates field_len into a u32. An external ExtensionCodec impl also swaps its required method to the fallible try_encode / try_encode_one.MSRV remains 1.75.
WirePayload is now an opaque struct. The 0.8.0 public-variant enum (Borrowed(&[u8]) / Owned(Bytes)) is replaced by a struct with private fields, the same accessors (as_slice, to_str, into_bytes, plus new len / is_empty / is_owned), and WirePayload::borrowed(&[u8]) / WirePayload::owned(Bytes) constructors. ProtoString / ProtoBytes from_wire implementations that use the accessors are unaffected; code that constructed WirePayload::Borrowed(..) / ::Owned(..) migrates by lowercasing to ::borrowed(..) / ::owned(..); code that matched on the variants moves to the accessors — is_owned() covers the take-the-Bytes-only-when-free pattern. The reshape lets a borrowed payload carry the surrounding wire-buffer tail, so to_str now reaches the slack-aware UTF-8 validator on the custom-ProtoString decode path (it was the one string decode site #241 didn't cover).
Singular message fields are now stored inline by default: codegen emits MessageField<T, ::buffa::Inline<T>> (laid out as Option<T>, no per-field heap allocation) for every non-recursive field. Recursive fields are detected and stay on Box automatically. (#248)
To restore the old behaviour for specific fields (e.g. large or rarely-set submessages), use box_type_in(PointerRepr::Box, &[".pkg.Msg.field"]); for the old global default, box_type(PointerRepr::Box). Explicit MessageField<Foo> type annotations now mean the boxed form and will mismatch the new default — drop the annotation and let P infer from the field's declared type (or, for a standalone value with no pinning context, write MessageField::<Foo, buffa::Inline<Foo>>::some(x)).
box_type_in / box_type_custom_in now normalize a missing leading dot on each path; previously a dotless path silently matched nothing.
OwnedView::to_owned_message and the generated FooOwnedView::to_owned_message are now infallible, returning the owned message directly instead of Result<_, DecodeError>. Every OwnedView constructor wire-decodes its view (or, for unsafe from_parts, requires wire-decode provenance as part of its strengthened safety contract), and since 0.8.1 a view produced by wire decoding always converts — so the Result only ever encoded an unreachable error path. Migration: on call sites whose receiver is an OwnedView or a generated FooOwnedView handle, delete the ? / .unwrap() / .expect(...) — or otherwise unwrap the previously-returned Result (a match or .map_err(...) needs the same treatment). Call sites on plain view types (FooView::to_owned_message via the MessageView trait) are unchanged and stay fallible, since hand-written impls and push_raw-built views can still legitimately fail. unsafe from_parts callers: the safety contract now requires that the view was produced by wire-decoding the buffer, not merely that its borrows point into it — a hand-assembled view that was legal under the old wording still compiles but now panics at to_owned_message, so audit from_parts call sites for provenance. A contract violation by a buggy hand-written MessageView impl wrapped in OwnedView likewise panics with a descriptive message instead of surfacing an error that correct code could never observe. (#268)
The u64 size-arithmetic discipline changes three public signatures: buffa::types::put_len_delimited_header takes len: u64 (was u32), and buffa::map_codec::field_len / message_field_len take and return u64 (MapCodec::encoded_len and FIXED_LEN likewise — that trait is sealed, so only the signatures are visible). Bare integer literals still infer; a u32 variable widens with u64::from(...). Checked-in generated code must be regenerated: code emitted by earlier buffa-codegen versions passes __cache.consume_next() (a u32) to put_len_delimited_header and adds field_len results into a u32 accumulator, so it fails to compile against this runtime. Regenerate with your build pipeline (or buffa-build) after updating. ExtensionCodec and extension::codecs::SingularCodec swap their required encode method: try_encode / try_encode_one (fallible) are now required, and the panicking encode / encode_one are provided wrappers — so a codec whose encode can fail cannot accidentally leave the fallible try_set_extension path panicking. All in-tree codecs are updated; an external codec impl (if any exist) renames its method and wraps the result in Ok. Runtime behavior also changes: the existing encode entry points now panic on messages whose encoded size exceeds the 2 GiB protobuf limit — see the Fixed entry for the full list and the try_encode* escape hatch.
Message::write_to/encode (and ViewEncode, the types::put_*/encode_* helpers, and generated code) now take &mut impl EncodeSink instead of &mut impl BufMut. Callers passing Vec<u8>, BytesMut, or any other BufMut are source-compatible via the blanket impl; manual Message/ViewEncode implementations must update their method signatures, and generated code must be regenerated with the matching codegen version. Note that EncodeSink deliberately exposes only the BufMut subset the encoders use (put_u8, put_slice, and the little-endian fixed-width writers) — a manual write_to that used other BufMut methods must assemble into a concrete buffer first. Generated write_to bodies now emit put_shared_bytes_field for bytes fields — copy-equivalent for Vec<u8>, segment-aware for bytes::Bytes.
DescriptorPool now implements Clone. (#253)
ReflectMessageMut::try_set / try_clear: checked mutation variants that return ReflectError::FieldNotMember instead of mutating when handed a FieldDescriptor from a different message or pool. (#254)
idiomatic_field_names codegen option (buffa_build::Config, CodeGenConfig, protoc plugin): opt-in conversion of camelCase proto field and oneof names to snake_case Rust identifiers, prost-compatible on underscore-free names; wire, JSON, and text-format names are unchanged, and collisions resolve deterministically with an _f<number> suffix and a build warning. Generated structs with non-snake member names now carry a detection-scoped #[allow(non_snake_case)] regardless of the option. (#260)
jiff feature for buffa-types (#264). Adds conversions between the well-known types and jiff: Timestamp ↔ jiff::Timestamp and Duration ↔ jiff::SignedDuration. Gated behind the new jiff Cargo feature and no_std-compatible (jiff is pulled with default-features = false + alloc). Note for chrono users switching over: the Duration → jiff::SignedDuration conversion has no Overflow error mode (SignedDuration stores full i64 seconds, so a well-formed proto Duration can never overflow it) — code that relied on DurationChronoError::Overflow to reject huge durations must check the proto JSON spec bound (±315,576,000,000 s) itself, or let JSON serialization enforce it.
exclude_package protoc plugin option (protoc-gen-buffa and protoc-gen-buffa-packaging): drop a proto package and its subpackages from generation and from the emitted mod.rs. Repeatable; the leading dot is optional. Intended for option-only imports that include_imports pulls into file_to_generate but that are never referenced as message fields (e.g. buf.validate, gnostic). Both plugins route exclusion through the new buffa_codegen::package_is_excluded predicate, so their output sets stay in sync. (#279)
MessageField<T, P> now has a consuming map combinator, equivalent to into_option().map(f), for Option-style transformation of optional message fields. (#280, #282)
Vectored ("rope") encode: the new EncodeSink trait abstracts the encode output, and the Rope sink captures large bytes::Bytes fields (and, via Rope::with_backing, large borrowed view fields) as reference-counted segments instead of copying them — encoding a message dominated by one large payload costs O(header) instead of O(payload). Contiguous callers are unaffected: every BufMut is an EncodeSink through a blanket impl. ProtoBytes gains a provided as_shared method so custom Bytes-backed representations can opt in, and RopeBuf adapts a finished rope to bytes::Buf with vectored-I/O support.
Package-root FILE_DESCRIPTOR_SET_BYTES re-export (#278). The embedded descriptor-set bytes were only reachable through the reserved __buffa sentinel module; generated packages now re-export the constant at the package root alongside descriptor_pool(), making pkg::FILE_DESCRIPTOR_SET_BYTES the supported access path.
buffa-remote-derive: derive macros for newtypes over remote types (#212, #251). New proc-macro crate with ProtoString, ProtoBytes, ProtoList, ProtoBox, and MapStorage derives that generate the buffa owned-type trait impls (plus Deref/AsRef/From conversions) for a single-field newtype wrapping a foreign type, mirroring serde's remote attribute pattern. Covers the binary codec only; serde/Arbitrary/reflection forwarders remain hand-written. Contributed by @rsd-darshan.
Path-scoped editions feature overrides via override_feature_in(path, FeatureOverride) — descriptor feature injection for integrators working with protos they cannot modify, applied as if the proto had been migrated to editions with that feature set at the matched paths. The supported override set is the FeatureOverride allowlist enum; the first entry is enum_type:OPEN, with open_enums_in(&[...]) as shorthand: selected closed enums (or individual closed enum fields) generate as EnumValue<E>, making unknown wire values directly visible as EnumValue::Unknown(n) for prost-parity migrations or memory-optimized builds with preserve_unknown_fields(false). Enum-type rules flow into the embedded reflection descriptor pool, so runtime reflection and dynamic JSON agree with the generated types. Rules that match nothing produce a generation-time warning. Default-off; existing output and semantics are unchanged. (#269)
buffa-remote-derive: optional as_shared override on derive(ProtoBytes) (#294). #[buffa(remote = ..., as_shared = path)] generates the encode-side ProtoBytes::as_shared hook, letting a remote bytes newtype that stores a bytes::Bytes splice into segmented (Rope) sinks by reference count instead of being copied. Without the key the trait default (None, copy) still applies.
DecodeOptions::with_element_memory_limit bounds the memory a decode materializes in the elements of length-delimited containers — repeated message, string and bytes fields, and map entries — defaulting to 32 MiB (DEFAULT_ELEMENT_MEMORY_LIMIT) where it was previously unbounded (#301). These elements cost far more decoded than encoded, which no existing option bounded: an empty repeated message element is 2 bytes on the wire and size_of::<T>() in the Vec it lands in — measured at 256 bytes for a message of a few Vec/String fields, a 128x ratio, so 4 MiB of them forced ~512 MiB. with_max_message_size cannot help, because it bounds the bytes going in rather than what they expand into. Empty bytes and string elements amplify 16x and 12x by the same route, and a map<string, Message> entry amplifies like a repeated message: an omitted value still materializes, and a few bytes of key buy a distinct slot. The budget is charged by element footprint and shared across the whole decode tree, so a limit means the same amount of memory whatever the element size, and nesting cannot multiply the ceiling. The owned, view and reflective (DynamicMessage) decoders are all bounded. Packed scalar fields are never charged: their worst case is a 1-byte varint becoming a 4-byte i32, and bounding them would reject columnar payloads that carry millions of elements by design. Lazy views are not charged either — they borrow byte slices at decode and materialize on access. This can reject input that previously decoded. A decode materializing more than 32 MiB of such elements now fails with DecodeError::ElementMemoryLimitExceeded; raise the budget with with_element_memory_limit for trusted input that legitimately decodes into more. Note Vec grows by doubling, so peak resident memory can reach roughly twice the budget — the budget bounds what is materialized, not what the allocator reserves.
Budget-checked encode entry points (try_encode_bounded, try_encode_view_bounded).
Message::try_encode_bounded(max_bytes, buf) and ViewEncode::try_encode_bounded perform a single size pass (populating the SizeCache) and reject the encode before write_to runs if the encoded size exceeds the caller-supplied budget, so nothing reaches buf on Err — including bytes a caller had already framed into it. SizeCachePool::try_encode_bounded and try_encode_view_bounded are the pooled equivalents, and both traits also carry a *_with_cache form for callers that manage their own SizeCache. All of them return the encoded body length on Ok, saving a second try_encoded_len call when the length is needed for metrics or framing.
A new EncodeError::ExceedsBudget { len, max_bytes } variant carries the exact encoded size and the budget that was exceeded. EncodeError::MessageTooLarge takes precedence when the message also exceeds the 2 GiB protobuf limit.
smoothutf8 bumped to 0.2.3. The decode hot path picks up the 0.2.1–0.2.3 performance work: verify_with_slack ASCII improves 38–64% at 1–32 bytes (inline-partition fix) and 28–39% at 32–512 bytes on aarch64 (NEON ascii_skip), and the safe verify tail rewrite improves 1–4-byte inputs up to 5.9×. wasm32 targets now run the portable shift-DFA validator instead of delegating to core::str::from_utf8. buffa's call site moves from the deprecated to_str alias to its new name from_utf8; behavior is unchanged.
Documented the struct evolution policy for generated types: exhaustive struct literals and destructuring of generated message/view structs are not covered by semver. See the Message trait docs. (#202, #244)
Tag::new is now #[inline], allowing the per-field-write call in generated write_to code to inline in optimized builds without LTO (cargo's default release profile). Measured up to ~13% fewer encode instructions on encode-heavy workloads. No API change. (#257)
Packed varint-family repeated fields (int32/int64/uint32/uint64/sint32/sint64/bool, and open enums) now decode each element through new force-inlined buffa::types::decode_*_packed helpers, removing the per-element out-of-line call from the generated packed loops. Measured on a quieted bare-metal A/B at the layout-normalized profile: −16–30% on packed-varint-dense decode/merge and −5–19% on decode_view across every benchmark shape (via the resulting decode_varint code-placement shift), at the cost of ~+2–4% on one dense-small-message synthetic (google_message1 owned decode/merge). Pair regenerated code with a buffa runtime that has the decode_*_packed helpers — code generated by this buffa-codegen does not compile against older runtimes, so a stale lockfile resolving an older caret version fails with cannot find function decode_*_packed; cargo update -p buffa fixes it. The plain decode_* helpers are unchanged.
source_code_info is stripped from the embedded FileDescriptorSet (#278). Codegen consumes source info for doc comments, but the runtime DescriptorPool never reads it, so embedding it only cost binary size — the well-known-types package shrank from 34,939 to 2,428 embedded bytes (-93%). Classified as non-breaking because the bytes were previously only reachable through the reserved __buffa sentinel module — but note the content change is silent: code that reached in anyway and forwarded the bytes to something that reads proto comments will see them disappear without a compile error. Consumers that need proto comments at runtime should build a descriptor set directly with protoc --include_source_info or buf build.
Docs: document the idiomatic .into() conversions for MessageField and EnumValue, and explain why message types are generated from .proto rather than derived. (#249)
Packed fixed-width payloads (fixed32/sfixed32/float, fixed64/sfixed64/double) now decode in one bulk call (buffa::types::extend_packed_*): the payload length is validated against the element width up front, the exact element count is reserved, and elements convert via chunks_exact + from_le_bytes, which optimizes to a bulk copy on little-endian targets. Applies to view decode and to owned decode into the default Vec when the payload is contiguous in the current chunk; fragmented buffers and custom list representations keep the per-element loop. Behavior note: on those bulk paths a misaligned (truncated) packed payload now fails without partially extending the field — previously the leading complete elements were pushed before the error; the error itself is unchanged (DecodeError::UnexpectedEof), and the per-element fallback paths keep the old partial-extension behavior.
Packed plain-varint payloads (int32/int64/uint32/uint64/sint32/sint64/bool) now decode through slice-specialized bulk extenders (buffa::types::extend_packed_*): one pre-allocation reserve up front (each decode path keeps the policy it had with the per-element loop: views reserve the exact count from count_varints, owned decode reserves the payload byte length as an upper bound), then an element loop with the per-element buffer dispatch hoisted out — 1-2-byte varints (the common case for ids, tokens, and small values) decode inline with no per-element chunk or remaining checks, longer elements fall through to the unrolled slice decoder. Applies to view decode and to owned decode into the default Vec when the payload is contiguous; fragmented buffers, custom list representations, and enums keep the per-element loop. A payload whose final byte has its continuation bit set (malformed trailing element) falls back to the per-element decoder, so accept/reject behavior and partial-extension semantics on malformed payloads are unchanged.
DescriptorPool now rejects descriptor sets with ambiguous field identities instead of building a pool whose lookups are undefined: two fields in a message sharing a number or a proto name, and a oneof_index that names no oneof in the containing message. The last of these also fixes a real bug — an out-of-u16 index was silently dropped, leaving the field with oneof presence but no oneof membership. Fields resolving to the same JSON name are rejected only where protobuf treats JSON as fully supported (json_format = ALLOW, i.e. proto3 and editions) and the message has not set deprecated_legacy_json_field_conflicts, so every descriptor set protoc emits still loads. protoc rejects the rest at compile time. (#300)
DescriptorPool now registers messages, enums, services, methods, extensions, and enum values in one symbol table, so a descriptor set that declares the same fully-qualified name twice is rejected at construction rather than building a pool where one declaration shadows the other. This catches collisions the pool previously accepted — a service and a message sharing a name, a duplicate RPC method, a duplicate enum value name — all of which protoc rejects at compile time. Numeric enum aliases under allow_alias are unaffected. (#302)
View decoding is laid out for payloads that match the schema: the routes that preserve an unrecognized field or enum value are marked #[cold]. Behaviour is unchanged. (#276)
DescriptorPool::add_file_descriptor_set is now transactional: a failed add (e.g. an unresolvable type name) leaves the pool unchanged instead of retaining placeholder descriptors and name entries that made a corrected retry fail with DuplicateName. (#253)
DynamicMessage::set / clear now validate field-descriptor membership in release builds and panic on a foreign descriptor, instead of silently writing to whatever field shares its number. Previously the check was a debug_assert! only. (#254)
JSON: quoted integer strings in decimal/exponent notation (e.g. "9007199254740993.0", "9.007199254740993e15") now parse exactly instead of routing through f64, which silently rounded magnitudes above 2^53. Non-integral and overflowing strings are still rejected. (#255)
Encode now enforces the protobuf 2 GiB message-size limit. Previously encoding was infallible: a message whose encoded size crossed 2^31-1 bytes serialized silently into a blob that no conforming decoder — including buffa's own (DecodeError::MessageTooLarge) — would read back, and sizes past 4 GiB wrapped u32 arithmetic into corrupt output. Generated compute_size now accumulates in u64 and saturates at each node's return (buffa::saturate_size), and every provided encode entry point on Message, ViewEncode, generated lazy views, and DynamicMessage checks the total against the new buffa::MAX_MESSAGE_BYTES constant. Behavior change: the existing entry points (encode, encode_to_vec, encode_to_bytes, encode_length_delimited, encoded_len, encode_with_cache) now panic on over-limit messages (previously they returned decoder-rejected or corrupt bytes); new try_encode, try_encode_with_cache, try_encoded_len, try_encode_length_delimited, try_encode_to_vec, and try_encode_to_bytes twins return Err(EncodeError::MessageTooLarge) instead — EncodeError gains its first variant. Fallible variants also cover the eager re-encode paths: ExtensionSet::try_set_extension and Any::try_pack (message-typed extension values and Any::pack encode their payload on the spot, so the panicking originals document the new panic and these twins return the error instead). In debug builds SizeCache::consume_next additionally rejects over-limit slots as a backstop for callers driving compute_size/write_to directly.
DynamicMessage::try_set now rejects Values whose runtime shape does not match the target field descriptor — repeated elements, map keys and values, and nested messages of the wrong type included — and set consequently panics on shapes it previously accepted and encoded as corrupt tag-only wire output. Invalid values planted through direct mutable field access are omitted during encode (whole field, with encoded_len in agreement) instead of corrupting the wire. (#272)
64-bit integer JSON helpers now reject unquoted decimal/exponent numbers at or above 2^52 in magnitude. serde_json's default float parsing is not correctly rounded, and from 2^52 a one-ulp parse error is a whole integer — an unquoted integer token there could silently decode to the adjacent i64/u64. Below 2^52 the same error breaks integrality and is rejected loudly by the existing exactness check. Quote large integer values to parse them exactly. (#274)
buffa-build Protoc mode now emits cargo:rerun-if-changed for imported .proto files from the generated descriptor set when they resolve under configured include roots, so editing transitive imports reruns codegen without requiring a clean build. (#275)
Editions LEGACY_REQUIRED fields with an explicit default = ... now honor the declared default consistently in Default::default(), clear(), and reflection presence checks. Previously clear() reset such fields to the type default instead of the declared default.
Reflective set adopts message values from another descriptor pool (#297). A generated type reflects against its defining crate's DescriptorPool, so a nested well-known-type (or any cross-package) message reached through the ReflectMessage vtable could never be pointer-identical to the referencing pool. DynamicMessage::try_set rejected such values with a self-contradictory error (expects message google.protobuf.Duration, got message google.protobuf.Duration), which made the rebuild walk for_each_set + set(fd, vr.to_owned()) panic for any message with a WKT field. Same-typed values from a foreign pool are now re-homed into the target pool by a wire round-trip; a genuinely wrong type is still rejected. Callers that relied on the previous cross-pool rejection as a provenance check must compare ReflectMessage::pool themselves. Adds MapValue::into_entries.
Code fences and indented code blocks in proto comments are no longer compiled as doctests in the consuming crate, so a comment carrying a non-Rust snippet no longer breaks cargo test downstream. An unannotated fence becomes text and any other keeps its language and gains ignore, so a rust fence is still syntax-highlighted. Tilde fences are handled like backtick fences, and a fence the author left open is closed at the end of the comment instead of swallowing the doc text after it. (#307)
Restore cross-crate inlining for generated view field decoding (#298). Generated merge_view_field methods now carry an inline hint, allowing the optimizer to avoid a per-field call on zero-copy view workloads.
Owned decoding now counts preserved unknown closed-enum values against the configured unknown-field limit, matching view decoding and preventing packed values from bypassing the allocation guard.
The reflective decoder (DynamicMessage) no longer drops an existing singular message field when a later wire occurrence of that field fails to decode. A truncated length varint or an over-long declared length left the field cleared, silently discarding data decoded from earlier occurrences; the partially-merged value is now retained, matching the owned decoder's merge semantics. (#299)
The reflective decoder (DynamicMessage) no longer clears a oneof's active member when the replacement on the wire fails to decode. A malformed varint, a truncated nested message, a mismatched group terminator, or invalid UTF-8 left the oneof empty, discarding the member that had decoded successfully; siblings are now cleared only once the replacement decodes. (#303)
The reflective decoder (DynamicMessage) now routes unknown values of a closed enum to unknown fields instead of materializing them as Value::EnumNumber, matching the generated decoder and the proto2/editions spec. Singular, oneof, extension, unpacked repeated, packed repeated, and map-value contexts are all covered, and each preserved value is charged against the decoder's unknown-field allowance on the same terms as the generated path — once per value, or once per entry for maps. Open enums are unaffected. (#304)
release: v0.8.1 by @iainmcgin in #267
A single-fix patch release: unknown-field limit accounting for zero-copy views now matches owned conversion exactly, establishing the guarantee that a view which decodes successfully always converts to an owned message. No API changes, and regenerating code is not required — the fix lives in the runtime.
with_recursion_limit).
Decode-time accounting now matches what to_owned_message re-materializes,
which gives views a guarantee: a view produced by decode_view always
converts via to_owned_message without error (the Result remains for
hand-written impls and push_raw-built views). Behavioral tightening:
payloads whose unknown-field count exceeds the limit but previously slipped
through view decode via span coalescing — e.g. a ~2 MiB run of >1M
contiguous 2-byte unknown records under the default limit, or an unknown
group with more nested fields than the limit — now fail at view decode with
UnknownFieldLimitExceeded. Consumers that converted such views already
got this error at conversion; view-only consumers that re-encoded such
payloads without converting (e.g. a zero-copy passthrough proxy) now see it
at decode — raise the bound with DecodeOptions::with_unknown_field_limit
if such payloads are trusted and expected. The accounting lives in the
runtime (UnknownFieldsView::push_record), so previously generated code is
fixed without regeneration. (#266)ci: fix protoc plugin release build under the pinned toolchain by @iainmcgin in #182
task lint by @iainmcgin in #221Full Changelog: v0.7.1...v0.8.0
The headline of this release is that the owned representation of every field
kind is now pluggable end to end: string, bytes, repeated, singular
message, and map fields each accept a crate-local type via a small
from_wire-style trait (ProtoString, ProtoBytes, ProtoList, ProtoBox,
MapStorage), so an inline-string or small-vector representation can avoid
the per-field heap allocation without giving up the generated codec. Alongside
that, UTF-8 validation on the decode path now defaults to
smoothutf8 with the slack-buffer fast path —
view decode is +15–22% on the string-heavy benchmark messages — and an opt-in
FooLazyView family lets a caller decode a few fields of a large message
without recursing into untouched sub-trees. There are eight breaking changes,
all on the trait surface or generated-code shape; regenerate code with the
matching buffa-codegen, then the convenience entry points (decode,
decode_from_slice, merge_from_slice, DecodeOptions) are unchanged.
WirePayload::to_str (#241) — borrow the payload as a &str if valid
UTF-8, using buffa's UTF-8 validator (so it picks up fast-utf8).
Convenience for ProtoString::from_wire implementations; buffa-smolstr
now uses it.
HasMessageView::decode_view / decode_view_with_options (#240) —
defaulted methods so generic code bounded on M: HasMessageView can write
M::decode_view(buf) instead of the associated-type path
<M as HasMessageView>::View::decode_view(buf). Additive; the existing
MessageView::decode_view is unchanged.
MessageField::unwrap / expect and From<MessageField<T, P>> for Option<T> (#240) — consume a MessageField directly (field.unwrap(),
field.expect("…"), field.into()) without the .into_option().unwrap()
round-trip. Both are #[track_caller] and panic on an unset field; prefer
ok_or / ok_or_else for fallible contexts. Additive.
buffa::SizeCachePool — opt-in reuse of the encode size-cache spill
allocation (#225). Every encode / encoded_len builds a fresh
SizeCache; its inline storage is free, but a message with more than the
inline capacity of nested length-delimited sub-messages (deeply nested,
repeated-sub-message shapes) spills to a heap Vec on every encode.
SizeCachePool is a caller-owned free-list of those spill buffers — keep one
in a thread_local! or a request/connection context and call pool.encode,
pool.encode_view, or pool.encoded_len to reuse one allocation across many
encodes. buffa holds no global state; only the spill Vec is pooled (each
cache's inline array stays on the stack), so routing small messages through a
pool costs only a Vec pop/push of an empty buffer — no allocation, no
thread-local, no synchronization — and the pool is alloc-only (no_std-OK).
Bounded by max_buffers (free-list length) and max_capacity (per-buffer
capacity, shrunk on return). Also adds SizeCache::with_spill_buffer /
into_spill_buffer to source/sink the spill buffer for manual reuse. Additive
and non-breaking; the default encode path is unchanged.
Custom owned string types for map keys and values (#222). A string_type
rule (string_type_custom / string_type_custom_in) now also applies to a
map<string, V> key and a map<K, string> value — one rule on the map field
path covers both slots of a map<string, string> — mirroring how bytes_type
already reaches map<K, bytes> values. The element decodes/encodes through the
new sealed buffa::map_codec::ProtoStringMap<S> codec; no new build knob. The
wire format is unchanged and view types still borrow &str. Requirements on
the custom type when used in a map: Hash + Eq (or Ord for
map_type(BTreeMap)) for a key; serde::Serialize / Deserialize for JSON;
and — because the map paths have no per-key generic shim — a crate-local
newtype (vtable reflection emits ReflectMapKey / ReflectElement for it) and
its own Arbitrary impl under generate_arbitrary. Custom-string-keyed maps
whose value needs proto3-JSON encoding (int64/float/bytes) serialize through a
new proto_str_key_map with-module (the existing proto_map requires
Display + FromStr, which a ProtoString need not implement).
Pluggable owned map container for map<K, V> fields (#210). A new
buffa::MapStorage trait (with associated Key / Value types) selects the
owned map collection, via buffa_build's map_type / map_type_custom knobs.
The default stays HashMap; BTreeMap is a zero-dependency built-in giving
deterministic (reproducible) encoded bytes, and a crate-local newtype can wrap
any other map (e.g. IndexMap). JSON and arbitrary work for every proto map
key/value type regardless of the container — the proto-JSON with-modules and
the arbitrary shim are generic over MapStorage. The wire format is
unchanged; only the in-memory collection changes, and view types are
unaffected.
Pluggable owned string / bytes types via from_wire (#206). A new
buffa::ProtoString / buffa::ProtoBytes trait selects the owned
representation of string / bytes fields, via buffa_build's
string_type_custom / bytes_type_custom knobs. Each trait has one method,
from_wire(WirePayload<'_>) -> Result<Self, DecodeError>, so a representation
decides validation and borrow-vs-own itself — an inline-capable type avoids
the transient String / Vec<u8> allocation a From<String> decode path
would force. The built-in String and Vec<u8> are the default
implementations; buffa-smolstr is the worked newtype example for a foreign
type. Breaking removal: the curated string_type presets (the
named-crate enum that previously selected compact_str / ecow /
smol_str) are dropped — use string_type_custom with a crate-local
newtype instead. The wire format is unchanged.
Pluggable owned collection for repeated fields (#208). A new
buffa::ProtoList<T> trait (Deref<Target = [T]> + FromIterator<T> + From<Vec<T>> + Default { push, reserve }) selects the owned collection
for repeated fields, via buffa_build's repeated_type /
repeated_type_custom knobs. The default stays Vec<T> and generated
output is byte-identical; the custom path takes a *-templated type
(e.g. "::my_crate::SmallVecRepeated<*>"). The collection must be
growable — the decoder appends one element per wire element with no
capacity check, so a fixed-capacity collection would panic on oversized
input rather than return a decode error. View types are unaffected.
Pluggable owned pointer for message fields (#209). A new
buffa::ProtoBox<T> trait (Deref<Target=T> + DerefMut { new, into_inner })
selects the smart pointer that a singular message field's MessageField wraps
— and the pointer of a boxed oneof message/group variant — via
buffa_build's box_type / box_type_custom knobs (the custom path takes a
*-templated type, e.g. "::my_crate::SmallBox<*>"). A oneof variant opted
into inline storage via unbox_oneof takes precedence and gets no pointer;
recursive variants stay pointered and so accept a custom pointer. The
default stays Box<T> and generated output is byte-identical. Only
exclusively-owned pointers qualify (Rc/Arc are excluded — the decoder
merges in place via DerefMut); inline pointers like SmallBox avoid the
per-field heap allocation. Source-breaking note: MessageField<T> gained
a defaulted pointer type parameter (MessageField<T, P = Box<T>>), so a
standalone MessageField::some(x) / none() with no pinning context now
needs a type annotation (MessageField::<T>::some(x)); struct-literal and
typed-assignment construction are unaffected. Added MessageField::from_pointer
(the generic counterpart to the Box-only from_box).
idiomatic_imports option (#189). buffa_build::Config::idiomatic_imports(true)
(also CodeGenConfig and protoc-gen-buffa's idiomatic_imports=true)
emits a use-backed short-name re-export at the package root under
file_per_package layout, so a generated type is reachable as
pkg::Foo instead of pkg::foo_module::Foo. Off by default; the file
layout and the wire format are unchanged.
#[diagnostic::on_unimplemented] hints on the custom-type traits (#229).
ProtoString, ProtoBytes, ProtoList, ProtoBox, and MapStorage now
carry diagnostic hints so a *_type_custom knob pointed at a foreign type
produces a "wrap it in a crate-local newtype" message instead of the raw
orphan-rule / unimplemented-trait error. Gated behind
rustversion::attr(since(1.78), …) for the MSRV.
examples/custom-types — end-to-end pluggable owned types (#234). A
runnable example crate that compiles a .proto with every owned-type knob
(string_type_custom, bytes_type_custom, repeated_type_custom,
map_type_custom, box_type_custom) pointed at crate-local newtypes
wrapping flexstr, smallvec, indexmap, and smallbox, then round-trips
a record through binary and JSON. The newtypes are the copy-paste template
for bridging a foreign storage type past the orphan rule.
Docker-free conformance runs (#192). task conformance-tools-local
builds conformance_test_runner from the pinned protobuf tag into
.local/bin/ and task conformance-local executes the same seven runs
as the Docker path with the same failure lists — for dev environments
without a Docker daemon or GHCR access.
Opt-in lazy views: the additive FooLazyView family (#188). With
Config::lazy_views(true) (plugin: lazy_views=true), each message
additionally generates a FooLazyView<'a> implementing the new
buffa::LazyMessageView trait — the eager FooView family is unchanged
and output is byte-identical with or without the flag. decode_lazy
performs one non-recursive scan, recording singular/repeated message
fields as undecoded byte ranges (LazyMessageFieldView /
LazyRepeatedView) that decode on access via fallible by-value accessors
(.get(), .get_or_default(), iteration), so reading a few fields of
many large sub-messages no longer allocates or recurses into untouched
sub-trees (~12× less allocation churn on the issue's workload; ~200×
faster when only 1% of items are read). Proto merge semantics are
preserved via per-occurrence fragments merged on access; the recursion
depth and unknown-field allowance recorded at each deferred field are
replayed per access (per-subtree capture of the shared pool), so
DecodeOptions limits flow through decode_lazy_view. Conversions are
fallible (to_owned_message() -> Result), the lazy Serialize impl
surfaces deferred errors as serde errors, and re-encoding replays
recorded fragments verbatim without validating them. Groups, oneof
message variants, map message values, and extern-typed fields (WKTs,
extern_path) stay eager inside the lazy view; the lazy family has no
reflection/OwnedView/text surface. A dedicated BUFFA_VIA_LAZY
conformance runner mode covers the lazy decoder against the full corpus.
Customizable feature-gate names (#183). CodeGenConfig::feature_gate_names
(exposed as buffa_build::Config::{json,views,text,reflect}_feature_name and
protoc-gen-buffa's {json,views,text,reflect}_feature= options) renames the
crate features that gate_impls_on_crate_features conditions the generated
impls on — e.g. gating the serde JSON impls behind a feature named serde
instead of json. Defaults are unchanged; the knob is inert unless gating is
enabled. A name that is not a valid Cargo feature name fails generation with
an error when its gate is active — the alternative is a permanently-false
#[cfg] that silently compiles the gated impls away.
buffa-build / buffa-codegen: oneof_attribute (#167) — attach Rust
attributes to generated oneof enums only (not message structs, not regular
enums), matched against the oneof's fully-qualified path
(.pkg.Message.oneof_name) with the same prefix rules as type_attribute.
Completes the type / message / enum / field attribute family for
the case where a oneof needs a different attribute set than the
surrounding types.
Unknown-field decode limit bounds decoder memory amplification (#184).
Unknown wire data can occupy ~20× more memory decoded than encoded:
every 2-byte unknown varint field materializes a ~40-byte
UnknownField, so a 64 MiB payload of minimal unknown fields (flat or
nested in a group) could force over 1 GiB of heap — not bounded by
with_max_message_size, which only caps input length. Decoding now
counts every materialized unknown field against a limit shared across
the whole decode call and fails with the new
DecodeError::UnknownFieldLimitExceeded when it is exceeded. The
default is 1,000,000 fields per decode (DEFAULT_UNKNOWN_FIELD_LIMIT),
capping slot overhead at ~40 MB, and applies to all decode entry points
including the trait-level convenience methods; tune it with
DecodeOptions::with_unknown_field_limit. Unknown length-delimited
payload bytes are not counted against the limit — the decoder only
allocates them once the sender has actually delivered the bytes, so
they are bounded by the input size and governed by
with_max_message_size. The limit covers owned-message and
DynamicMessage decoding; zero-copy views store unknown fields as
borrowed spans and are not affected by the amplification.
chrono interop for buffa-types (#163). A new off-by-default,
no_std-compatible chrono feature adds conversions between the
well-known Timestamp / Duration types and chrono::DateTime /
chrono::TimeDelta: From<chrono::DateTime<Tz>> for Timestamp (any time
zone; the instant is preserved), TryFrom<Timestamp> for DateTime<Utc>,
From<TimeDelta> for Duration, and TryFrom<Duration> for TimeDelta. The
last returns a new DurationChronoError because TimeDelta's range
(±i64::MAX milliseconds) is narrower than proto Duration's.
Contributed by @yordis.
New buffa-yaml crate: YAML serialization with protobuf-JSON semantics
(Phase 1 of protoyaml support, #155). A thin carrier layer that routes
buffa's generated protobuf-JSON serde impls through serde_norway, so YAML
I/O gets the full protobuf JSON mapping: camelCase/snake_case field
names, quoted int64/uint64, base64 bytes, enum string names, and
canonical well-known-type encodings. Public API: to_string, to_writer,
from_str, from_slice, from_reader, plus to_string_view /
to_writer_view for zero-copy views, and an Error type exposing a
carrier-agnostic Location { line, column }. Requires message types
generated with json = true. Contributed by @rsd-darshan.
Proto2 required-field presence on views (#200). Generated view types
(FooView and FooLazyView) for messages with proto2/editions
LEGACY_REQUIRED singular fields now expose has_<field>() accessors
that distinguish a field absent on the wire from one explicitly encoded
with its default value. Scalar required fields are tracked via hidden
__buffa_required_seen_* bit words; message/group required fields
delegate to MessageFieldView::is_set() / LazyMessageFieldView::is_set().
The view ReflectMessage::has() implementation consults the same
tracking, so reflection agrees with the inherent accessors. Owned
messages are unchanged: they store required fields bare and their
reflection still reports has() == false for a required field at its
default value. Messages without required fields are byte-identical to
before. MessageFieldView::is_set / is_unset are now const fn.
type_name_prefix option (#199). buffa_build::Config::type_name_prefix("Rpc")
(also CodeGenConfig::type_name_prefix and protoc-gen-buffa's
type_name_prefix= option) prepends a prefix to every generated message
struct and enum type name — message User {} generates struct RpcUser,
with views (RpcUserView), cross-references, and re-exports following.
Module names, oneof enums, extern_path-mapped types (including
well-known types), and the wire/JSON format are unaffected. The prefix
must be PascalCase (an ASCII uppercase letter followed by ASCII letters
and digits); anything else is rejected at generation time.
UTF-8 validation on the decode path now uses
smoothutf8 (default-on fast-utf8
feature). The view-decode hot path (borrow_str) and the contiguous-input
branch of the owned-decode helpers (decode_string, merge_string) take
verify_with_slack against the source buffer whenever at least 8 readable
bytes follow the field — so the slack-buffer fast path is reached for every
string field except one ending in the last 8 bytes of the wire buffer. The
non-contiguous fallback and WirePayload::to_str take the safe
smoothutf8::to_str, with simdutf8 delegation for inputs of 128 bytes or
more when std is also on. Measured on bare metal at the default x86-64
target: view decode +15–22% and owned merge +7–15% on the string-heavy
benchmark messages, neutral on bytes-dominated shapes. Consumers building
with -C target-cpu=x86-64-v3 (Haswell+, 2013–) additionally get
smoothutf8's BMI2 shift-DFA and AVX2 ASCII prefix scan, which the smoothutf8
README measures at ~1.6–2.2× stdlib on mixed-content input. Default builds
gain smoothutf8 (and simdutf8 under
std) as new dependencies; consumers already on default-features = false
should add fast-utf8 to their feature list to keep the faster validator,
or omit it to stay on core::str::from_utf8. A no_std build with
fast-utf8 adds only smoothutf8 (zero-dependency, formally verified).
(#241)
(breaking) protoc-gen-buffa now rejects malformed plugin parameters
instead of stderr-warning or silently defaulting (#235). An unknown option
key, a missing =, a non-true/false boolean value, an invalid
reflect_mode, or a malformed extern_path now fails generation via
CodeGeneratorResponse.error. Previously the default-on options
(unknown_fields, register_types, with_setters) treated any value other
than false as on, and unknown keys were silently ignored — typos produced
generated code that did not match the requested config. Migration: re-run
generation; if it newly fails, the named option was already being ignored.
The accepted spellings have always been the only documented ones.
MSRV lowered from 1.87 to 1.75, and the
README MSRV policy revised:
rust-version now declares the lowest toolchain the released code actually
compiles on (verified in CI), with bumps capped at roughly twelve months
behind stable. The 1.75 floor is set by return-position impl Trait in
traits, used by MapStorage::storage_iter. Reaching it required only
mechanical respellings of newer stdlib conveniences — Option::is_none_or,
i32::cast_unsigned, f64::abs in const fn — and
gating the six #[diagnostic::on_unimplemented] hints behind
rustversion::attr(since(1.78), …) so they remain active on modern
toolchains. Adds rustversion as a dependency of buffa and
buffa-descriptor. (#228)
DecodeOptions::with_max_message_size now clamps values above the protobuf
2 GiB - 1 message-size limit (with a debug assertion to catch accidental
sentinel use). DecodeOptions::without_reader_size_limit is the explicit
std-only opt-out for EOF-bounded decode_reader input; slice, Buf,
view, and length-delimited decode paths keep their configured cap, and
length-delimited declared lengths never exceed 2 GiB - 1. Callers that
used with_max_message_size(usize::MAX) for unbounded reader input should
switch to without_reader_size_limit; in release builds, the old spelling
now caps at 2 GiB - 1. (#236)
MapValueDecode::merge now returns Result<MapValueDecodeStatus, _>
instead of Result<(), _>, and a new merge_entry_with_unknowns carries
the closed-enum-map preservation path. The trait is sealed, so downstream
implementations are unaffected; direct callers of merge (rare) must
handle the new return value. (#218)
SizeCache no longer zeroes its inline slot array on construction. A fresh
cache is built for every encode/compute_size, and because it is passed by
&mut to an out-of-line compute_size the compiler cannot elide the unused
tail, so the previous [0u32; N] initializer emitted N/4 SSE stores on
every encode (confirmed by disassembly). The inline storage is now
[MaybeUninit<u32>; N], written only for the slots actually used; a slot is
always written by reserve before len advances past it and read only at
indices < len, so the single assume_init in consume_next is sound. This
invariant is private to the size_cache module (no external code can break
it — worst case is a panic, never UB) and is checked mechanically in CI by a
Miri job over the size_cache tests. No API or wire-format change. (#223)
Default map<K,V> hasher is now foldhash::fast::RandomState on std
builds (previously std::hash::RandomState / SipHash-1-3). The container
remains std::collections::HashMap; only the S type parameter changes.
This brings the std build in line with no_std (which already used
foldhash via hashbrown's default) and matches the hasher class used by
Google's protobuf-v4 (upb / Wyhash). On the LogRecord benchmark — a
string-and-map-heavy shape — this is roughly a 12% owned-decode speedup.
foldhash::fast is per-instance seeded (from ASLR addresses and process
start time, not a CSPRNG) and does not advertise HashDoS resistance; treat
the default as not hardened against adversarial hash flooding. Consumers
decoding map fields with attacker-controlled keys who need a hardened
bound can select MapRepr::BTreeMap (no hashing) or supply a SipHash-backed
map via MapRepr::Custom. The MapStorage and
ReflectMap impls are now generic over the hasher S, so a custom-hasher
std::collections::HashMap works without a newtype. Migration: the
concrete map field type changes, so code that names
std::collections::HashMap<K,V> (default S) for a generated field no
longer type-checks — use the buffa::Map<K,V> alias instead. Construct
empty maps with buffa::Map::default() (HashMap::new() /
HashMap::with_capacity() are unavailable on std builds because they are
pinned to std's default hasher; use default() on both std and no_std
for portability). Array-literal construction via Map::from([...]) /
.into() is likewise unavailable; use [...].into_iter().collect().
buffa::Map and buffa::foldhash are now re-exported at the crate root.
(#224)
Generated decode arms (owned merge, view decode, lazy record arms,
map-entry loops) emit a single ::buffa::encoding::check_wire_type call
instead of a seven-line inline wire-type guard (~1,100 sites across a
generated corpus). Error payloads are byte-identical; the #[cold]
out-of-line error constructor moves construction off the hot decode
path. Regenerate checked-in code to pick up the shrink. (#193)
Owned map fields encode/decode through the new buffa::map_codec module
(zero-sized per-proto-type codecs plus generic field helpers) instead of
~40-50 inline generated lines per map field. Wire output, decode-limit
semantics, and the fixed-width sizing fast path are unchanged; everything
monomorphizes to the previous code. (#194)
Generated write_to bodies use new fused put_*_field runtime writers
(one call per field arm) instead of separate tag-encode + payload-encode
pairs (~870 sites); owned and view impls share them. Wire output is
byte-identical. The fused writers are #[inline(always)] so the field
number reaching Tag::new const-folds and the inlined encode_varint
collapses to a single byte store for tags below 128 — restoring the encode
fast path that the call boundary had blocked. (#195, #207)
DefaultInstance / DefaultViewInstance / ViewReborrow impls are
emitted via new public runtime macros (impl_default_instance!,
impl_default_view_instance!, impl_view_reborrow!) instead of being
expanded per generated type (~290 sites); hand-written message and view
types can reuse them. No behavioural change. (#196)
Generated JSON Serialize impls use new internal (#[doc(hidden)])
buffa::json_helpers adapter newtypes (ProtoJson, BytesJson,
MapKeyJson, sequence variants, ...) instead of ~65 per-site local _W*
wrapper structs. JSON output is unchanged. (#197)
Breaking: MessageView gains a required merge_view_field method,
and the per-view decode tag loop is now a provided trait method
(merge_into_view), mirroring the owned side's Message::merge /
merge_field split. Generated views supply only the field match —
regenerate code from earlier releases. Hand-written MessageView impls
must add merge_view_field; the trait docs include the canonical shape,
the unknown-field-preserving arm, and the decode_view →
decode_view_ctx wiring. Sub-message arms call the new provided
decode_view_ctx / merge_into_view instead of the removed inherent
_decode_ctx / _merge_into_view helpers. (#198)
Breaking: the decode-path Message trait methods (merge,
merge_field, merge_to_limit, merge_group, merge_length_delimited),
encoding::decode_unknown_field, and message_set::merge_item now take a
DecodeContext<'_> — carrying the remaining recursion depth and the
shared unknown-field allowance — in place of the bare depth: u32. Code
generated with earlier releases must be regenerated. Callers of the
convenience methods (decode, decode_from_slice, merge_from_slice,
DecodeOptions) are unaffected. (#184)
(breaking) Zero-copy views enforce the unknown-field limit and
coalesce adjacent unknown records (#184). View decoding previously stored
one borrowed span (16 bytes) per unknown wire record with no bound beyond
the input size. Spans for adjacent unknown records now coalesce into a
single span — a contiguous run of unknown fields costs one Vec slot
regardless of field count, and re-encodes byte-identically — and each new
span (one per unknown run) is counted against the same unknown-field limit
that bounds owned-message decoding, configured via
DecodeOptions::with_unknown_field_limit and honored by
DecodeOptions::decode_view. As part of this, the view decode path now
threads DecodeContext<'_>: MessageView::decode_view_with_limit(buf, depth) is replaced by decode_view_with_ctx(buf, ctx), and generated
views' hidden _decode_depth helpers become _decode_ctx — code
generated by earlier releases must be regenerated, consistent with the
owned-path change above.
(breaking) View-to-owned conversion is now fallible and honors the
decode-time limit (#184). MessageView::to_owned_message and
to_owned_from_source (and the OwnedView wrapper) now return
Result<Owned, DecodeError>: generated conversions previously swallowed
unknown-field re-materialization errors via unwrap_or_default(), silently
dropping every unknown field. UnknownFieldsView::to_owned also now
re-materializes under the unknown-field allowance that remained when the
view recorded its first unknown field — so a tight with_unknown_field_limit
configured at decode_view time carries through conversion, where each
owned UnknownField counts individually (unlike the coalesced spans the
view stores). Views built manually via push_raw fall back to the default
limit.
Extension JSON serialization no longer scans previously seen unknown-field
numbers linearly for every record. The serialize-side extension registry
still emits each registered extension at most once in first-seen unknown-field
order, but duplicate detection now uses a set instead of a Vec, avoiding
quadratic work for messages with many distinct unknown field numbers.
(#237)
extern_path references to nested types now use the owning crate's
deconflicted module name (#233). When the owning crate renames a
message's nested-types module to avoid colliding with a sibling sub-package
(the [#135] deconfliction, e.g. Money's nested types land in money_
because sub-package pb.lyft.money also exists), a consumer referencing
.pb.lyft.Money.Currency via extern_path previously emitted the
un-deconflicted …::money::Currency and failed to compile. The consumer
now computes the same deconflicted name. Caveat: the consumer's
descriptor set must include the colliding sub-package file (importing any
type from it suffices); otherwise the consumer cannot see the collision and
emits the un-deconflicted path.
box_type_custom and repeated_type_custom now compile under
generate_json(true). Two json_helpers functions were still hard-wired
to the default representations: skip_if::is_unset_message_field only
accepted &MessageField<T> (default Box<T> pointer), and
proto_seq::deserialize only returned Vec<T>. So any JSON-enabled message
with a custom-boxed optional sub-message, or a custom repeated collection of
a 64-bit / float / bytes element, failed to compile. The former is now
generic over P: ProtoBox<T> and the latter over C: From<Vec<T>>; both
are inferred from the field type, so default-representation code is
unchanged. (#234)
DecodeOptions::decode_reader no longer overflows when the read size is
unbounded. The internal read_limited helper computed
max_message_size as u64 + 1 to read one sentinel byte past the limit; on
64-bit targets this overflowed — a debug panic, or in release a wrap to zero
that silently decoded an empty default message. The addition now saturates in
the bounded path, and unbounded reads are spelled explicitly via
DecodeOptions::without_reader_size_limit. 32-bit targets and finite
limits are unaffected. (#219)
Closed-enum map values now preserve unknown entries correctly. For
proto2 map<K, ClosedEnum> fields, an unknown enum value now prevents the
map entry from being inserted and routes the whole original map-entry record
to unknown fields. This fixes the previous default-valued entry synthesis
(key -> E::default()) and applies to owned and view decode paths.
Regenerate code with the matching buffa-codegen to get preservation;
with an older codegen, runtime-only upgrades change unknown closed-enum
map entries from default-insert to drop. (#218)
Packed varint views reserve by element count, not byte length (#216).
Decoding a packed varint repeated field into a view previously reserved
payload.len() slots — the upper bound for one-byte varints, but a 10×
over-reservation for negative int32 / int64 (every element is a
10-byte varint). The reservation is now the exact element count, computed
by a single pass that counts terminator bytes (most-significant-bit clear)
before decoding. View memory for packed-signed-heavy messages drops
proportionally; the wire format and owned decode are unchanged.
DecodeOptions::decode_length_delimited_reader no longer allocates the
wire-declared length up front. The method previously allocated a zeroed
buffer of the declared length before reading, so a source that declared a
large length (up to max_message_size, 2 GiB by default) but delivered
few or no bytes still forced the full allocation. The buffer now grows
incrementally as bytes are actually delivered (initial capacity capped at
64 KiB), so peak allocation tracks delivered data. Truncated streams
report UnexpectedEof exactly as before; behavior for well-formed
streams is unchanged. (#185)
ci: publish buffa-descriptor before buffa-types by @iainmcgin in #160
Full Changelog: v0.7.0...v0.7.1
This release is a patch bump under the
Rust 0.x convention:
everything below is additive or a fix, with no breaking changes and no MSRV
change. The new codegen capabilities are opt-in (unbox_oneof) or gated on a
proto option (debug_redact); the packed view pre-allocation applies to all
regenerated code but is behaviorally invisible — a pure performance hint. Code
regenerated with 0.7.1 calls the new (hidden) RepeatedView::reserve hook, so
pair regenerated code with a buffa 0.7.1 runtime — any caret 0.7 requirement
resolves there automatically.
unbox_oneof opt-out for Boxed message oneof variants (#126).
Config::unbox_oneof_in(&[paths]) stores the matching message-typed oneof
variants inline in the owned enum instead of behind Box<T>, removing an
allocation per construction; Config::unbox_oneof() is the blanket form.
Recursive variants cannot be inlined: a rule naming one exactly is
rejected at codegen time, while broader prefix rules (including the
blanket) silently keep recursive variants boxed and inline the rest. View
oneof variants are unaffected and stay boxed. Enums with an inline message
variant carry #[allow(clippy::large_enum_variant)]. Contributed by
@sam-shridhar1950f.
[debug_redact = true] is honored in generated Debug impls. Fields
carrying the standard debug_redact field option print [REDACTED] instead
of their value in the owned message's Debug impl, and oneof enums, view
structs, and view-oneof enums containing such fields swap their
#[derive(Debug)] for a generated impl that redacts those fields/variants.
Output for messages without the annotation is unchanged. Note this covers
Debug formatting only — text-format and JSON serialization are
intentionally unaffected. A view struct containing a redacted field now
lists proto fields only in its Debug output (matching owned messages), so
__buffa_unknown_fields / phantom internals no longer appear there.
The reflective DynamicMessage Debug impl honors the option as well,
printing [REDACTED] in place of the value of any field whose descriptor
carries it.
Packed repeated view decoders pre-allocate RepeatedView capacity.
Generated view decode arms for packed repeated scalar / enum fields now
call RepeatedView::reserve(_) before the decode loop, matching the
existing pre-allocation hint on the owned decode path. Fixed-width kinds
(fixed32, sfixed32, float, fixed64, sfixed64, double) reserve
the exact element count; varint kinds (int32/64, uint32/64,
sint32/64, bool, enum) reserve payload.len() as a safe upper
bound (every wire varint is ≥ 1 byte). The hidden RepeatedView::reserve
hook is also new but #[doc(hidden)]. This trims allocator pressure on
workloads that decode many small packed repeated fields (MVT-style
payloads), reported in #171.
TimestampError::Overflow's Display message generalized. It now
reads "timestamp is out of range for the target type" instead of naming
SystemTime, since the same error is returned by the new
Timestamp → chrono::DateTime<Utc> conversion. Code matching on the
enum variant is unaffected.
HasMessageView carries a #[diagnostic::on_unimplemented] hint. When a
type is used where the generated view family is required but its crate was
generated without one (buffa older than 0.7.0, or views disabled) or has it
behind a disabled feature, the compile error now explains the cause and how
to fix it — regenerate the defining crate with buffa ≥ 0.7.0 and views
enabled (generate_views(true) / views=true), or enable the crate's views
feature — instead of only naming the missing trait bound. Downstream
consumers such as connect-rust rely on this trait for their request
wrappers, so the notes land directly in the consumer's build output.
DynamicMessage
snapshots at the boundary instead of failing to compile: vtable accessors
for message-typed fields route through the field type's own
Reflectable::reflect(), and bridge mode now also emits ReflectElement
so repeated / map fields degrade too. View reflection still requires
vtable-grade types throughout — that limitation is now documented. (Code
matching exhaustively on ReflectCow may now observe Owned for
bridge-grade message fields; all-vtable builds are unchanged.)ReflectMessage, Reflectable, and ReflectElement carry
#[diagnostic::on_unimplemented] hints, so building vtable codegen against
an extern-path crate without its reflection feature (e.g. buffa-types
without reflect) names the missing cargo feature instead of emitting a
bare unsatisfied-trait error. The reflect_mode docs state the
requirement.Debug impl now labels keyword-named fields without the
raw-identifier prefix (type instead of r#type), matching what
#[derive(Debug)] prints and what the view Debug impl emits.\377 (255) in a proto2 bytes field's default_value
are now rejected with a codegen error instead of silently wrapping to a
wrong byte (\400 previously decoded to 0x00), matching protobuf C++'s
UnescapeCEscapeString behavior (#164). Such escapes never appear in
protoc-emitted descriptors, so this only affects hand-built or corrupted
FileDescriptorSet input.default_value now consume the full
run of hex digits and reject accumulated values above \xff (255) with a
codegen error, matching protobuf C++'s UnescapeCEscapeString behavior
(#173). Previously exactly two digits were read, so \xfff decoded to the
byte 0xFF followed by a literal f instead of erroring, and a
single-digit escape such as \x1 at end of input was wrongly rejected. As
with the octal fix, such escapes never appear in protoc-emitted
descriptors, so this only affects hand-built or corrupted
FileDescriptorSet input.agents: grant reviewer subagents scoped Bash for git + cargo checks by @iainmcgin in #122
Full Changelog: v0.6.0...v0.7.0
This release is a minor bump under the
Rust 0.x convention.
The breaking changes are the removal of OwnedView<V>'s Deref impl and the
extension of use_bytes_type() to map<K, bytes> values (both under
Changed below), plus an MSRV raise from 1.85 to 1.87. Consumers with
checked-in generated code should regenerate with the 0.7.0 toolchain to pick
up the new FooOwnedView wrappers, HasMessageView impls, and
UpperCamelCase enum aliases — all additive.
Runtime reflection: DescriptorPool and DynamicMessage.
buffa-descriptor gains a reflect feature with a descriptor-driven
reflection runtime. DescriptorPool::decode builds linked,
feature-resolved descriptors (MessageDescriptor, FieldDescriptor,
EnumDescriptor, ServiceDescriptor, …) from a FileDescriptorSet,
treating the input as untrusted (malformed sets return PoolError rather
than panicking) and retaining the raw FileDescriptorProtos plus a symbol
index (file_by_name, file_containing_symbol) for gRPC server
reflection. DynamicMessage decodes and encodes any message by descriptor
— no generated types required — with unknown-field preservation, in-place
mutation (field_mut / field_by_number_mut), Any pack/unpack,
extension fields, and custom-option access (options() on every linked
descriptor, DynamicMessage::from_options). With the json feature it
also speaks proto3 canonical JSON (Serialize, DynamicMessage::from_json,
lenient from_json_ignoring_unknown, duplicate-key rejection). The
dyn-safe ReflectMessage / ReflectMessageMut traits and the
ReflectCow / Value / ValueRef types are the surface generated types
plug into (see vtable mode below). Generated code opts in with
buffa_build::Config::generate_reflection(true) (plugin:
reflection=true), which embeds the package's FileDescriptorSet and
exposes a lazily-built pool as pkg::descriptor_pool(). The reflection
codec passes the protobuf conformance suite through a dedicated
DynamicMessage-only runner mode.
Vtable reflection mode. Generated types now implement
buffa_descriptor::reflect::ReflectMessage directly — on both the owned
structs and the zero-copy view types — so foo.reflect() borrows foo in
place (ReflectCow::Borrowed) with no encode/decode round-trip and no
per-field allocation. This is the path a CEL evaluator, transcoding gateway, or
generic interceptor takes to read fields by descriptor; reflecting a decoded
view runs several times faster than the previous bridge round-trip. Select the
mode with the new buffa_build::ReflectMode enum:
buffa_build::Config::new()
.reflect_mode(buffa_build::ReflectMode::VTable) // or ::Bridge / ::Off
.compile()?;
The protoc-gen-buffa equivalent is reflect_mode=off|bridge|vtable. Vtable
mode does not require view generation: with views off, only the owned
ReflectMessage is emitted. generate_reflection(true) selects vtable mode;
reflect_mode(ReflectMode::Bridge) opts into the smaller round-trip
implementation (one DynamicMessage encode/decode per reflect() call)
instead of one impl ReflectMessage per generated type.
buffa-types reflect feature. Well-known types (Timestamp,
Duration, Struct/Value, Any, wrappers, …) now implement
ReflectMessage, so messages that embed WKTs reflect end to end.
Pluggable owned types for string and bytes fields (#127, #156, #206).
Generated string / bytes fields can use a custom in-memory type chosen at
code-generation time, with no change to the wire format. buffa_build::Config
gains string_type(StringRepr) / string_type_in and the convenience
string_type_custom("::path::To::Type") / string_type_custom_in, where
buffa_build::StringRepr is { String (default), Custom(path) }. The new
bytes counterpart is bytes_type(BytesRepr) / bytes_type_in /
bytes_type_custom / bytes_type_custom_in, where BytesRepr is
{ Vec (default), Bytes, Custom(path) }; use_bytes_type / use_bytes_type_in
remain as aliases for BytesRepr::Bytes. Rules accumulate and the last match
wins. Only the owned struct field type changes — view types still borrow
&str / &[u8], and map keys/values keep their default type.
The chosen type must implement the marker traits buffa::ProtoString /
buffa::ProtoBytes. Each requires a from_wire(WirePayload<'_>) -> Result<Self, DecodeError> constructor (alongside the supertraits
Clone + PartialEq + Default + Debug + Send + Sync, Deref to str / [u8],
AsRef, and From<String> / From<Vec<u8>>). from_wire lets each
representation own validation and borrow-vs-own:
WirePayload is Borrowed(&[u8]) (zero-copy) or
Owned(Bytes), with as_slice() and into_bytes(). A representation that
enforces extra invariants can reject a value from from_wire with the new
DecodeError::Custom(&'static str) variant. buffa ships the built-in
impls for String, Vec<u8>, and bytes::Bytes; a foreign type (e.g.
smol_str::SmolStr) is wrapped in a local newtype that implements the trait —
the new buffa-smolstr crate is the template (an inline, allocation-free
from_wire). A custom type needs no native Arbitrary impl (a generic builder
handles it). A custom type used as the element of a repeated field — or a
custom bytes type as a map<K, bytes> value — must be crate-local:
codegen emits ReflectElement (vtable) and, for bytes, base64 ProtoElemJson
(JSON) impls for it, which the orphan rule forbids for a foreign type. A custom
bytes map value is honored just like the built-in Bytes (only the
map<bytes, bytes> carve-out keeps Vec<u8>). Singular / optional / oneof uses
work with the newtype without the crate-local restriction.
Why from_wire rather than a blanket From-based impl: the decode path was
first built as a blanket impl over From<String> / From<Vec<u8>> to learn the
tradeoff, but that path always pays decode_string's allocate-and-copy and a
transient heap allocation even for a short string that an inline type
(smol_str) could store without touching the heap. from_wire hands the
representation the raw payload so it can inline, validate lazily, or take
ownership zero-copy — so it never disadvantages a custom type.
BREAKING (unreleased only): the earlier unreleased string_type shapes are
removed — both the StringRepr::{SmolStr, EcoString, CompactString} presets
(with the buffa / buffa-descriptor smol_str / ecow / compact_str
features and ::buffa::{smol_str, …} re-exports) and the later blanket
From-based ProtoString / ProtoBytes. Pointing string_type_custom at a
foreign type directly no longer compiles; use buffa-smolstr (or a local
newtype implementing from_wire). Default output (String / Vec<u8>) is
byte-for-byte unchanged. buffa-build / buffa-codegen only — there is no
protoc-gen-buffa plugin option yet.
Generated FooOwnedView wrapper types. When views are generated, each
message now also gets a FooOwnedView — re-exported at the package root
next to Foo and FooView (canonical path __buffa::view::FooOwnedView):
a self-contained 'static handle wrapping OwnedView<FooView<'static>>
with one accessor method per field (owned.name(), owned.id(), …). Every
accessor borrows from &self, so field data can never outlive the
underlying buffer, and the handle stays Send + Sync for async handlers and
spawned tasks. The wrapper forwards decode / decode_with_options /
from_owned / to_owned_message / bytes / into_bytes, exposes the full
view via view(), converts to and from the raw OwnedView, and serializes
to protobuf JSON when generate_json is enabled. A field or oneof whose
name collides with one of the wrapper's reserved method names keeps working
through view(); its accessor is skipped with a build warning
(CodeGenWarning::OwnedViewAccessorSuppressed).
HasMessageView view-family trait. Generated code now implements
buffa::HasMessageView for every message (when views are generated),
linking the owned type to its view types: Foo::View<'a> = FooView<'a>
and Foo::ViewHandle = FooOwnedView, with a provided
decode_view_handle() helper. The generated wrapper additionally
implements From<OwnedView<FooView<'static>>> and
AsRef<OwnedView<FooView<'static>>>, so code that is generic over an owned
message can decode, reborrow, and convert without naming the concrete
types — the hook an RPC framework needs to accept M and work with
M::View<'_> and M::ViewHandle generically.
Idiomatic UpperCamelCase enum value aliases (#13). Generated enums
now also carry associated const aliases with the enum-name prefix
stripped and the value converted to UpperCamelCase —
RuleLevel::RULE_LEVEL_HIGH is reachable as RuleLevel::High — usable in
expressions and in pattern position with exhaustiveness preserved. The
SHOUTY_SNAKE_CASE variants remain the definitive variants and Debug
output is unchanged, so the aliases are purely additive; consumers with
checked-in generated code will see new consts on regeneration. If two
values of an enum would collide after conversion, aliases are suppressed
for that enum as a whole and reported through the new CodeGenWarning
diagnostics (buffa_codegen::generate_with_diagnostics). Default on; opt
out per compilation unit with
buffa_build::Config::idiomatic_enum_aliases(false) /
CodeGenConfig::idiomatic_enum_aliases = false.
OwnedView<V> no longer implements Deref<Target = V>. Breaking.
The Deref impl exposed the inner view as FooView<'static>, so borrowed
fields appeared 'static to the compiler and could be held past the point
where the OwnedView (and the buffer they point into) was dropped — safe
code could end up reading freed memory. In practice this required the
calling application to deliberately store a field reference beyond the
handle's lifetime, so the practical exposure is limited, but the API should
not allow it at all. Field access now goes through reborrow() (one extra
call per scope: let person = owned.reborrow(); person.name) or, more
conveniently, the new generated FooOwnedView accessor methods, both of
which tie every borrow to the handle. Serializing the handle directly
(serde_json::to_string(&owned_view)) is unaffected.use_bytes_type() / use_bytes_type_in(...) now applies to map<K, bytes>
values (#76). Previously map values were always Vec<u8> regardless of
config — the only bytes-context not covered. They now match the type used
for singular / optional / repeated / oneof bytes fields under the same rule
(bytes::Bytes when configured), so view → owned conversion of map values
participates in the to_owned_from_source zero-copy slice_ref path just
like the other shapes. Breaking for code that already enabled
use_bytes_type() on a proto containing map<K, bytes>: at construction
sites, rewrite map-value construction from Vec<u8> to bytes::Bytes
(b"v".to_vec() → bytes::Bytes::from_static(b"v") for literals,
bytes::Bytes::from(v) for an owned Vec<u8>, or
bytes::Bytes::copy_from_slice(s) for a non-'static borrow). At read sites,
bytes::Bytes has no inherent as_slice, so any as_slice() on the value
needs replacing — e.g. map.get(k).map(Vec::as_slice) becomes
map.get(k).map(|b| &b[..]). One carve-out: an effective map<bytes, bytes>
keeps Vec<u8> values; this requires strict_utf8_mapping(true) and a
map<string, bytes> whose key carries [features.utf8_validation = NONE]
(strict_utf8_mapping alone keeps a plain map<string, bytes> value as
Bytes). See the use_bytes_type_in docs. Under generate_arbitrary,
affected map fields use the new __private::arbitrary_bytes_map<K> shim
(K: Arbitrary + Eq + Hash — every proto map-key type satisfies this).Module redefinition error when a message and a sub-package share a name
(#135). A message with nested types emits a snake_case(MessageName)
submodule, which collided with a sibling sub-package of the same name
(protobuf is case-sensitive — message Oof and package foo.oof legally
coexist — but both mapped to mod oof, producing an E0428). Codegen now
deconflicts the nested-types module by appending _ (e.g. oof_; more
underscores if several modules collide in the same scope — see DESIGN.md),
leaving the message struct (foo::Oof) and the sub-package module
(foo::oof) at their natural names. This only triggers on a collision that
previously failed to compile, so existing output is unchanged. Two caveats:
(1) if you add a sub-package whose name collides with an existing message's
nested-types module, paths to those nested types move from foo::oof::… to
foo::oof_::…; (2) both packages must be generated in the same
buffa_build::Config::compile() call — deconfliction cannot span separate
compilations, since each only sees its own descriptor set.
Per-type extern_path mappings were silently ignored (#111). An
extern_path entry naming a single type FQN (e.g.
.extern_path(".google.protobuf.Timestamp", "::my_types::Timestamp"), the
prost/tonic idiom) parsed but never matched, because resolution only
considered package prefixes. Type references now resolve per-type: an exact
type-FQN entry wins over the internal descriptor.proto routing, which wins
over the longest matching package prefix, which wins over local generation.
Nested types inherit an enclosing message's override, resolving to the
override's parent module plus the usual snake_case(MessageName)
nested-types module. Note that entries which previously had no effect now
take effect: a type-FQN entry (including a typo'd one) that was a silent
no-op before will now change the generated reference, and a wrong target
surfaces as a compile error in the generated code.
docs: indicate that claude has friends by @iainmcgin in #109
Full Changelog: v0.5.2...v0.6.0
Generated message structs now include with_<field>(value) -> Self
builder-style setter methods for every explicit-presence field (proto3
optional, proto2 optional, and editions fields with
field_presence = EXPLICIT). This allows chained construction without
Some(...) wrapping:
let req = GetSecretRequest::default()
.with_name("alice")
.with_timeout_ms(30_000)
.with_enabled(true);
String fields accept impl Into<String> (&str works directly); bytes
fields accept impl Into<Vec<u8>> or impl Into<bytes::Bytes> (byte
array literals like b"data" work directly); enum fields accept
impl Into<EnumValue<E>> (bare variant works directly, no
EnumValue::Known(...) wrapper needed); plain scalars take the bare
type to keep integer-literal inference unambiguous. Message fields
(MessageField<T>), repeated fields, map fields, oneof variants,
proto2 required fields, and implicit-presence fields are unaffected.
To clear a field, assign None directly. Setters are pure inherent
methods with no runtime dependency, so they're emitted unconditionally
regardless of gate_impls_on_crate_features. Disable per compilation
unit with CodeGenConfig::generate_with_setters = false,
buffa_build::Config::generate_with_setters(false), or the
with_setters=false plugin opt. Consumers with checked-in generated
code will see new methods on regen.
(#30,
#93, by @tejas-dharani)
buffa::MessageName trait exposes a generated message's protobuf
identifiers as compile-time &'static str constants. Codegen emits
impl MessageName for #Msg (and for #MsgView<'a>) with four consts:
PACKAGE ("my.pkg", empty for the unnamed root package), NAME
("Outer.Inner" — unqualified, with . between nesting levels),
FULL_NAME ("my.pkg.Outer.Inner"), and TYPE_URL
("type.googleapis.com/my.pkg.Outer.Inner" — the
google.protobuf.Any.type_url form). All four are computed at codegen
time as string literals, so there's no runtime allocation or
concatenation — unlike prost::Name, whose full_name() and
type_url() are runtime format! calls. PACKAGE and NAME are
separate consts because the dotted FULL_NAME cannot be split
unambiguously (foo.Bar.Baz could be package foo.Bar + message Baz
or package foo + nested Bar.Baz).
The trait has no supertrait — it doesn't reach into the wire codec —
so view types implement it too: a generic event-sourcing registry can
bound on T: MessageName and dispatch zero-copy views and owned
messages identically. Useful for type-erased registries, logging, and
any code that needs the protobuf name without the descriptor machinery.
The inherent Foo::TYPE_URL const generated since 0.4.0 is unchanged
and equal to <Foo as MessageName>::TYPE_URL; for messages that also
implement ExtensionSet, FULL_NAME is equal to
ExtensionSet::PROTO_FQN (all derive from the same codegen source).
MessageName is not object-safe (associated const only) — use it
as a bound, not dyn MessageName. Migrating from prost::Name: rename
the bound and replace runtime M::full_name() / M::type_url() calls
with the consts. (#108,
by @yordis)
buf.build/anthropics/buffa is published to the public Buf Schema
Registry. buf generate can now reference protoc-gen-buffa as a
remote: plugin with no local install: remote: buf.build/anthropics/buffa
with opt: [file_per_package=true] and a small hand-written pub mod
tree, or paired with a locally-installed protoc-gen-buffa-packaging
for a generated mod.rs. The README quick-start, docs/guide.md
"Using buf" section, and a new
examples/bsr-quickstart/ project document
the workflow. The stale in-repo protoc-gen-buffa/buf.plugin.yaml
metadata file is removed — the canonical plugin definition lives in
bufbuild/plugins.
buffa-codegen: CodeGenConfig::gate_impls_on_crate_features.
When true, generated impls controlled by generate_json,
generate_views, and generate_text are wrapped in
#[cfg(feature = "json" | "views" | "text")] (or #[cfg_attr(...)] for
derives and field attributes) instead of being emitted unconditionally.
The consuming crate defines matching Cargo features and enables the
corresponding runtime support (buffa/json, buffa/text, serde, …)
behind them. The generate_* flags still control whether an impl kind
is emitted; the new flag only controls how. Default false — no
change to existing output. This is the codegen mechanism that will let
buffa-descriptor and buffa-types ship every impl while keeping the
codegen toolchain (buffa-codegen / buffa-build / protoc-gen-buffa)
lean — it depends on them with default-features = false. Tracked in
#113. Exposed as
buffa_build::Config::gate_impls_on_crate_features(bool) and the
gate_impls=true plugin opt, both default-off.
buffa-descriptor: regenerated with views, JSON, text, and arbitrary
impls behind crate features. descriptor.proto and
compiler/plugin.proto types now ship the full impl surface — gated on
views, json, text, and arbitrary Cargo features so the codegen
toolchain (buffa-codegen / buffa-build / protoc-gen-buffa) can
depend on buffa-descriptor with default-features = false and stay
free of serde / serde_json / base64 / arbitrary. Consumers
whose protos reference a descriptor.proto type as a field (most
commonly anything depending on buf/validate/validate.proto, or
buf.registry.module.v1 / buf.alpha.image.v1 which embed
FileDescriptorSet / FileDescriptorProto) must enable the
buffa-descriptor features matching their codegen modes —
views = ["buffa-descriptor/views"], json = ["buffa-descriptor/json"],
etc., or just buffa-descriptor = { ..., features = ["views", "json"] }.
This closes #113: the
full bufbuild/registry and bufbuild/buf modules now generate and
compile cleanly with views=true + json=true.
Migration: if your Cargo.toml already declares buffa-descriptor
as a dependency, add the features matching your codegen config:
# build.rs uses .generate_views(true).generate_json(true)
buffa-descriptor = { version = "0.6", features = ["views", "json"] }
If you don't declare buffa-descriptor directly, the failure mode is a
missing-impl error at the embedding type's serde / view call site (e.g.
the trait bound FileDescriptorSet: serde::Deserialize is not satisfied); add buffa-descriptor with the right features.
The buffa_descriptor::generated module tree now nests
google.protobuf.compiler inside google.protobuf to mirror the proto
package hierarchy (so cross-package super::* references in the view
code resolve); the previous sibling-style
buffa_descriptor::generated::compiler and
buffa_descriptor::generated::{FileDescriptorProto, GeneratedCodeInfo}
paths are preserved with pub use re-exports.
serde::Serialize is now implemented for generated view types when generate_json is
enabled, allowing zero-copy JSON serialization without .to_owned_message().
OwnedView<V> also gains a blanket Serialize impl so serde_json::to_string(&owned_view)
works directly. Well-known type views (TimestampView, DurationView, AnyView, etc.)
also implement Serialize (delegating to the owned form) when the buffa-types/json
feature is enabled, so messages that nest WKT fields work out of the box. MapView gains
iter_unique() and len_unique() helpers (last-write-wins deduplication) so map fields
with duplicate wire keys serialize to a valid JSON object. The protobuf conformance suite
gains a BUFFA_VIEW_JSON=1 run that exercises view-side JSON output against the
conformance reference assertions.
Known limitations: (1) Extension fields are not included in view JSON output —
serialize the owned form (view.to_owned_message()) to include extensions. (2) The view
impl uses serialize_map(None), which is fine for serde_json but will be rejected at
runtime by length-prefixed formats like bincode or postcard; use the owned form for
those serializers. (#83)
buffa / buffa-codegen: serde_json re-exported from buffa for
generated extension JSON deserialize. Messages with extensions N to M;
ranges and json=true codegen get a hand-written Deserialize impl that
buffers "[pkg.ext]" JSON keys into a serde_json::Value before
dispatching to extension_registry::deserialize_extension_key. The emitted
path was a bare ::serde_json::Value, which silently required every
consumer of json=true codegen to declare serde_json directly in its own
Cargo.toml — a footgun reported by Buf for bufbuild_registry_* SDKs
generated against buf/validate/validate.proto (which has 21 extension
ranges). buffa now re-exports serde_json (gated on the json feature,
#[doc(hidden)], matching the existing bytes re-export) and codegen
emits ::buffa::serde_json::Value, so consumers only need buffa,
buffa-types, and serde (the latter for the #[derive] macro). No
generated output exists for this path in the checked-in WKTs (none declare
extension ranges), so no regen.
buffa-codegen: descriptor.proto types now resolve to
buffa-descriptor, not buffa-types. The auto-injected WKT
extern_path .google.protobuf → ::buffa_types::google::protobuf
covers everything in the google.protobuf package, including
descriptor.proto types — but buffa-types only ships the
JSON-mappable WKTs. Any proto referencing a descriptor.proto type as
a field — e.g. buf/validate/validate.proto, which has three optional google.protobuf.FieldDescriptorProto.Type fields — produced a
generated path that doesn't exist:
::buffa_types::google::protobuf::field_descriptor_proto::Type. An
internal file-level extern resolution now routes
google/protobuf/descriptor.proto to
::buffa_descriptor::generated::descriptor and
google/protobuf/compiler/plugin.proto to
::buffa_descriptor::generated::compiler, taking priority over the
package-level WKT mapping. Suppression mirrors the WKT mapping: a user
.google.protobuf extern_path overrides it (preserving the long-standing
behaviour that the override covers descriptor types too), and a file in
files_to_generate resolves locally. Consumers whose protos
import "google/protobuf/descriptor.proto" and reference its types as
fields must add buffa-descriptor to their [dependencies] — the
same way protos that reference WKTs require buffa-types. The
user-facing extern_path API is unchanged (still package-prefix keyed).
buffa: closed-enum JSON helpers no longer require the enum to
impl Deserialize. opt_closed_enum, repeated_closed_enum, and
map_closed_enum deserialized via serde_json::from_value::<E>(),
which bound E: DeserializeOwned. That meant a closed-enum field whose
enum type lives in an externally-generated crate built without
generate_json — e.g. google.protobuf.FieldDescriptorProto.Type from
buffa-descriptor, referenced by buf/validate/validate.proto — could
not satisfy the bound and refused to compile under json=true codegen.
The helpers now decode the buffered serde_json::Value directly via the
Enumeration trait (from_proto_name, from_i32, default for null),
which is the same dispatch the codegen-emitted Deserialize impl
performs anyway. The DeserializeOwned bound is removed (a relaxation —
non-breaking). Lenient mode (ignore_unknown_enum_values) is unchanged:
any element that fails to decode — unknown variant, out-of-range
integer, or wrong JSON type — is dropped from the container / leaves the
optional unset, exactly as before. Additionally, that lenient filtering
for closed-enum containers now works under no_std: the previous
implementation needed the std-only scoped strict-mode override to
surface a distinguishable error from the inner deserialize, but the new
Enumeration-direct dispatch has no inner deserialize to override.
The workspace [profile.release] now sets lto = true and
codegen-units = 1. This shrinks the prebuilt protoc-gen-buffa /
protoc-gen-buffa-packaging release binaries by roughly 20% at the cost of
~2× clean release-build time. Cargo only honors profile sections from the
top-level workspace, so library consumers of buffa / buffa-build do not
inherit this — set [profile.release] in your own workspace (or
CARGO_PROFILE_RELEASE_LTO=true for cargo install) to get the same
benefit. (#60)
buffa-codegen: empty ancillary content files and modules are no
longer emitted. A .proto with no oneofs / no extension declarations
/ views=false previously produced placeholder
<stem>.__oneof.rs / <stem>.__ext.rs / <stem>.__view.rs /
<stem>.__view_oneof.rs files containing only the @generated header,
and the package stitcher unconditionally authored a
pub mod __buffa { pub mod oneof { ... } pub mod ext { ... } ... }
tree that include!d them. Codegen now omits an ancillary content file
when it would be empty, the stitcher only include!s files that exist,
and the __buffa wrapper (and each view / oneof / ext submodule
inside it) is itself omitted when it would be empty — so a package with
only owned messages emits no __buffa block at all. Eliminates pure
noise in generated trees, editor file lists, search, and review diffs.
Consumers with checked-in generated code will see file deletions
and stitcher diffs on regeneration; remove orphaned empty files. The
__buffa::* paths are an internal sentinel namespace (consumers reach
for the natural-path re-exports added in 0.5.0), so no supported public
surface changes — but a hand-written
use crate::pkg::__buffa::oneof::*; for a package that has no oneofs
would now fail to resolve (it was previously a no-op import of an
empty module). (#107)
ci(cla): bump cla-github-action pin to v3.0.0 by @iainmcgin in #103
Full Changelog: v0.5.1...v0.5.2
buffa-codegen: oneof Serialize match arms now use Self::#variant.
generate_oneof_serialize emits the manual JSON serde impl as
impl Serialize for #enum_ident { fn serialize(&self, …) { match self { … } } },
where Self resolves to the oneof enum. The match arms used the
fully-qualified #enum_ident::#variant form, which trips
clippy::use_self in workspaces that opt it on — particularly visible
under connectrpc-build, which doesn't carry an inner #![allow(...)]
the way protoc-gen-buffa-packaging does, so the oneof companion file
inherits the surrounding mod's lint set. The deserialize arms in
oneof_variant_deser_arm remain qualified because they construct the
oneof from inside the message's Deserialize impl, where Self would
be wrong. No behavioural change.buffa-codegen: enum JSON deserialize errors use inlined format args.
The enum visitor's range-check and unknown-value error messages used
positional format!("enum value {} out of i32 range", v) etc., which
trip clippy::uninlined_format_args for the same reason as above (the
enum impls live in the per-proto Owned content, outside the __buffa
#[allow(...)] block). Now format!("enum value {v} out of i32 range") etc. — semantically identical, lint-clean regardless of which
module wrapper covers it.deps: bump rand to 0.9.4 in gen-datasets ( GHSA-cq8v-f236-94qc ) by @iainmcgin in #98
Full Changelog: v0.5.0...v0.5.1
buffa-codegen: ALLOW_LINTS now includes unused_qualifications.
Cross-proto references within the same package are emitted through the
canonical super::super::__buffa::view::… (and …::oneof::…) path even
though the target lives in the same generated module. The bare name would
resolve, but the canonical path is stable when a sibling proto defines a
same-named natural-path re-export. Workspaces that opt
unused_qualifications = "warn" and build with -D warnings were getting
false positives from generated code; the lint is now in the package
stitcher's #[allow(...)] block alongside dead_code, unused_imports,
etc.ci: bump cla-github-action to 1ecf0d2f (impersonation guard, co-author trailers) by @iainmcgin in #72
Full Changelog: v0.4.0...v0.5.0
This release is a minor bump under the
Rust 0.x convention:
the only API break is #[non_exhaustive] on buffa_codegen::GeneratedFileKind
(see Changed below), which affects downstream code generators only — it does
not change the runtime API. Everything else is additive.
Consumers with checked-in generated code must regenerate with the 0.5.0
toolchain before depending on the 0.5.0 runtime crates: generated code from
0.5.0's buffa-codegen references ViewReborrow, decode_bytes_to_bytes,
and __private::arbitrary_bytes, none of which exist in buffa 0.4.0.
buffa_codegen::GeneratedFileKind is now #[non_exhaustive]. Match it
with a wildcard arm — future kinds can then be added without a major
version bump. Build integrations that compare with == (the common case,
including connect-rust) are unaffected.buffa_codegen::GeneratedFileKind::Companion and apply_companions let
downstream code generators (e.g. connect-rust) supply extra per-proto
files that buffa wires into the per-package stitcher, instead of having
to mislabel them as GeneratedFileKind::Owned and rely on filename
matching. Companion files are include!d at package root alongside
owned message types. (#81)
OwnedView<V> gains a reborrow<'b>(&'b self) -> &'b V::Reborrowed<'b> method
that makes the internal 'static lifetime visible as 'b (the lifetime of the
borrow), so view fields can be passed into functions or return types bounded by
the OwnedView's lifetime. Requires V: ViewReborrow, a safe trait whose
reborrow method body is a covariance-checked subtype coercion; codegen emits
the impl automatically for every generated view type. Hand-written view types
opt in with impl ViewReborrow for MyView<'static> { type Reborrowed<'b> = MyView<'b>; fn reborrow<'b>(this: &'b Self) -> &'b Self::Reborrowed<'b> { this } }
— the body fails to compile for invariant view types, so no unsafe is needed.
(#82)
Codegen now emits "natural-path" pub use re-exports for ancillary types
(views, oneof enums, view-of-oneof enums, file-level extension consts,
register_types) at the module path you'd write first — pkg::FooView,
pkg::foo::Kind, pkg::foo::KindView, etc. The canonical __buffa::
paths are unchanged and remain what generated code and downstream codegen
always reference; the re-exports are purely an ergonomic convenience and
are silently skipped when the natural name is already taken by a real
proto item or by another candidate re-export. Because of that skip rule,
adding a proto type whose name shadows a re-export (e.g. message FooView
next to message Foo) can silently rebind a natural path between releases
— the canonical __buffa:: path is always stable; use it directly when a
natural import stops resolving (see examples/conflicts for one alias
convention). (#80)
Doc comments in generated Rust code now resolve AIP-192 proto type cross-references
([Book][google.example.v1.Book], [Book][]) to rustdoc intra-doc links.
Only type-level refs are resolved; member refs such as [Genre.GENRE_SCI_FI][]
fall back to escaped literals. Unknown or cross-crate references also fall back
silently. (#26)
protoc-gen-buffa and protoc-gen-buffa-packaging now respond to
--version / -V and --help / -h instead of blocking on stdin.
Any other command-line argument prints a "this is a protoc plugin" hint
to stderr and exits non-zero.
buffa::types::decode_bytes_to_bytes reads a length-delimited bytes field
into a bytes::Bytes via Buf::copy_to_bytes. When decoding from a
Bytes-backed buffer this is a zero-copy refcount bump. Generated
merge_field arms for bytes_fields-tagged fields (singular, optional,
repeated, and oneof) now use it instead of Bytes::from(decode_bytes(..)?),
eliminating one allocation + memcpy per field on the owned decode path. Note
that in the zero-copy case the resulting field aliases the source
allocation, so the source buffer is freed only once every aliased field is
dropped. Consumers with checked-in generated code must regenerate to pick
this up. (#53)
buffa-types --features arbitrary now compiles. Any.value is
bytes::Bytes (since 0.4.0 / #51), which has no Arbitrary impl.
Codegen now emits #[arbitrary(with = ::buffa::__private::arbitrary_bytes*)]
on every bytes_fields-typed field — singular, optional, and repeated
struct fields plus oneof variant inner fields — when
generate_arbitrary = true, so the struct-level derive(Arbitrary)
succeeds. Map values are unaffected (they are always Vec<u8> regardless
of bytes_fields). The same fix covers any user crate that uses
bytes_fields + generate_arbitrary. cargo doc --workspace --all-features and cargo clippy --workspace --all-features are also
unblocked, and CI now runs cargo check --workspace --all-features to
prevent recurrence.
(#88)
write_to now emits fields in ascending field-number order regardless of
cardinality (singular / repeated / map / oneof), matching prost,
protoc-C++, and the spec's serialize-in-field-order recommendation.
Previously fields were emitted grouped by kind, which broke
byte-equivalence with other implementations for messages mixing a
high-numbered singular field with a lower-numbered repeated/map/oneof.
Decoders accept any order, so this is not a wire-compat break, but
consumers content-addressing serialized bytes (e.g. hash(encode(msg)))
will see different hashes for affected message shapes.
(#75)
Ancillary generated types moved under `pkg::__buffa::`. View structs, oneof enums, view-of-oneof enums, extension consts, and register_types no longer
Ancillary generated types moved under pkg::__buffa::. View structs,
oneof enums, view-of-oneof enums, extension consts, and register_types
no longer share the package-level Rust namespace with owned message
structs. The new layout:
| Item | Before | After |
|---|---|---|
| View struct | pkg::FooView |
pkg::__buffa::view::FooView |
| Nested view | pkg::foo::BarView |
pkg::__buffa::view::foo::BarView |
| Oneof enum | pkg::foo::KindOneof |
pkg::__buffa::oneof::foo::Kind |
| View-of-oneof | pkg::foo::KindOneofView |
pkg::__buffa::view::oneof::foo::Kind |
| Extension const | pkg::FOO |
pkg::__buffa::ext::FOO |
| Registration fn | pkg::register_types (per file) |
pkg::__buffa::register_types (per package) |
Owned message structs and nested-type modules are unchanged. Migration is
a mechanical path rewrite per the table above. The Oneof / OneofView
suffixes are dropped — the parallel module tree disambiguates.
This makes name collisions between user proto types and codegen-derived
ancillary names structurally impossible. __buffa is the only name
codegen reserves in user namespace; it aligns with the existing __buffa_
reserved field-name prefix. A proto message, file-level enum, or package
segment that snake-cases to __buffa is rejected with
CodeGenError::ReservedModuleName.
Consumer include pattern: use buffa::include_proto!("dotted.pkg").
Codegen now emits a per-package <dotted.pkg>.mod.rs stitcher alongside
the per-proto content files. Hand-authored
include!(concat!(env!("OUT_DIR"), "/my_file.rs")) blocks no longer
produce a complete module; replace with:
pub mod my_pkg {
buffa::include_proto!("my.pkg");
}
buffa-build's generate_include_file() already emits the correct
structure; consumers using that helper need no change.
__buffa_cached_size is removed from all generated structs (owned and
view); Message::compute_size / write_to and ViewEncode::compute_size /
write_to now take a &mut SizeCache parameter. Sizes are recorded in an
external pre-order Vec<u32> cache that the provided encode* methods
construct internally, so generated types contain only their proto fields
plus __buffa_unknown_fields — no interior mutability, structurally
Send + Sync, and concurrent encode() of the same &msg from multiple
threads is sound. Message::cached_size() and ViewEncode::cached_size()
are removed; use the new provided encoded_len() to get the size alone.
__private::CachedSize is removed. Hand-written Message / ViewEncode
impls must add the cache: &mut SizeCache parameter, drop cached_size(),
and (for nested message fields) wrap recursion in cache.reserve() /
cache.set(); see the custom-types section of the user
guide for the pattern.
(#14,
#22)
DefaultInstance and DefaultViewInstance are no longer unsafe traits,
and HasDefaultViewInstance is removed. The liveness and immutability
invariants are fully encoded by the return type and cannot be violated by
a safe implementation. DefaultViewInstance is now implemented for
FooView<'v> at every lifetime (not just 'static), with
fn default_view_instance<'a>() -> &'a Self where Self: 'a; the
covariant lifetime coercion happens in the impl body where the compiler
checks it via ordinary subtyping, eliminating the raw pointer cast in
Deref for MessageFieldView. Hand-written impls must drop the unsafe
keyword and adopt the new method signature; the separate
HasDefaultViewInstance impl is no longer needed.
(#68,
#69)
CodeGenError::OneofNameConflict and ::ViewNameConflict removed.
These collisions are now structurally impossible (the inputs that
previously triggered them produce valid output).
google.protobuf.Any.value is now ::bytes::Bytes instead of Vec<u8>.
Makes Any::clone() a cheap refcount bump (up to ~170x faster for large
payloads) instead of a full memcpy. Call sites constructing an Any by hand
need .into() on the payload (e.g. value: my_vec.into(), or pass Bytes
directly). Reading any.value is unchanged — Bytes derefs to &[u8].
buffa-types now depends on bytes unconditionally.
buffa_build::proto_path_to_rust_module — consumers should use
the per-package <pkg>.mod.rs stitcher path via buffa::include_proto!
instead.ViewEncode<'a> — serialization from borrowed view types. Generated
*View<'a> types implement ViewEncode (whenever views are generated,
i.e. generate_views(true), the default) with the same two-pass
compute_size/write_to model as Message. Views can be constructed
from borrowed &'a str / &'a [u8] and encoded without intermediate
String/Vec allocation. Benchmarks: parity on serialize-only; ~6× on
build+encode for a 15-label string-map message.buffa::include_proto!("dotted.pkg") macro — wraps the per-package
.mod.rs stitcher; the canonical consumer integration point.MapView::new(Vec) / From<Vec> / FromIterator for constructing
map views directly (for ViewEncode).SizeCache — external pre-order size table for the two-pass encode
protocol ([u32; 16] inline + Vec<u32> spill, allocation-free for ≤16
nested LEN sub-messages). The provided encode*() methods construct one
internally; for hot loops, the new encode_with_cache(&mut SizeCache, buf)
reuses a single cache across calls.Message::encoded_len() / ViewEncode::encoded_len() — provided
method returning the serialized size without writing (replaces
compute_size()-then-discard).Enumeration::values() — &'static [Self] slice of all variants for
iteration.buffa-build / buffa-codegen: type_attribute, field_attribute,
message_attribute, enum_attribute — attach Rust attributes (e.g.
#[derive(...)], #[serde(...)]) to specific generated types or fields
by proto path.protoc-gen-buffa: text=true and allow_message_set=true plugin
parameters — match the existing buffa-build config flags.#[must_use] on Message/ViewEncode compute_size, encoded_len,
encode_to_vec, encode_to_bytes.Option — anywhere in the proto package, including
nested in a sibling message or in another file — no longer shadows
core::option::Option in generated optional/oneof field types and the
JSON deserialize path. Generated code now always emits the
fully-qualified ::core::option::Option.
(#36,
#64)self → variant Self) are now escaped.
(#47)RegionCodes
case) and Foo next to FooView (gh#32) — both now structurally
resolved by the __buffa:: namespacing above.…which installs all maps in one call. The deprecated functions still work.
Extension::new(number) → Extension::new(number, extendee). Same for
Extension::with_default. Codegen consumers are unaffected — the pub const
items are regenerated. Hand-written Extension consts (unusual) need the
extendee string added.ExtensionSet trait gained a required const PROTO_FQN: &'static str.
Codegen consumers are unaffected. Hand-written impls need the const added.extension(), set_extension(), clear_extension() now panic on extendee
mismatch (previously: silently returned None / no-op). has_extension()
returns false gracefully. Catches field_options.extension(&MESSAGE_OPTION)
bugs at the first call site; matches protobuf-go (panics) and protobuf-es
(throws).set_any_registry, set_extension_registry — use
buffa::type_registry::set_type_registry instead, which installs all maps
in one call. The deprecated functions still work.AnyTypeEntry → JsonAnyEntry, ExtensionRegistryEntry → JsonExtEntry.
Type aliases for one release cycle. The text-format fields have moved to
separate TextAnyEntry / TextExtEntry structs in type_registry.Extension<C> typed descriptors,
ExtensionSet trait with extension/set_extension/has_extension/
clear_extension/extension_or_default, codec types for every proto field
type (including GroupCodec for editions DELIMITED / proto2 groups),
proto2 [default = ...] on extension declarations, and MessageSet wire
format behind CodeGenConfig::allow_message_set. See the
Extensions section of the user guide.TypeRegistry — unified registry covering Any type entries and
extension entries for both JSON and text formats. Codegen emits
register_types(&mut TypeRegistry) per file; call once per generated file,
then set_type_registry(reg). JSON entries (JsonAnyEntry, JsonExtEntry)
and text entries (TextAnyEntry, TextExtEntry) live in feature-split
maps so json and text are independently enableable.JsonParseOptions::strict_extension_keys — error on unregistered "[...]"
JSON keys (default: silently drop, matching pre-0.3 behavior for all unknown
keys).features.message_encoding = DELIMITED — fully supported in
codegen, previously parsed but ignored. Message fields with this feature use
the group wire format (StartGroup/EndGroup) instead of length-prefixed.textproto) — the buffa::text module provides
TextFormat trait, TextEncoder, TextDecoder, and encode_to_string /
decode_from_str conveniences. Enable with features = ["text"]
(zero-dependency, no_std-compatible) and Config::generate_text(true).
Covers Any expansion ([type.googleapis.com/...] { ... }), extension
brackets ([pkg.ext] { ... }), and group/DELIMITED naming. Any expansion
and extension brackets consult the text maps in TypeRegistry — the json
and text features are independently enableable. Passes the full
text-format conformance suite (883/883).TestAllTypesEdition2023 enabled; binary+JSON 5539 → 5549
passing (std). Text format suite 0 → 883 passing (was entirely skipped).buffa-descriptor crate — FileDescriptorProto and friends are now in a
standalone crate that depends only on buffa, so descriptor types are usable
without pulling in quote/syn/prettyplease. buffa-codegen re-exports
the module so existing buffa_codegen::generated::* paths still resolve.
(#8).proto files are now
emitted as /// doc comments on generated structs, fields, enums, variants,
and view types. Requires --include_source_info (set automatically by
buffa-build and the protoc plugins).
(#7)buffa::encoding::MAX_FIELD_NUMBER constant ((1 << 29) - 1), replacing
the magic number at all call sites.
(#21)buffa-build skips writing unchanged outputs, avoiding mtime bumps that
trigger needless downstream recompilation.
(#17)Self in impl blocks instead of repeating the
type name, so consumer crates that enable clippy::use_self get clean
output. (#15)optional field whose synthetic oneof PascalCases to the same
name. (#20,
fixes #12)[foo][] reference-style links or bare URLs — these are now escaped so
rustdoc treats them as literal text.
(#25)`protoc-gen-buffa`: the `mod_file= ` option is removed. Module tree assembly (mod.rs generation) is now a separate plugin, protoc-gen-buffa-packaging.
protoc-gen-buffa: the mod_file=<name> option is removed. Module tree
assembly (mod.rs generation) is now a separate plugin,
protoc-gen-buffa-packaging. The codegen plugin emits per-file .rs only
and no longer requires strategy: all.
Migration - replace this:
plugins:
- local: protoc-gen-buffa
out: src/gen
strategy: all
opt: [mod_file=mod.rs]
with this:
plugins:
- local: protoc-gen-buffa
out: src/gen
- local: protoc-gen-buffa-packaging
out: src/gen
strategy: all
Passing mod_file= to the 0.2 plugin is a hard error with a migration hint
(not a silent no-op).
protoc-gen-buffa-packaging - new protoc plugin that emits a mod.rs
module tree for per-file output. Works with any codegen plugin that follows
buffa's per-file naming convention (foo/v1/bar.proto -> foo.v1.bar.rs).
Invoke once per output tree; compose via multiple buf.gen.yaml entries.
Optional filter=services restricts the tree to proto files that declare
at least one service, for packaging service-stub-only output from plugins
layered on buffa. Released as standalone binaries for the same five targets
as protoc-gen-buffa, with SLSA provenance and cosign signatures.
buffa-codegen: "." accepted as a catch-all extern_path prefix.
extern_path = (".", "crate::proto") maps every proto package to an
absolute Rust path rooted at crate::proto. More-specific mappings (including
the auto-injected WKT mapping) still win via longest-prefix-match.
buffa, buffa-types, buffa-codegen, and buffa-build have no breaking
API changes in this release. The version bump reflects the
protoc-gen-buffa CLI change; library consumers upgrading from 0.1 should
see no code changes required.
| Binary wire format (proto2, proto3, editions 2023/2024) | ✅ |
Initial release.
| Feature | Status |
|---|---|
| Binary wire format (proto2, proto3, editions 2023/2024) | ✅ |
| Proto3 JSON canonical mapping | ✅ |
| Well-known types (Timestamp, Duration, Any, Struct, Value, FieldMask, wrappers) | ✅ |
| Unknown field preservation | ✅ (default on) |
| Zero-copy view types | ✅ |
Open enums (EnumValue<E>) with unknown-value preservation |
✅ |
| Closed enums (proto2) with unknown-value routing to unknown fields | ✅¹ |
| proto2 groups (singular, repeated, oneof) | ✅ |
proto2 custom defaults ([default = X]) |
✅ on required; optional stays None |
Editions feature resolution (field_presence, enum_type, repeated_field_encoding, utf8_validation) |
✅ |
Editions message_encoding = DELIMITED |
⚠️ Parsed but ignored — see Known Limitations in README |
no_std + alloc (core runtime, views, JSON) |
✅ |
Text format (textproto) |
❌ Not planned |
| proto2 extensions | ❌ Not planned (use Any) |
| Runtime reflection | ❌ Not planned for 0.1 |
¹ See Known Limitations for two closed-enum edge cases (packed-repeated in views, map values).
Passes the protobuf conformance suite (v33.5):
IgnoreUnknownEnumStringValue* in repeated/map contexts, which requires scoped strict-mode override; no_std has set_global_json_parse_options for singular-enum accept-with-default but not container filtering)decode_view → to_owned_message → encode; direct JSON decode is not supported for views).proto files in the Google Cloud API setComparison against prost 0.13 (lower = buffa faster):
| Operation | buffa vs prost |
|---|---|
| Binary encode | 0.56–0.74× (26–44% faster) |
| Binary decode | 0.91–1.29× (mixed; deep-nested messages slower) |
| JSON encode | 0.97–1.08× (parity) |
| JSON decode | 0.40–0.88× (12–60% faster) |
See the README Performance section for charts and raw data.
This release publishes:
buffa — core runtimebuffa-types — well-known types (Timestamp, Duration, Any, etc.)buffa-codegen — descriptor → Rust source (for downstream code generators)buffa-build — build.rs integrationprotoc-gen-buffa — protoc plugin binary (also released as standalone binaries for linux-x86_64, linux-aarch64, darwin-x86_64, darwin-aarch64, windows-x86_64)MSRV: Rust 1.85.
Your coding agent can read these notes before it upgrades. Set up the MCP server →