Skip to content

descriptor: reject a field or extension typed as a map entry it does not own - #720

Open
veljjanoski wants to merge 4 commits into
anthropics:mainfrom
veljjanoski:descriptor-map-entry-field-type
Open

veljjanoski wants to merge 4 commits into
anthropics:mainfrom
veljjanoski:descriptor-map-entry-field-type

Conversation

@veljjanoski

@veljjanoski veljjanoski commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #637.

Problem

find_map_entry treated a repeated message field as a map when its type was a map_entry message nested in the same message, without comparing the entry's name with the field's. Every field that failed that test linked as an ordinary message field. So a second repeated field of an entry's type linked as a second map, a field of another message (or a singular field) of an entry type linked as a message field, and so did an extension of an entry type.

Fix

This follows protoc's checks (descriptor.cc:7915-7965 at 90229ff): a group may not have a map entry type, and a message field of a map entry type must be repeated, must share the entry's containing message, and must be named so that ToCamelCase(name, false) followed by Entry is the entry's name.

  • find_map_entry also requires that name. The new private map_entry_name is a port of it.
  • link_field returns the new PoolError::InvalidMapEntryUse { field, entry } when the resolved message type has map_entry = true and find_map_entry did not accept the field.
  • link_extension applies the same rule to an extension, with the extendee as its message. protoc's check accepts exactly one shape there: protoc 33.5 compiles extend Holder { repeated Holder.EntriesEntry entries = 100; } for a Holder with map<string, int32> entries, and rejects a singular one. That one shape still links, as a list of entries, as it did before (the map's wire form). Every other extension of a map entry type is rejected. protobuf-go rejects that shape too (desc_validate.go:254-256); if you would rather follow it, the check is one condition.
  • Pass 1 now puts a message's options into its placeholder, and pass 2 moves them into the linked descriptor (take(), so it is still one clone per message). Pass 2 links a message's fields before its nested messages, and can link another message's field before the entry, so without this link_field would see no options for the entry. With only this part reverted, 6 of the 7 rejection tests below fail (the extension one passes, since extensions link in pass 3).
  • tests/dynamic_e2e.rs: the hand-built maps in map_wire_type_pool used fields f1, f2, … with entries E1, E2, …, which protoc rejects. The entries are now F1Entry, F2Entry, …; the test is about wire types and is otherwise unchanged.

Net change outside tests: pool.rs +109/-13, including the new variant's rustdoc and Display text and the comments.

Not changed here: codegen's own find_map_entry (buffa-codegen/src/message.rs) does not check the entry name either, so for a hand-built set with a misnamed entry codegen still emits a map field. protoc output cannot reach this.

Tests

New in tests/map_entry_fields.rs. Each rejection is checked through both DescriptorPool::new and DescriptorPool::decode:

  • a second field of the entry's type, repeated and singular
  • a field named for another entry
  • a singular field named for the entry
  • a field of another message declared before and after the holder, repeated and singular
  • a group of the entry's type
  • a map in a nested message, valid and misnamed
  • extensions of the entry's type: singular, named for another entry, extending another message, and a group (rejected); the repeated one named for the entry of its extendee (links as a list)
  • valid names that exercise the rule: string_to_int, _leading, double__under, trailing_, v2_items, already_Upper

With both checks and the name test disabled, all 7 rejection tests fail; with the fix, all 16 tests in the file pass.

Run locally on Windows (x86_64-pc-windows-gnu, toolchain 1.95.0, protoc 33.5):

  • cargo fmt --all --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace: all pass except two buffa-types tests that fail on Windows only, because SystemTime there has 100 ns resolution
  • cargo test -p buffa-descriptor --all-features
  • task doc, task test-arbitrary, markdownlint
  • task conformance (Docker, tools image built locally with task tools-image-local): all 16 suites passed, 0 unexpected failures

AI-usage disclosure: Claude (Anthropic) was used as assistant.

@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

veljjanoski and others added 2 commits October 16, 2026 06:42
… expected entry

An extension whose type is a map entry now fails with
PoolError::ExtensionWithMapEntryType, whatever its shape. protoc accepts
one, a repeated extension of the entry's containing message that is
named for the entry, and treats it as a map. The pool has no map
extension, linking it as a list left the entry's own rules unchecked,
and the code buffa-codegen emits for it names an entry struct that is
not generated.

InvalidMapEntryUse gains expected_entry, the full name a map field of
that name requires of its entry, and its Display text gives the remedy
for the case at hand: the field's shape when the name is right, the
entry's name otherwise.

The docs say which rules are stricter than protoc 33.5 (the extension,
and a repeated group named for the entry), map_entry_name cites both
protoc functions it mirrors, and the fragment names the one shape that
linked as a working map before.

@iainmcgin iainmcgin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[claude code] Thanks. The field rules are a faithful port of protoc, the pass-1 options change holds on every path traced, and the no_std fix is in. Pushed one maintainer commit on top (a2bc0a0), with one maintainer decision and the review fixes:

  • Every extension whose type is a map entry is rejected, with the new PoolError::ExtensionWithMapEntryType, including the one shape protoc compiles. The pool has no map extension, linking it as a list left the entry shape unchecked, and the code buffa-codegen emits for it names an entry struct that is not generated. The docs name this as a rule stricter than protoc.
  • InvalidMapEntryUse gains expected_entry, the full name a map field of that name requires of its entry. Its Display text gives the remedy for the case at hand: the field shape when the name is already right, the entry name otherwise. Both texts are asserted in tests.
  • Parity wording: the line numbers in the description are from protobuf main. At v33.5, ValidateMapEntry (descriptor.cc:9135) has no group rule, so a repeated group named for the entry is accepted there. The pool still rejects it, and the variant doc says so.
  • Tests: the extension cases are one table of seven shapes, through new and decode; a rejection for a name that differs only in case; a_1b and FOO_BAR naming cases; the linked entry keeps its options.
  • The fragment is rewritten: the exact naming rule, the one shape that linked as a working map before and what to do about it, and the extension rule.

cargo test --workspace passes on this head (4279 passed, 0 failed), as do the no_std check and tests. The description will be updated to match.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

descriptor: a field typed as a map entry message it does not own links, as a plain message field or as a second map

2 participants