Skip to main content

buffa_descriptor/
features.rs

1//! Edition feature resolution.
2//!
3//! Protoc emits only *unresolved* (user-written) features in descriptor
4//! options.  This module resolves them by walking the descriptor hierarchy
5//! (file → message → field/enum/oneof) and merging overrides onto edition
6//! defaults, matching the algorithm in protobuf's C++ `FeatureResolver`.
7//!
8//! Used by both [`crate::pool::DescriptorPool`] (runtime reflection) and
9//! `buffa-codegen` (re-exported via `buffa_codegen::features`). Codegen adds
10//! one extra resolver, `resolve_field`, that needs `CodeGenContext` and so
11//! lives in `buffa-codegen`.
12
13pub use buffa::editions::{
14    EnumType, FieldPresence, JsonFormat, MessageEncoding, RepeatedFieldEncoding, ResolvedFeatures,
15    Utf8Validation,
16};
17
18use crate::generated::descriptor::feature_set::{
19    EnumType as FeatureSetEnumType, FieldPresence as FeatureSetFieldPresence,
20    JsonFormat as FeatureSetJsonFormat, MessageEncoding as FeatureSetMessageEncoding,
21    RepeatedFieldEncoding as FeatureSetRepeatedFieldEncoding,
22    Utf8Validation as FeatureSetUtf8Validation,
23};
24use crate::generated::descriptor::{
25    DescriptorProto, Edition, EnumDescriptorProto, FeatureSet, FieldDescriptorProto,
26    FileDescriptorProto, OneofDescriptorProto,
27};
28
29/// Resolve a file's effective features from its declared syntax/edition and
30/// any explicit file-level feature overrides.
31#[must_use]
32pub fn for_file(file: &FileDescriptorProto) -> ResolvedFeatures {
33    match file.syntax.as_deref() {
34        Some("proto3") => {
35            let base = ResolvedFeatures::proto3_defaults();
36            merge(base, file_features(file))
37        }
38        Some("editions") => {
39            let edition = file.edition.unwrap_or(Edition::EDITION_UNKNOWN);
40            let base = for_edition(edition);
41            merge(base, file_features(file))
42        }
43        // proto2 or absent
44        _ => {
45            let base = ResolvedFeatures::proto2_defaults();
46            merge(base, file_features(file))
47        }
48    }
49}
50
51/// Compute a child element's resolved features by merging the parent's
52/// resolved features with the child's unresolved `FeatureSet` override.
53///
54/// If `child_features` is `None`, returns the parent unchanged.
55#[must_use]
56pub fn resolve_child(
57    parent: &ResolvedFeatures,
58    child_features: Option<&FeatureSet>,
59) -> ResolvedFeatures {
60    merge(*parent, child_features)
61}
62
63/// Map an `Edition` value to the corresponding default features.
64fn for_edition(edition: Edition) -> ResolvedFeatures {
65    match edition {
66        Edition::EDITION_PROTO2 | Edition::EDITION_LEGACY => ResolvedFeatures::proto2_defaults(),
67        Edition::EDITION_PROTO3 => ResolvedFeatures::proto3_defaults(),
68        // Edition 2024 is finalized: its only additions over 2023 are
69        // source-retained features (enforce_naming_style, default_symbol_visibility)
70        // which protoc enforces and strips before the descriptor reaches a plugin.
71        // From a runtime perspective 2023 and 2024 are permanently equivalent.
72        Edition::EDITION_2023 | Edition::EDITION_2024 => ResolvedFeatures::edition_2023_defaults(),
73        // EDITION_UNSTABLE and the *_TEST_ONLY editions are intentionally not
74        // supported; fall back to 2023 defaults so experimental descriptors
75        // at least decode rather than panic.
76        _ => ResolvedFeatures::edition_2023_defaults(),
77    }
78}
79
80/// Merge an optional unresolved `FeatureSet` onto a resolved set.
81/// Each field in the child that is set (not `None` / not `UNKNOWN`)
82/// overrides the corresponding parent value.
83fn merge(parent: ResolvedFeatures, features: Option<&FeatureSet>) -> ResolvedFeatures {
84    let Some(fs) = features else {
85        return parent;
86    };
87    ResolvedFeatures {
88        field_presence: fs
89            .field_presence
90            .and_then(convert_field_presence)
91            .unwrap_or(parent.field_presence),
92        enum_type: fs
93            .enum_type
94            .and_then(convert_enum_type)
95            .unwrap_or(parent.enum_type),
96        repeated_field_encoding: fs
97            .repeated_field_encoding
98            .and_then(convert_repeated_field_encoding)
99            .unwrap_or(parent.repeated_field_encoding),
100        utf8_validation: fs
101            .utf8_validation
102            .and_then(convert_utf8_validation)
103            .unwrap_or(parent.utf8_validation),
104        message_encoding: fs
105            .message_encoding
106            .and_then(convert_message_encoding)
107            .unwrap_or(parent.message_encoding),
108        json_format: fs
109            .json_format
110            .and_then(convert_json_format)
111            .unwrap_or(parent.json_format),
112    }
113}
114
115// ── Feature extractors ──────────────────────────────────────────────────
116
117/// Extract the unresolved `FeatureSet` from file options.
118fn file_features(file: &FileDescriptorProto) -> Option<&FeatureSet> {
119    file.options
120        .as_option()
121        .and_then(|o| o.features.as_option())
122}
123
124/// Extract the unresolved `FeatureSet` from message options.
125#[must_use]
126pub fn message_features(msg: &DescriptorProto) -> Option<&FeatureSet> {
127    msg.options.as_option().and_then(|o| o.features.as_option())
128}
129
130/// Extract the unresolved `FeatureSet` from field options.
131#[must_use]
132pub fn field_features(field: &FieldDescriptorProto) -> Option<&FeatureSet> {
133    field
134        .options
135        .as_option()
136        .and_then(|o| o.features.as_option())
137}
138
139/// Extract the unresolved `FeatureSet` from enum options.
140#[must_use]
141pub fn enum_features(e: &EnumDescriptorProto) -> Option<&FeatureSet> {
142    e.options.as_option().and_then(|o| o.features.as_option())
143}
144
145/// Extract the unresolved `FeatureSet` from oneof options.
146#[must_use]
147pub fn oneof_features(o: &OneofDescriptorProto) -> Option<&FeatureSet> {
148    o.options.as_option().and_then(|o| o.features.as_option())
149}
150
151// ── Descriptor enum → runtime enum converters ───────────────────────────
152//
153// The generated descriptor enums (e.g. `FeatureSetFieldPresence`) have an
154// `UNKNOWN = 0` variant that means "not set".  We convert to the runtime
155// enums, returning `None` for unknown/unset so the merge uses the parent.
156
157fn convert_field_presence(v: FeatureSetFieldPresence) -> Option<FieldPresence> {
158    match v {
159        FeatureSetFieldPresence::EXPLICIT => Some(FieldPresence::Explicit),
160        FeatureSetFieldPresence::IMPLICIT => Some(FieldPresence::Implicit),
161        FeatureSetFieldPresence::LEGACY_REQUIRED => Some(FieldPresence::LegacyRequired),
162        FeatureSetFieldPresence::FIELD_PRESENCE_UNKNOWN => None,
163    }
164}
165
166fn convert_enum_type(v: FeatureSetEnumType) -> Option<EnumType> {
167    match v {
168        FeatureSetEnumType::OPEN => Some(EnumType::Open),
169        FeatureSetEnumType::CLOSED => Some(EnumType::Closed),
170        FeatureSetEnumType::ENUM_TYPE_UNKNOWN => None,
171    }
172}
173
174fn convert_repeated_field_encoding(
175    v: FeatureSetRepeatedFieldEncoding,
176) -> Option<RepeatedFieldEncoding> {
177    match v {
178        FeatureSetRepeatedFieldEncoding::PACKED => Some(RepeatedFieldEncoding::Packed),
179        FeatureSetRepeatedFieldEncoding::EXPANDED => Some(RepeatedFieldEncoding::Expanded),
180        FeatureSetRepeatedFieldEncoding::REPEATED_FIELD_ENCODING_UNKNOWN => None,
181    }
182}
183
184fn convert_utf8_validation(v: FeatureSetUtf8Validation) -> Option<Utf8Validation> {
185    match v {
186        FeatureSetUtf8Validation::VERIFY => Some(Utf8Validation::Verify),
187        FeatureSetUtf8Validation::NONE => Some(Utf8Validation::None),
188        FeatureSetUtf8Validation::UTF8_VALIDATION_UNKNOWN => None,
189    }
190}
191
192fn convert_message_encoding(v: FeatureSetMessageEncoding) -> Option<MessageEncoding> {
193    match v {
194        FeatureSetMessageEncoding::LENGTH_PREFIXED => Some(MessageEncoding::LengthPrefixed),
195        FeatureSetMessageEncoding::DELIMITED => Some(MessageEncoding::Delimited),
196        FeatureSetMessageEncoding::MESSAGE_ENCODING_UNKNOWN => None,
197    }
198}
199
200fn convert_json_format(v: FeatureSetJsonFormat) -> Option<JsonFormat> {
201    match v {
202        FeatureSetJsonFormat::ALLOW => Some(JsonFormat::Allow),
203        FeatureSetJsonFormat::LEGACY_BEST_EFFORT => Some(JsonFormat::LegacyBestEffort),
204        FeatureSetJsonFormat::JSON_FORMAT_UNKNOWN => None,
205    }
206}
207#[cfg(test)]
208mod tests {
209    use super::*;
210    use crate::generated::descriptor::FeatureSetDefaults;
211
212    #[test]
213    fn proto2_file_returns_proto2_defaults() {
214        let file = FileDescriptorProto {
215            name: Some("test.proto".into()),
216            ..Default::default()
217        };
218        let f = for_file(&file);
219        assert_eq!(f, ResolvedFeatures::proto2_defaults());
220    }
221
222    #[test]
223    fn proto3_file_returns_proto3_defaults() {
224        let file = FileDescriptorProto {
225            name: Some("test.proto".into()),
226            syntax: Some("proto3".into()),
227            ..Default::default()
228        };
229        let f = for_file(&file);
230        assert_eq!(f, ResolvedFeatures::proto3_defaults());
231    }
232
233    #[test]
234    fn editions_2023_file_returns_edition_defaults() {
235        let file = FileDescriptorProto {
236            name: Some("test.proto".into()),
237            syntax: Some("editions".into()),
238            edition: Some(Edition::EDITION_2023),
239            ..Default::default()
240        };
241        let f = for_file(&file);
242        assert_eq!(f, ResolvedFeatures::edition_2023_defaults());
243    }
244
245    #[test]
246    fn editions_2024_shares_2023_defaults() {
247        let file = FileDescriptorProto {
248            name: Some("test.proto".into()),
249            syntax: Some("editions".into()),
250            edition: Some(Edition::EDITION_2024),
251            ..Default::default()
252        };
253        let f = for_file(&file);
254        assert_eq!(f, ResolvedFeatures::edition_2023_defaults());
255    }
256
257    #[test]
258    fn edition_proto2_maps_to_proto2_defaults() {
259        let file = FileDescriptorProto {
260            name: Some("test.proto".into()),
261            syntax: Some("editions".into()),
262            edition: Some(Edition::EDITION_PROTO2),
263            ..Default::default()
264        };
265        let f = for_file(&file);
266        assert_eq!(f, ResolvedFeatures::proto2_defaults());
267    }
268
269    #[test]
270    fn edition_legacy_maps_to_proto2_defaults() {
271        assert_eq!(
272            for_edition(Edition::EDITION_LEGACY),
273            ResolvedFeatures::proto2_defaults()
274        );
275    }
276
277    #[test]
278    fn child_inherits_parent_when_no_override() {
279        let parent = ResolvedFeatures::proto2_defaults();
280        let child = resolve_child(&parent, None);
281        assert_eq!(parent, child);
282    }
283
284    #[test]
285    fn child_partial_override() {
286        let parent = ResolvedFeatures::proto3_defaults();
287        let override_fs = FeatureSet {
288            enum_type: Some(FeatureSetEnumType::CLOSED),
289            ..Default::default()
290        };
291        let child = resolve_child(&parent, Some(&override_fs));
292        assert_eq!(child.enum_type, EnumType::Closed);
293        assert_eq!(child.field_presence, FieldPresence::Implicit);
294        assert_eq!(child.repeated_field_encoding, RepeatedFieldEncoding::Packed);
295    }
296
297    #[test]
298    fn child_full_override() {
299        let parent = ResolvedFeatures::proto3_defaults();
300        let override_fs = FeatureSet {
301            field_presence: Some(FeatureSetFieldPresence::EXPLICIT),
302            enum_type: Some(FeatureSetEnumType::CLOSED),
303            repeated_field_encoding: Some(FeatureSetRepeatedFieldEncoding::EXPANDED),
304            utf8_validation: Some(FeatureSetUtf8Validation::NONE),
305            message_encoding: Some(FeatureSetMessageEncoding::DELIMITED),
306            json_format: Some(FeatureSetJsonFormat::LEGACY_BEST_EFFORT),
307            ..Default::default()
308        };
309        let child = resolve_child(&parent, Some(&override_fs));
310        assert_eq!(child.field_presence, FieldPresence::Explicit);
311        assert_eq!(child.enum_type, EnumType::Closed);
312        assert_eq!(
313            child.repeated_field_encoding,
314            RepeatedFieldEncoding::Expanded
315        );
316        assert_eq!(child.utf8_validation, Utf8Validation::None);
317        assert_eq!(child.message_encoding, MessageEncoding::Delimited);
318        assert_eq!(child.json_format, JsonFormat::LegacyBestEffort);
319    }
320
321    #[test]
322    fn unknown_enum_values_are_treated_as_unset() {
323        let parent = ResolvedFeatures::edition_2023_defaults();
324        let override_fs = FeatureSet {
325            field_presence: Some(FeatureSetFieldPresence::FIELD_PRESENCE_UNKNOWN),
326            enum_type: Some(FeatureSetEnumType::ENUM_TYPE_UNKNOWN),
327            ..Default::default()
328        };
329        let child = resolve_child(&parent, Some(&override_fs));
330        assert_eq!(child.field_presence, parent.field_presence);
331        assert_eq!(child.enum_type, parent.enum_type);
332    }
333
334    #[test]
335    fn editions_file_with_file_level_override() {
336        let mut file = FileDescriptorProto {
337            name: Some("test.proto".into()),
338            syntax: Some("editions".into()),
339            edition: Some(Edition::EDITION_2023),
340            ..Default::default()
341        };
342        file.options
343            .get_or_insert_default()
344            .features
345            .get_or_insert_default()
346            .enum_type = Some(FeatureSetEnumType::CLOSED);
347
348        let f = for_file(&file);
349        assert_eq!(f.enum_type, EnumType::Closed);
350        assert_eq!(f.field_presence, FieldPresence::Explicit);
351        assert_eq!(f.repeated_field_encoding, RepeatedFieldEncoding::Packed);
352    }
353
354    #[test]
355    fn multi_level_hierarchy() {
356        // File: edition 2023 defaults (open enums, packed, explicit presence)
357        let file_features = for_edition(Edition::EDITION_2023);
358        assert_eq!(file_features.enum_type, EnumType::Open);
359
360        // Message: override to closed enums
361        let msg_override = FeatureSet {
362            enum_type: Some(FeatureSetEnumType::CLOSED),
363            ..Default::default()
364        };
365        let msg_features = resolve_child(&file_features, Some(&msg_override));
366        assert_eq!(msg_features.enum_type, EnumType::Closed);
367        assert_eq!(msg_features.field_presence, FieldPresence::Explicit);
368
369        // Field: override to implicit presence
370        let field_override = FeatureSet {
371            field_presence: Some(FeatureSetFieldPresence::IMPLICIT),
372            ..Default::default()
373        };
374        let field_features = resolve_child(&msg_features, Some(&field_override));
375        // Field-level override
376        assert_eq!(field_features.field_presence, FieldPresence::Implicit);
377        // Inherited from message
378        assert_eq!(field_features.enum_type, EnumType::Closed);
379        // Inherited from file/edition defaults
380        assert_eq!(
381            field_features.repeated_field_encoding,
382            RepeatedFieldEncoding::Packed
383        );
384    }
385
386    /// Resolve a `FeatureSet` from the protoc defaults into our
387    /// `ResolvedFeatures`, merging both the fixed and overridable halves.
388    ///
389    /// The protoc defaults split features into `fixed_features` (cannot be
390    /// overridden by users) and `overridable_features` (can be).  For the
391    /// purpose of computing the edition's effective defaults, we merge both:
392    /// start from fixed, then layer overridable on top.
393    ///
394    /// # Panics
395    ///
396    /// Panics if `field_presence` or `message_encoding` still hold
397    /// sentinel values after merging, which indicates protoc did not set
398    /// them in either half.  These two fields are checked because no
399    /// real edition defaults to `LegacyRequired` or `Delimited`.
400    fn resolve_protoc_defaults(
401        fixed: Option<&FeatureSet>,
402        overridable: Option<&FeatureSet>,
403    ) -> ResolvedFeatures {
404        // Merge both halves onto a deliberately distinguishable sentinel
405        // base, then verify every field was overwritten.
406        let sentinel = ResolvedFeatures {
407            field_presence: FieldPresence::LegacyRequired,
408            enum_type: EnumType::Closed,
409            repeated_field_encoding: RepeatedFieldEncoding::Expanded,
410            utf8_validation: Utf8Validation::None,
411            message_encoding: MessageEncoding::Delimited,
412            json_format: JsonFormat::LegacyBestEffort,
413        };
414        let after_fixed = merge(sentinel, fixed);
415        let result = merge(after_fixed, overridable);
416
417        // If any field still holds the sentinel value, protoc didn't set
418        // it in either half — our assumptions are wrong.
419        assert_ne!(
420            result.field_presence,
421            FieldPresence::LegacyRequired,
422            "protoc did not set field_presence in either fixed or overridable features"
423        );
424        assert_ne!(
425            result.message_encoding,
426            MessageEncoding::Delimited,
427            "protoc did not set message_encoding in either fixed or overridable features"
428        );
429
430        result
431    }
432
433    /// Verify that our hardcoded edition defaults match what protoc emits
434    /// via `--edition_defaults_out`.
435    ///
436    /// The golden file `conformance/edition_defaults.binpb` is generated
437    /// by running:
438    /// ```text
439    /// protoc --proto_path=<protobuf-src>/src \
440    ///     --edition_defaults_out=conformance/edition_defaults.binpb \
441    ///     google/protobuf/descriptor.proto
442    /// ```
443    /// It should be regenerated whenever the protoc/tools version changes.
444    #[test]
445    fn hardcoded_defaults_match_protoc_edition_defaults() {
446        use buffa::Message;
447
448        let binpb = include_bytes!("../../conformance/edition_defaults.binpb");
449        let defaults = FeatureSetDefaults::decode_from_slice(binpb)
450            .expect("failed to parse edition_defaults.binpb");
451
452        // Map each entry's edition to the expected hardcoded defaults.
453        // EDITION_2024 is included to verify it resolves to the same
454        // defaults as 2023 (the golden file's maximum_edition is 2023,
455        // so the lookup falls through to the 2023 entry).
456        let expected_mappings: &[(Edition, ResolvedFeatures)] = &[
457            (Edition::EDITION_LEGACY, ResolvedFeatures::proto2_defaults()),
458            (Edition::EDITION_PROTO2, ResolvedFeatures::proto2_defaults()),
459            (Edition::EDITION_PROTO3, ResolvedFeatures::proto3_defaults()),
460            (
461                Edition::EDITION_2023,
462                ResolvedFeatures::edition_2023_defaults(),
463            ),
464            (
465                Edition::EDITION_2024,
466                ResolvedFeatures::edition_2023_defaults(),
467            ),
468        ];
469
470        for &(target_edition, ref expected) in expected_mappings {
471            // Find the highest defaults entry with edition ≤
472            // target_edition, matching the algorithm from the protobuf
473            // implementation guide.
474            let entry = defaults
475                .defaults
476                .iter()
477                .rfind(|d| {
478                    d.edition
479                        .is_some_and(|e| (e as i32) <= (target_edition as i32))
480                })
481                .unwrap_or_else(|| panic!("no defaults entry for edition {target_edition:?}"));
482
483            let resolved = resolve_protoc_defaults(
484                entry.fixed_features.as_option(),
485                entry.overridable_features.as_option(),
486            );
487
488            assert_eq!(
489                &resolved, expected,
490                "defaults mismatch for edition {target_edition:?}: \
491                 protoc emitted {resolved:?}, we hardcode {expected:?}"
492            );
493        }
494    }
495}