Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changes/unreleased/fixed-20260903-134500.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
kind: Fixed
body: |-
**`DescriptorPool` rejects empty enum declarations** (#421), as `protoc` does ("Enums must contain at least one value"). Only hand-built or synthesized descriptor sets can reach this state; the rejection is the new `PoolError::EmptyEnum` variant.
time: 2026-09-03T13:45:00+02:00
8 changes: 8 additions & 0 deletions buffa-descriptor/src/pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ pub enum PoolError {
name: String,
number: i32,
},
/// An enum declares no values.
EmptyEnum { enum_name: String },
}

/// Renders an optional range bound for [`PoolError`] messages: the number,
Expand Down Expand Up @@ -472,6 +474,9 @@ impl core::fmt::Display for PoolError {
f,
"enum {enum_name} value {name:?} reuses number {number} without allow_alias"
),
Self::EmptyEnum { enum_name } => {
write!(f, "enum {enum_name} declares no values")
}
}
}
}
Expand Down Expand Up @@ -1683,6 +1688,9 @@ impl DescriptorPool {
format!("{parent_fqn}.{name}")
};
let enum_features = features::resolve_child(parent_features, features::enum_features(e));
if e.value.is_empty() {
return Err(PoolError::EmptyEnum { enum_name: fqn });
}
if enum_features.enum_type == EnumType::Open {
if let Some(first) = e.value.first() {
let number = first.number.unwrap_or(0);
Expand Down
46 changes: 40 additions & 6 deletions buffa-descriptor/tests/pool_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,14 @@ fn proto2_enum_first_value_can_be_nonzero() {
}

#[test]
fn open_enum_with_no_values_is_not_rejected_by_the_first_value_rule() {
fn empty_open_enum_reports_empty_enum_not_the_first_value_rule() {
use buffa_descriptor::generated::descriptor::{
EnumDescriptorProto, FileDescriptorProto, FileDescriptorSet,
};

// protoc rejects an empty enum for a different reason; this rule must
// not panic or misfire on `value.first()` being `None`.
// An empty enum is rejected as `EmptyEnum` (checked first, as protoc
// does); the open-enum first-value rule must not be the one that fires,
// nor panic on `value.first()` being `None`.
let result = DescriptorPool::new(FileDescriptorSet {
file: vec![FileDescriptorProto {
name: Some("proto3-empty-enum.proto".into()),
Expand All @@ -465,14 +466,47 @@ fn open_enum_with_no_values_is_not_rejected_by_the_first_value_rule() {
..Default::default()
});
assert!(
!matches!(
result,
Err(buffa_descriptor::PoolError::OpenEnumFirstValueNotZero { .. })
matches!(
&result,
Err(buffa_descriptor::PoolError::EmptyEnum { enum_name }) if enum_name == "valid.test.Empty"
),
"{result:?}"
);
}

#[test]
fn empty_enums_are_rejected_transactionally() {
use buffa_descriptor::generated::descriptor::{
EnumDescriptorProto, FileDescriptorProto, FileDescriptorSet,
};

let set = FileDescriptorSet {
file: vec![FileDescriptorProto {
name: Some("empty-enum.proto".into()),
package: Some("invalid.test".into()),
syntax: Some("proto3".into()),
enum_type: vec![EnumDescriptorProto {
name: Some("Empty".into()),
..Default::default()
}],
..Default::default()
}],
..Default::default()
};

assert_set_rejected_without_mutating_pool(
"empty-enum.proto",
"invalid.test.Empty",
set,
|err| {
assert!(matches!(
err,
PoolError::EmptyEnum { enum_name } if enum_name == "invalid.test.Empty"
));
},
);
}

#[test]
fn oneof_links() {
let p = pool();
Expand Down
Loading