connectrpc_reflection/reflector.rs
1//! The descriptor index behind the reflection service.
2//!
3//! A [`Reflector`] answers the five queries of the gRPC server reflection
4//! protocol — file by name, file containing symbol, file containing
5//! extension, extension numbers of a type, and the service list — by
6//! delegating name resolution to a [`buffa_descriptor::DescriptorPool`].
7//!
8//! Two descriptor sources are supported:
9//!
10//! - **Wire-format `FileDescriptorSet` bytes**
11//! ([`from_descriptor_set_bytes`](Reflector::from_descriptor_set_bytes)),
12//! e.g. the output of `connectrpc_build::Config::emit_descriptor_set`.
13//! Responses carry the **original** per-file `FileDescriptorProto` bytes
14//! sliced out of the input — never a re-encode — so descriptor payloads
15//! produced by newer compilers survive byte-for-byte.
16//! - **An existing [`DescriptorPool`]**
17//! ([`from_descriptor_pool`](Reflector::from_descriptor_pool)), e.g. the
18//! `descriptor_pool()` a buffa-generated package exposes when reflection
19//! is enabled. Responses re-encode the pool's parsed
20//! `FileDescriptorProto`s; buffa retains unknown fields, so the bytes
21//! are semantically faithful but not guaranteed byte-identical to the
22//! compiler's output.
23
24use std::collections::{HashMap, HashSet};
25use std::sync::Arc;
26
27use buffa::Message;
28use buffa_descriptor::generated::descriptor::{FileDescriptorProto, FileDescriptorSet};
29use buffa_descriptor::{DescriptorPool, PoolError};
30
31/// Errors from building a [`Reflector`].
32#[derive(Debug, thiserror::Error)]
33#[non_exhaustive]
34pub enum ReflectionError {
35 /// The bytes did not decode as a `FileDescriptorSet`.
36 ///
37 /// Exceeding the element-memory budget is reported separately as
38 /// [`ElementBudget`](Self::ElementBudget), because there the bytes are
39 /// fine; this variant is every other wire-level failure.
40 #[error("failed to decode FileDescriptorSet: {0}")]
41 Decode(buffa::DecodeError),
42 /// The descriptor set was well-formed but exceeded the decode's
43 /// element-memory budget.
44 ///
45 /// Split apart from [`Decode`](Self::Decode) because the remedy is
46 /// different: the bytes are fine, and a reflection service is normally
47 /// handed its own server's descriptors, which are trusted. The remedy is
48 /// a smaller set — strip `source_code_info`, or narrow it to the files
49 /// this server reflects.
50 #[error(
51 "FileDescriptorSet exceeds the decode element-memory budget; the bytes \
52 are well-formed, the schema is simply large. Strip source_code_info \
53 or reduce the set to the files this server reflects."
54 )]
55 ElementBudget,
56 /// The decoded descriptors did not link into a valid pool (dangling
57 /// type reference, duplicate symbol, malformed map entry, ...).
58 #[error("invalid descriptor set: {0}")]
59 Pool(#[from] PoolError),
60 /// The top-level wire structure of the set was malformed (e.g. a
61 /// truncated length prefix), so per-file byte ranges could not be
62 /// sliced out.
63 #[error("malformed FileDescriptorSet framing at byte {offset}")]
64 MalformedFraming {
65 /// Byte offset of the unreadable tag or length.
66 offset: usize,
67 },
68 /// A file in the set has no `name` field; the reflection protocol
69 /// keys every file query by name.
70 #[error("FileDescriptorProto at index {index} has no name")]
71 UnnamedFile {
72 /// Position of the nameless file within the set.
73 index: usize,
74 },
75 /// The framing walk and the message decoder disagreed on how many
76 /// files the set contains — the bytes are not a coherent
77 /// `FileDescriptorSet`.
78 #[error("FileDescriptorSet framing yields {framed} files but decoding yields {decoded}")]
79 CountMismatch {
80 /// Files found by the top-level framing walk.
81 framed: usize,
82 /// Files in the decoded `FileDescriptorSet`.
83 decoded: usize,
84 },
85 /// [`add_descriptor_set_bytes`](Reflector::add_descriptor_set_bytes)
86 /// was called on a reflector whose pool is shared (adopted via
87 /// [`from_descriptor_pool`](Reflector::from_descriptor_pool) with
88 /// other outstanding references). Merge sets before sharing, or build
89 /// the reflector from bytes.
90 #[error("cannot add to a descriptor pool with outstanding references")]
91 SharedPool,
92}
93
94/// Written out rather than derived with `#[from]`, so that the budget/corrupt
95/// split cannot be bypassed. A derived conversion sends every
96/// [`buffa::DecodeError`] to [`Decode`](ReflectionError::Decode), so the next
97/// decode path added here — reached with `?`, which compiles fine — would
98/// report an over-budget set as corruption again, silently undoing the reason
99/// [`ElementBudget`](ReflectionError::ElementBudget) exists.
100impl From<buffa::DecodeError> for ReflectionError {
101 fn from(e: buffa::DecodeError) -> Self {
102 match e {
103 buffa::DecodeError::ElementMemoryLimitExceeded => Self::ElementBudget,
104 other => Self::Decode(other),
105 }
106 }
107}
108
109/// The answer to a single reflection query, protocol-version agnostic.
110///
111/// `service.rs` maps this onto the generated `v1` / `v1alpha` response
112/// messages, which are structurally identical.
113pub(crate) enum Answer {
114 /// Serialized `FileDescriptorProto`s: the matched file followed by its
115 /// transitive import closure.
116 Files(Vec<Vec<u8>>),
117 /// Extension field numbers registered on `base_type`.
118 ExtensionNumbers {
119 base_type: String,
120 numbers: Vec<i32>,
121 },
122 /// Fully-qualified names of the advertised services.
123 Services(Vec<String>),
124 /// The queried entity does not exist; carries the error message.
125 NotFound(String),
126}
127
128/// Descriptor index serving gRPC server reflection queries.
129///
130/// Build one from the wire bytes of a `FileDescriptorSet` (typically
131/// embedded with `include_bytes!` from
132/// `connectrpc_build::Config::emit_descriptor_set` output) or from an
133/// existing [`DescriptorPool`], and hand it to
134/// [`ReflectionService`](crate::ReflectionService).
135///
136/// ```no_run
137/// use connectrpc_reflection::Reflector;
138///
139/// // In real code: include_bytes!(concat!(env!("OUT_DIR"), "/app.fds.bin"))
140/// # fn descriptor_set_bytes() -> &'static [u8] { &[] }
141/// let reflector = Reflector::from_descriptor_set_bytes(descriptor_set_bytes()).unwrap();
142/// ```
143///
144/// # Multiple descriptor sets
145///
146/// [`add_descriptor_set_bytes`](Self::add_descriptor_set_bytes) merges
147/// further sets into the index. Files whose name is already registered
148/// are skipped (first registration wins), so sets that each carry their
149/// own copy of shared imports — `google/protobuf/*.proto`, common
150/// vendored protos — merge cleanly.
151pub struct Reflector {
152 pool: Arc<DescriptorPool>,
153 /// Per-file response payloads keyed by file name: the original input
154 /// bytes for sets loaded from wire bytes, a canonical re-encode for
155 /// pools adopted via [`from_descriptor_pool`](Self::from_descriptor_pool).
156 response_bytes: HashMap<String, Vec<u8>>,
157 /// `ListServices` override installed by [`with_services`](Self::with_services);
158 /// `None` advertises every service in the pool.
159 services_override: Option<Vec<String>>,
160}
161
162impl std::fmt::Debug for Reflector {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("Reflector")
165 .field("files", &self.pool.files().len())
166 .field("services", &self.service_names())
167 .finish_non_exhaustive()
168 }
169}
170
171impl Reflector {
172 /// Build a reflector from wire-format `FileDescriptorSet` bytes.
173 ///
174 /// The set should carry the transitive import closure of the files
175 /// it contains (both `protoc --include_imports` and
176 /// `Config::emit_descriptor_set` guarantee this); imports missing
177 /// from the set are silently omitted from file-closure responses.
178 ///
179 /// # Errors
180 ///
181 /// Returns [`ReflectionError`] when the bytes do not decode as a
182 /// `FileDescriptorSet`, the descriptors do not link, or a contained
183 /// file has no name. A set too large for the decode's element-memory
184 /// budget reports [`ElementBudget`](ReflectionError::ElementBudget)
185 /// rather than a decode failure.
186 pub fn from_descriptor_set_bytes(bytes: &[u8]) -> Result<Self, ReflectionError> {
187 let mut reflector = Self {
188 pool: Arc::new(DescriptorPool::default()),
189 response_bytes: HashMap::new(),
190 services_override: None,
191 };
192 reflector.add_descriptor_set_bytes(bytes)?;
193 Ok(reflector)
194 }
195
196 /// Serve reflection from an existing [`DescriptorPool`] — typically
197 /// the lazily-built `descriptor_pool()` that a buffa-generated package
198 /// exposes when reflection codegen is enabled, which spares the build
199 /// script a separate `emit_descriptor_set` step.
200 ///
201 /// The pool must cover **every** proto you want resolvable. Under
202 /// `buf generate`'s default per-directory plugin strategy, each
203 /// generated package embeds only its own package's closure — set
204 /// `strategy: all` on the buffa plugin so any one package's pool
205 /// spans the whole codegen run.
206 ///
207 /// Response payloads are re-encoded from the pool's parsed
208 /// `FileDescriptorProto`s. buffa preserves unknown fields, so the
209 /// bytes are semantically faithful to the compiler's output but not
210 /// guaranteed byte-identical (field ordering is canonicalized). For
211 /// byte-exact responses, build from
212 /// [`from_descriptor_set_bytes`](Self::from_descriptor_set_bytes).
213 ///
214 /// A reflector adopting a pool that other references point at — a
215 /// generated package's pool always qualifies, since the lazy static
216 /// keeps one — cannot be extended with
217 /// [`add_descriptor_set_bytes`](Self::add_descriptor_set_bytes);
218 /// build from bytes if you need to merge sets.
219 ///
220 /// # Errors
221 ///
222 /// Returns [`ReflectionError::UnnamedFile`] when a pool file has no
223 /// name.
224 pub fn from_descriptor_pool(pool: Arc<DescriptorPool>) -> Result<Self, ReflectionError> {
225 let mut response_bytes = HashMap::with_capacity(pool.files().len());
226 for (index, fd) in pool.files().iter().enumerate() {
227 let name = fd
228 .name
229 .clone()
230 .ok_or(ReflectionError::UnnamedFile { index })?;
231 response_bytes
232 .entry(name)
233 .or_insert_with(|| fd.encode_to_vec());
234 }
235 Ok(Self {
236 pool,
237 response_bytes,
238 services_override: None,
239 })
240 }
241
242 /// Merge another wire-format `FileDescriptorSet` into the index.
243 ///
244 /// Files whose name is already registered are skipped, so shared
245 /// imports duplicated across sets do not conflict.
246 ///
247 /// # Errors
248 ///
249 /// Returns [`ReflectionError`] when the bytes do not decode or link
250 /// (including [`ElementBudget`](ReflectionError::ElementBudget) for a set
251 /// over the decode's element-memory budget), a contained file has no
252 /// name, or any other reference to the backing pool exists
253 /// ([`ReflectionError::SharedPool`]) — which is
254 /// always the case for reflectors built with
255 /// [`from_descriptor_pool`](Self::from_descriptor_pool) from a
256 /// long-lived pool. On error the reflector should be discarded: the
257 /// pool may have absorbed part of the failed set.
258 pub fn add_descriptor_set_bytes(&mut self, bytes: &[u8]) -> Result<(), ReflectionError> {
259 let raw_files = split_descriptor_set(bytes)?;
260 let set = FileDescriptorSet::decode_from_slice(bytes)?;
261 // The framing walk and buffa's decode are independent parsers of
262 // the same bytes; if they disagree on the file count, zipping
263 // them would silently pair names with the wrong raw bytes.
264 if raw_files.len() != set.file.len() {
265 return Err(ReflectionError::CountMismatch {
266 framed: raw_files.len(),
267 decoded: set.file.len(),
268 });
269 }
270 let mut names = Vec::with_capacity(set.file.len());
271 for (index, fd) in set.file.iter().enumerate() {
272 names.push(
273 fd.name
274 .clone()
275 .ok_or(ReflectionError::UnnamedFile { index })?,
276 );
277 }
278
279 let pool = Arc::get_mut(&mut self.pool).ok_or(ReflectionError::SharedPool)?;
280 pool.add_file_descriptor_set(set)?;
281
282 for (name, raw) in names.into_iter().zip(raw_files) {
283 self.response_bytes
284 .entry(name)
285 .or_insert_with(|| raw.to_vec());
286 }
287 Ok(())
288 }
289
290 /// Restrict the service list advertised by `ListServices` to the
291 /// given fully-qualified names, in the given order.
292 ///
293 /// Like the Go `grpcreflect` `Namer`, this affects only
294 /// `ListServices`; files and symbols in the descriptor set stay
295 /// resolvable. Use it when the set's import closure carries services
296 /// you do not actually mount. Names absent from the descriptor set
297 /// are advertised as given — the protocol does not require the list
298 /// to be resolvable.
299 ///
300 /// Calling this more than once **replaces** the previous list; it
301 /// does not accumulate (unlike tonic's `with_service_name`).
302 #[must_use]
303 pub fn with_services<I, S>(mut self, names: I) -> Self
304 where
305 I: IntoIterator<Item = S>,
306 S: Into<String>,
307 {
308 self.services_override = Some(names.into_iter().map(Into::into).collect());
309 self
310 }
311
312 /// The fully-qualified service names `ListServices` will advertise:
313 /// every service in the descriptor pool in registration order plus
314 /// the reflection services themselves (matching grpc-go, which
315 /// always lists them) — or, verbatim, the override installed by
316 /// [`with_services`](Self::with_services).
317 #[must_use]
318 pub fn service_names(&self) -> Vec<String> {
319 self.services_override.clone().unwrap_or_else(|| {
320 let mut names: Vec<String> = self
321 .pool
322 .services()
323 .iter()
324 .map(|svc| svc.full_name().to_owned())
325 .collect();
326 for own in self_descriptors().pool.services() {
327 if !names.iter().any(|name| name == own.full_name()) {
328 names.push(own.full_name().to_owned());
329 }
330 }
331 names
332 })
333 }
334
335 /// The descriptor pool backing this reflector, for read-only
336 /// inspection (listing files, resolving descriptors).
337 #[must_use]
338 pub fn pool(&self) -> &DescriptorPool {
339 &self.pool
340 }
341
342 // ── Queries ─────────────────────────────────────────────────────────
343 //
344 // Each query consults the user's pool first and the crate's own
345 // descriptors second, so every reflector is self-describing: the
346 // reflection service can answer queries about `grpc.reflection.*`
347 // itself, which schema-free clients (`buf curl`, `grpcurl` without
348 // proto files) need to invoke `ServerReflectionInfo`. This matches
349 // grpc-go, where the reflection proto is always registered.
350
351 pub(crate) fn file_by_filename(&self, name: &str) -> Answer {
352 for source in self.sources() {
353 if let Some(fd) = source.pool.file_by_name(name) {
354 return Answer::Files(source.closure(fd));
355 }
356 }
357 Answer::NotFound(format!("file {name:?} not found"))
358 }
359
360 pub(crate) fn file_containing_symbol(&self, symbol: &str) -> Answer {
361 for source in self.sources() {
362 if let Some(fd) = source.pool.file_containing_symbol(symbol) {
363 return Answer::Files(source.closure(fd));
364 }
365 }
366 Answer::NotFound(format!("symbol {symbol:?} not found"))
367 }
368
369 pub(crate) fn file_containing_extension(&self, containing_type: &str, number: i32) -> Answer {
370 let not_found = || {
371 Answer::NotFound(format!(
372 "extension {number} of type {containing_type:?} not found"
373 ))
374 };
375 let Ok(number) = u32::try_from(number) else {
376 return not_found();
377 };
378 for source in self.sources() {
379 let Some(extendee) = source.pool.message_index(containing_type) else {
380 continue;
381 };
382 let Some(extension) = source.pool.extension_for(extendee, number) else {
383 return not_found();
384 };
385 return match source.pool.file_containing_symbol(extension.full_name()) {
386 Some(fd) => Answer::Files(source.closure(fd)),
387 None => not_found(),
388 };
389 }
390 not_found()
391 }
392
393 pub(crate) fn all_extension_numbers_of_type(&self, name: &str) -> Answer {
394 let normalized = name.strip_prefix('.').unwrap_or(name);
395 for source in self.sources() {
396 let Some(extendee) = source.pool.message_index(normalized) else {
397 continue;
398 };
399 // `extensions_of` iterates a (extendee, number)-keyed map
400 // range, so the numbers come out unique and ascending.
401 let numbers = source
402 .pool
403 .extensions_of(extendee)
404 .filter_map(|ext| i32::try_from(ext.field().number()).ok())
405 .collect();
406 return Answer::ExtensionNumbers {
407 base_type: normalized.to_owned(),
408 numbers,
409 };
410 }
411 Answer::NotFound(format!("message {normalized:?} not found"))
412 }
413
414 pub(crate) fn list_services(&self) -> Answer {
415 Answer::Services(self.service_names())
416 }
417
418 /// The user's descriptors followed by the crate's own — the lookup
419 /// order for every query.
420 fn sources(&self) -> [DescriptorSource<'_>; 2] {
421 let own = self_descriptors();
422 [
423 DescriptorSource {
424 pool: &self.pool,
425 response_bytes: &self.response_bytes,
426 },
427 DescriptorSource {
428 pool: &own.pool,
429 response_bytes: &own.response_bytes,
430 },
431 ]
432 }
433}
434
435/// One pool plus the per-file response payloads sliced or encoded from
436/// its input — either the user's descriptors or the crate's own.
437struct DescriptorSource<'a> {
438 pool: &'a DescriptorPool,
439 response_bytes: &'a HashMap<String, Vec<u8>>,
440}
441
442impl DescriptorSource<'_> {
443 /// The serialized bytes of `fd` followed by its transitive imports,
444 /// deduplicated; after the requested file, the import order is
445 /// unspecified (clients assemble a set). Imports missing from the
446 /// pool are skipped.
447 fn closure(&self, fd: &FileDescriptorProto) -> Vec<Vec<u8>> {
448 let mut seen = HashSet::new();
449 let mut out = Vec::new();
450 let mut stack = vec![fd];
451 while let Some(fd) = stack.pop() {
452 let Some(name) = fd.name.as_deref() else {
453 continue;
454 };
455 if !seen.insert(name) {
456 continue;
457 }
458 if let Some(bytes) = self.response_bytes.get(name) {
459 out.push(bytes.clone());
460 }
461 stack.extend(
462 fd.dependency
463 .iter()
464 .filter_map(|dep| self.pool.file_by_name(dep)),
465 );
466 }
467 out
468 }
469}
470
471/// The crate's own descriptors (`grpc/reflection/{v1,v1alpha}`), built
472/// once per process from [`crate::FILE_DESCRIPTOR_SET`] and consulted as
473/// the fallback source behind every user pool.
474struct SelfDescriptors {
475 pool: DescriptorPool,
476 response_bytes: HashMap<String, Vec<u8>>,
477}
478
479fn self_descriptors() -> &'static SelfDescriptors {
480 static SELF: std::sync::OnceLock<SelfDescriptors> = std::sync::OnceLock::new();
481 SELF.get_or_init(|| {
482 // The bytes are embedded at compile time and validated by this
483 // crate's tests, so a failure here is a build defect, not input.
484 let bytes = crate::FILE_DESCRIPTOR_SET;
485 let raw_files = split_descriptor_set(bytes).expect("embedded descriptor set is framed");
486 // This crate's own descriptors, nothing user-controlled, and far
487 // below any budget — so the default limits stay.
488 let set = FileDescriptorSet::decode_from_slice(bytes)
489 .expect("this crate's embedded descriptor set decodes");
490 let response_bytes = set
491 .file
492 .iter()
493 .zip(&raw_files)
494 .filter_map(|(fd, raw)| Some((fd.name.clone()?, raw.to_vec())))
495 .collect();
496 let pool = DescriptorPool::new(set).expect("embedded descriptor set links");
497 SelfDescriptors {
498 pool,
499 response_bytes,
500 }
501 })
502}
503
504/// Slice the original per-file `FileDescriptorProto` byte ranges out of a
505/// wire-format `FileDescriptorSet` (`repeated FileDescriptorProto file = 1`).
506fn split_descriptor_set(bytes: &[u8]) -> Result<Vec<&[u8]>, ReflectionError> {
507 let mut files = Vec::new();
508 let mut pos = 0;
509 while pos < bytes.len() {
510 let tag_offset = pos;
511 let tag = read_varint(bytes, &mut pos)
512 .ok_or(ReflectionError::MalformedFraming { offset: tag_offset })?;
513 let (field, wire_type) = (tag >> 3, tag & 0x7);
514 match wire_type {
515 0 => {
516 read_varint(bytes, &mut pos)
517 .ok_or(ReflectionError::MalformedFraming { offset: tag_offset })?;
518 }
519 1 => pos += 8,
520 2 => {
521 let len = read_varint(bytes, &mut pos)
522 .ok_or(ReflectionError::MalformedFraming { offset: tag_offset })?
523 as usize;
524 let end = pos
525 .checked_add(len)
526 .filter(|&end| end <= bytes.len())
527 .ok_or(ReflectionError::MalformedFraming { offset: tag_offset })?;
528 if field == 1 {
529 files.push(&bytes[pos..end]);
530 }
531 pos = end;
532 }
533 5 => pos += 4,
534 _ => return Err(ReflectionError::MalformedFraming { offset: tag_offset }),
535 }
536 if pos > bytes.len() {
537 return Err(ReflectionError::MalformedFraming { offset: tag_offset });
538 }
539 }
540 Ok(files)
541}
542
543/// Read one base-128 varint. Assumes canonical encodings (a non-canonical
544/// 10th byte has its high bits silently dropped, matching common protobuf
545/// decoders); returns `None` on truncation or an unterminated varint.
546fn read_varint(bytes: &[u8], pos: &mut usize) -> Option<u64> {
547 let mut value = 0u64;
548 for shift in (0..64).step_by(7) {
549 let byte = *bytes.get(*pos)?;
550 *pos += 1;
551 value |= u64::from(byte & 0x7f) << shift;
552 if byte & 0x80 == 0 {
553 return Some(value);
554 }
555 }
556 None
557}
558
559#[cfg(test)]
560mod tests {
561 use buffa_descriptor::generated::descriptor::field_descriptor_proto::{Label, Type};
562 use buffa_descriptor::generated::descriptor::{
563 DescriptorProto, EnumDescriptorProto, EnumValueDescriptorProto, FieldDescriptorProto,
564 MethodDescriptorProto, OneofDescriptorProto, ServiceDescriptorProto,
565 };
566
567 use super::*;
568
569 const SELF_V1: &str = "grpc.reflection.v1.ServerReflection";
570 const SELF_V1ALPHA: &str = "grpc.reflection.v1alpha.ServerReflection";
571
572 /// A two-file set: `acme/base.proto` (imported) and `acme/api.proto`
573 /// exercising every symbol kind the index covers.
574 fn test_set() -> FileDescriptorSet {
575 let base = FileDescriptorProto {
576 name: Some("acme/base.proto".into()),
577 package: Some("acme.base".into()),
578 message_type: vec![DescriptorProto {
579 name: Some("Shared".into()),
580 extension_range: vec![
581 buffa_descriptor::generated::descriptor::descriptor_proto::ExtensionRange {
582 start: Some(100),
583 end: Some(200),
584 ..Default::default()
585 },
586 ],
587 ..Default::default()
588 }],
589 ..Default::default()
590 };
591 let api = FileDescriptorProto {
592 name: Some("acme/api.proto".into()),
593 package: Some("acme.api".into()),
594 dependency: vec!["acme/base.proto".into()],
595 message_type: vec![DescriptorProto {
596 name: Some("Request".into()),
597 field: vec![FieldDescriptorProto {
598 name: Some("query".into()),
599 number: Some(1),
600 label: Some(Label::LABEL_OPTIONAL),
601 r#type: Some(Type::TYPE_STRING),
602 ..Default::default()
603 }],
604 oneof_decl: vec![OneofDescriptorProto {
605 name: Some("variant".into()),
606 ..Default::default()
607 }],
608 nested_type: vec![DescriptorProto {
609 name: Some("Inner".into()),
610 ..Default::default()
611 }],
612 enum_type: vec![EnumDescriptorProto {
613 name: Some("Kind".into()),
614 value: vec![EnumValueDescriptorProto {
615 name: Some("KIND_UNSPECIFIED".into()),
616 number: Some(0),
617 ..Default::default()
618 }],
619 ..Default::default()
620 }],
621 ..Default::default()
622 }],
623 enum_type: vec![EnumDescriptorProto {
624 name: Some("Code".into()),
625 value: vec![EnumValueDescriptorProto {
626 name: Some("CODE_OK".into()),
627 number: Some(0),
628 ..Default::default()
629 }],
630 ..Default::default()
631 }],
632 service: vec![ServiceDescriptorProto {
633 name: Some("Search".into()),
634 method: vec![MethodDescriptorProto {
635 name: Some("Query".into()),
636 input_type: Some(".acme.api.Request".into()),
637 output_type: Some(".acme.api.Request".into()),
638 ..Default::default()
639 }],
640 ..Default::default()
641 }],
642 extension: vec![FieldDescriptorProto {
643 name: Some("tag".into()),
644 number: Some(150),
645 label: Some(Label::LABEL_OPTIONAL),
646 r#type: Some(Type::TYPE_INT32),
647 extendee: Some(".acme.base.Shared".into()),
648 ..Default::default()
649 }],
650 ..Default::default()
651 };
652 FileDescriptorSet {
653 file: vec![base, api],
654 ..Default::default()
655 }
656 }
657
658 fn test_reflector() -> Reflector {
659 Reflector::from_descriptor_set_bytes(&test_set().encode_to_vec()).unwrap()
660 }
661
662 fn files(answer: Answer) -> Vec<Vec<u8>> {
663 match answer {
664 Answer::Files(files) => files,
665 _ => panic!("expected Answer::Files"),
666 }
667 }
668
669 fn assert_not_found(answer: &Answer) {
670 assert!(matches!(answer, Answer::NotFound(_)));
671 }
672
673 #[test]
674 fn file_by_filename_returns_raw_bytes_and_closure() {
675 let set = test_set();
676 let reflector = test_reflector();
677
678 let got = files(reflector.file_by_filename("acme/api.proto"));
679 // api.proto plus its import.
680 assert_eq!(got.len(), 2);
681 assert_eq!(got[0], set.file[1].encode_to_vec());
682 assert_eq!(got[1], set.file[0].encode_to_vec());
683
684 // The import alone has no dependencies.
685 let got = files(reflector.file_by_filename("acme/base.proto"));
686 assert_eq!(got.len(), 1);
687
688 assert_not_found(&reflector.file_by_filename("nope.proto"));
689 }
690
691 #[test]
692 fn raw_bytes_survive_unknown_fields() {
693 // Hand-frame a set whose file payload carries an unknown field
694 // (number 12345, varint 1) that a re-encode might reorder or drop.
695 // The bytes-built reflector must return it byte-for-byte.
696 let mut file = test_set().file[0].encode_to_vec();
697 let unknown = [0xc8, 0x83, 0x06, 0x01]; // tag 12345<<3|0, value 1
698 file.extend_from_slice(&unknown);
699 let mut set_bytes = vec![0x0a, u8::try_from(file.len()).unwrap()];
700 set_bytes.extend_from_slice(&file);
701
702 let reflector = Reflector::from_descriptor_set_bytes(&set_bytes).unwrap();
703 let got = files(reflector.file_by_filename("acme/base.proto"));
704 assert_eq!(got, vec![file]);
705 }
706
707 #[test]
708 fn symbol_lookup_covers_every_kind() {
709 let reflector = test_reflector();
710 for symbol in [
711 "acme.api.Request",
712 "acme.api.Request.query",
713 "acme.api.Request.variant",
714 "acme.api.Request.Inner",
715 "acme.api.Request.Kind",
716 "acme.api.Request.KIND_UNSPECIFIED", // enum values scope to the parent
717 "acme.api.Code",
718 "acme.api.CODE_OK",
719 "acme.api.Search",
720 "acme.api.Search.Query",
721 "acme.api.tag",
722 ".acme.api.Request", // leading dot tolerated
723 ] {
724 let got = files(reflector.file_containing_symbol(symbol));
725 assert_eq!(got.len(), 2, "symbol {symbol}");
726 }
727 // Enum values do NOT live inside the enum's own scope.
728 assert_not_found(&reflector.file_containing_symbol("acme.api.Code.CODE_OK"));
729 // Packages are not symbols.
730 assert_not_found(&reflector.file_containing_symbol("acme.api"));
731 assert_not_found(&reflector.file_containing_symbol("acme.api.Missing"));
732 }
733
734 #[test]
735 fn extension_queries() {
736 let reflector = test_reflector();
737
738 let got = files(reflector.file_containing_extension("acme.base.Shared", 150));
739 assert_eq!(got.len(), 2); // api.proto declares it, base.proto imported
740
741 assert_not_found(&reflector.file_containing_extension("acme.base.Shared", 151));
742 assert_not_found(&reflector.file_containing_extension("acme.base.Shared", -1));
743 assert_not_found(&reflector.file_containing_extension("acme.api.Request", 150));
744
745 match reflector.all_extension_numbers_of_type("acme.base.Shared") {
746 Answer::ExtensionNumbers { base_type, numbers } => {
747 assert_eq!(base_type, "acme.base.Shared");
748 assert_eq!(numbers, vec![150]);
749 }
750 _ => panic!("expected extension numbers"),
751 }
752 // A known message with no extensions answers with an empty list,
753 // not an error.
754 match reflector.all_extension_numbers_of_type("acme.api.Request") {
755 Answer::ExtensionNumbers { numbers, .. } => assert!(numbers.is_empty()),
756 _ => panic!("expected extension numbers"),
757 }
758 // Unknown types — and non-message symbols like services — are
759 // not extendable.
760 assert_not_found(&reflector.all_extension_numbers_of_type("acme.Missing"));
761 assert_not_found(&reflector.all_extension_numbers_of_type("acme.api.Search"));
762 }
763
764 #[test]
765 fn list_services() {
766 match test_reflector().list_services() {
767 Answer::Services(names) => {
768 assert_eq!(names, vec!["acme.api.Search", SELF_V1, SELF_V1ALPHA]);
769 }
770 _ => panic!("expected services"),
771 }
772 }
773
774 #[test]
775 fn with_services_overrides_advertised_list_only() {
776 let reflector = test_reflector().with_services(["acme.api.Curated"]);
777 assert_eq!(reflector.service_names(), ["acme.api.Curated"]);
778 match reflector.list_services() {
779 Answer::Services(names) => assert_eq!(names, vec!["acme.api.Curated"]),
780 _ => panic!("expected services"),
781 }
782 // Symbols stay resolvable, including the de-listed service.
783 let got = files(reflector.file_containing_symbol("acme.api.Search"));
784 assert_eq!(got.len(), 2);
785 }
786
787 #[test]
788 fn merging_sets_skips_duplicate_files() {
789 let mut reflector = test_reflector();
790 // A second set re-shipping base.proto (different content — would
791 // clobber the symbol index if not skipped) plus a new file.
792 let second = FileDescriptorSet {
793 file: vec![
794 FileDescriptorProto {
795 name: Some("acme/base.proto".into()),
796 package: Some("acme.other".into()),
797 ..Default::default()
798 },
799 FileDescriptorProto {
800 name: Some("acme/extra.proto".into()),
801 package: Some("acme.extra".into()),
802 service: vec![ServiceDescriptorProto {
803 name: Some("Extra".into()),
804 ..Default::default()
805 }],
806 ..Default::default()
807 },
808 ],
809 ..Default::default()
810 };
811 reflector
812 .add_descriptor_set_bytes(&second.encode_to_vec())
813 .unwrap();
814
815 // First registration of base.proto won: its message survives and
816 // the replacement package was never indexed.
817 assert!(matches!(
818 reflector.file_containing_symbol("acme.base.Shared"),
819 Answer::Files(_)
820 ));
821 match reflector.list_services() {
822 Answer::Services(names) => {
823 assert_eq!(
824 names,
825 vec!["acme.api.Search", "acme.extra.Extra", SELF_V1, SELF_V1ALPHA]
826 );
827 }
828 _ => panic!("expected services"),
829 }
830 }
831
832 #[test]
833 fn from_descriptor_pool_serves_reencoded_files() {
834 let set = test_set();
835 let pool = Arc::new(DescriptorPool::new(set.clone()).unwrap());
836 let reflector = Reflector::from_descriptor_pool(Arc::clone(&pool)).unwrap();
837
838 // Same queries work; payloads decode to the same descriptors
839 // (byte-exactness is only guaranteed for the bytes-built path).
840 let got = files(reflector.file_containing_symbol("acme.api.Search"));
841 assert_eq!(got.len(), 2);
842 let decoded = FileDescriptorProto::decode_from_slice(&got[0]).unwrap();
843 assert_eq!(decoded, set.file[1]);
844
845 match reflector.list_services() {
846 Answer::Services(names) => {
847 assert_eq!(names, vec!["acme.api.Search", SELF_V1, SELF_V1ALPHA]);
848 }
849 _ => panic!("expected services"),
850 }
851
852 // The pool is shared (we still hold `pool`), so merging more
853 // bytes into it must refuse rather than mutate shared state.
854 let mut reflector = reflector;
855 let err = reflector
856 .add_descriptor_set_bytes(&FileDescriptorSet::default().encode_to_vec())
857 .unwrap_err();
858 assert!(matches!(err, ReflectionError::SharedPool));
859 }
860
861 /// The runtime path deliberately stays bounded, so an oversized set must
862 /// reach the caller as `ElementBudget` — the variant that says the bytes
863 /// are fine — and not as a decode failure that reads like corruption.
864 #[test]
865 fn an_oversized_set_reports_the_element_budget_not_a_decode_failure() {
866 // Charged per element on struct size, so many small files exceed the
867 // budget while staying small on the wire. The count is derived rather
868 // than written as a literal: the charge tracks
869 // `size_of::<FileDescriptorProto>()`, so a literal silently stops
870 // exceeding the budget whenever that struct shrinks, and the test
871 // would then assert a rejection that no longer happens. Same
872 // derivation as `connectrpc::test_budget` and connectrpc-build's
873 // `over_default_budget_set`, repeated because a `#[cfg(test)]` helper
874 // cannot cross a crate boundary; that module carries the full
875 // rationale.
876 let per_element = std::mem::size_of::<FileDescriptorProto>();
877 let n = (buffa::DEFAULT_ELEMENT_MEMORY_LIMIT / per_element) * 5 / 4;
878 let set = FileDescriptorSet {
879 file: (0..n)
880 .map(|i| FileDescriptorProto {
881 name: Some(format!("f{i}.proto")),
882 ..Default::default()
883 })
884 .collect(),
885 ..Default::default()
886 };
887 let bytes = buffa::Message::encode_to_vec(&set);
888
889 let err = Reflector::from_descriptor_set_bytes(&bytes).unwrap_err();
890 assert!(
891 matches!(err, ReflectionError::ElementBudget),
892 "expected ElementBudget, got {err:?}"
893 );
894 // The message has to say the schema is large, not that the bytes are
895 // bad — that distinction is the whole reason the variant exists.
896 let rendered = err.to_string();
897 assert!(
898 rendered.contains("well-formed"),
899 "message should absolve the bytes, got {rendered:?}"
900 );
901 }
902
903 #[test]
904 fn construction_errors() {
905 // Truncated length prefix.
906 let err = Reflector::from_descriptor_set_bytes(&[0x0a, 0xff]).unwrap_err();
907 assert!(matches!(err, ReflectionError::MalformedFraming { .. }));
908
909 // A file without a name.
910 let set = FileDescriptorSet {
911 file: vec![FileDescriptorProto::default()],
912 ..Default::default()
913 };
914 let err = Reflector::from_descriptor_set_bytes(&set.encode_to_vec()).unwrap_err();
915 assert!(matches!(err, ReflectionError::UnnamedFile { index: 0 }));
916
917 // An empty set is valid and answers everything with not-found.
918 let reflector = Reflector::from_descriptor_set_bytes(&[]).unwrap();
919 assert_not_found(&reflector.file_by_filename("x.proto"));
920 match reflector.list_services() {
921 // Even an empty set self-lists the reflection services.
922 Answer::Services(names) => assert_eq!(names, vec![SELF_V1, SELF_V1ALPHA]),
923 _ => panic!("expected services"),
924 }
925 }
926}