Skip to main content

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}