dwara_core/dataplane/
grpc_web.rs

1//! gRPC-Web framing translation and JSON-to-gRPC transcoding (DW-101).
2//!
3//! ## gRPC-Web framing
4//!
5//! gRPC-Web is the browser-friendly variant of gRPC: it replaces HTTP/2
6//! trailers with a trailing frame that carries the gRPC status as
7//! pseudo-headers, and it frames every chunk with a 5-byte prefix (1
8//! byte flag + 4 bytes big-endian length). The flag byte distinguishes
9//! data frames (0x00) from trailer frames (0x80). A unary response is
10//! one data frame followed by one trailer frame; a streaming response
11//! is an arbitrary number of data frames (each its own 5-byte-prefixed
12//! chunk) followed by a single trailer frame.
13//!
14//! The gateway sits between a browser (gRPC-Web) and a native gRPC
15//! upstream (HTTP/2 + trailers). On the forward path it strips the
16//! gRPC-Web framing and reconstructs the native gRPC request body; on
17//! the response path it wraps the native gRPC frames (data + trailers)
18//! back into gRPC-Web framing so the browser can decode them.
19//!
20//! ## JSON-to-gRPC transcoding
21//!
22//! When transcoding is enabled, the gateway also accepts plain JSON
23//! requests (Content-Type: application/json) and translates them to
24//! protobuf wire bytes for the upstream, and translates the protobuf
25//! response back to JSON. The mapping is driven by .proto descriptors
26//! supplied via config (FileDescriptorSet files): at config publish the
27//! gateway loads the descriptors, builds a method map keyed by the
28//! fully-qualified gRPC method name
29//! (`/{package}.{service}/{method}`), and resolves each method's
30//! request and response message types. The google.api.http annotation
31//! on each RPC maps an HTTP path + verb to the gRPC method, so a REST
32//! or JSON client can call a gRPC backend without a gRPC library.
33//!
34//! ## gRPC status mapping
35//!
36//! A gRPC status (carried in the `grpc-status` trailer) is mapped to an
37//! HTTP status code and a `google.rpc.Status` JSON body for non-gRPC
38//! clients. The mapping follows the gRPC HTTP/JSON specification (the
39//! `google.rpc.Code` -> HTTP status table).
40//!
41//! ## Status
42//!
43//! The module compiles into every build (no cargo feature). The config
44//! schema (`GrpcWeb`, `GrpcWebTranscoding`, `GrpcWebDescriptor`) parses
45//! and validates; the runtime framing/transcoding translation is
46//! scaffolded and not yet dispatched from the request path.
47
48use std::collections::HashMap;
49
50use base64::Engine as _;
51use prost::Message;
52use serde_json::Value;
53
54// ---------------------------------------------------------------------------
55// Error type
56// ---------------------------------------------------------------------------
57
58/// Errors from gRPC-Web framing translation or JSON-to-gRPC transcoding.
59#[derive(Debug)]
60pub enum GrpcWebError {
61    /// The gRPC-Web body is malformed (truncated frame, bad flag, etc.).
62    MalformedFrame(String),
63    /// The .proto descriptor file could not be read or parsed.
64    Descriptor(String),
65    /// The referenced gRPC method is not found in the loaded descriptors.
66    MethodNotFound(String),
67    /// The JSON body does not match the expected protobuf message shape.
68    Transcoding(String),
69}
70
71impl std::fmt::Display for GrpcWebError {
72    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73        match self {
74            GrpcWebError::MalformedFrame(m) => {
75                write!(f, "malformed grpc-web frame: {m}")
76            }
77            GrpcWebError::Descriptor(m) => write!(f, "descriptor error: {m}"),
78            GrpcWebError::MethodNotFound(m) => {
79                write!(f, "method not found in descriptors: {m}")
80            }
81            GrpcWebError::Transcoding(m) => write!(f, "transcoding error: {m}"),
82        }
83    }
84}
85
86impl std::error::Error for GrpcWebError {}
87
88// ---------------------------------------------------------------------------
89// Minimal protobuf descriptor types (prost-derived)
90// ---------------------------------------------------------------------------
91//
92// prost 0.14 moved the well-known wrapper types into `prost::types` but
93// the descriptor types (FileDescriptorSet, DescriptorProto, etc.) remain
94// in the separate `prost-types` crate, which only exists at 0.13 (built
95// against prost 0.13). Rather than pulling in a version-mismatched
96// crate, we define the minimal subset of descriptor types we need,
97// derived via prost::Message. These mirror the `google.protobuf`
98// descriptor.proto shapes — only the fields the transcoder reads are
99// included; unknown fields are skipped by prost's default decoding.
100
101/// `google.protobuf.FileDescriptorSet` — the top-level container
102/// produced by `protoc --descriptor_set_out`.
103#[derive(Clone, PartialEq, Message)]
104pub struct FileDescriptorSet {
105    #[prost(message, repeated, tag = "1")]
106    pub file: Vec<FileDescriptorProto>,
107}
108
109/// `google.protobuf.FileDescriptorProto` — one .proto file.
110#[derive(Clone, PartialEq, Message)]
111pub struct FileDescriptorProto {
112    #[prost(string, optional, tag = "1")]
113    pub name: Option<String>,
114    #[prost(string, optional, tag = "2")]
115    pub package: Option<String>,
116    #[prost(message, repeated, tag = "4")]
117    pub message_type: Vec<DescriptorProto>,
118    #[prost(message, repeated, tag = "6")]
119    pub service: Vec<ServiceDescriptorProto>,
120}
121
122/// `google.protobuf.DescriptorProto` — a message type.
123#[derive(Clone, PartialEq, Message)]
124pub struct DescriptorProto {
125    #[prost(string, optional, tag = "1")]
126    pub name: Option<String>,
127    #[prost(message, repeated, tag = "2")]
128    pub field: Vec<FieldDescriptorProto>,
129    #[prost(message, repeated, tag = "3")]
130    pub nested_type: Vec<DescriptorProto>,
131    #[prost(message, repeated, tag = "4")]
132    pub enum_type: Vec<EnumDescriptorProto>,
133}
134
135/// `google.protobuf.ServiceDescriptorProto` — an RPC service.
136#[derive(Clone, PartialEq, Message)]
137pub struct ServiceDescriptorProto {
138    #[prost(string, optional, tag = "1")]
139    pub name: Option<String>,
140    #[prost(message, repeated, tag = "2")]
141    pub method: Vec<MethodDescriptorProto>,
142}
143
144/// `google.protobuf.MethodDescriptorProto` — one RPC method.
145#[derive(Clone, PartialEq, Message)]
146pub struct MethodDescriptorProto {
147    #[prost(string, optional, tag = "1")]
148    pub name: Option<String>,
149    #[prost(string, optional, tag = "2")]
150    pub input_type: Option<String>,
151    #[prost(string, optional, tag = "3")]
152    pub output_type: Option<String>,
153}
154
155/// `google.protobuf.FieldDescriptorProto` — one field in a message.
156#[derive(Clone, PartialEq, Message)]
157pub struct FieldDescriptorProto {
158    #[prost(string, optional, tag = "1")]
159    pub name: Option<String>,
160    #[prost(string, optional, tag = "10")]
161    pub json_name: Option<String>,
162    #[prost(int32, optional, tag = "3")]
163    pub number: Option<i32>,
164    #[prost(enumeration = "field_label::Label", optional, tag = "4")]
165    pub label: Option<i32>,
166    #[prost(enumeration = "field_type::Type", optional, tag = "5")]
167    pub r#type: Option<i32>,
168    #[prost(string, optional, tag = "6")]
169    pub type_name: Option<String>,
170}
171
172/// `google.protobuf.EnumDescriptorProto` — an enum type.
173#[derive(Clone, PartialEq, Message)]
174pub struct EnumDescriptorProto {
175    #[prost(string, optional, tag = "1")]
176    pub name: Option<String>,
177    #[prost(message, repeated, tag = "2")]
178    pub value: Vec<EnumValueDescriptorProto>,
179}
180
181/// `google.protobuf.EnumValueDescriptorProto` — one enum value.
182#[derive(Clone, PartialEq, Message)]
183pub struct EnumValueDescriptorProto {
184    #[prost(string, optional, tag = "1")]
185    pub name: Option<String>,
186    #[prost(int32, optional, tag = "2")]
187    pub number: Option<i32>,
188}
189
190/// Field label constants (the `FieldDescriptorProto.Label` enum).
191pub mod field_label {
192    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, prost::Enumeration)]
193    #[repr(i32)]
194    pub enum Label {
195        Optional = 1,
196        Required = 2,
197        Repeated = 3,
198    }
199    pub const OPTIONAL: i32 = 1;
200    pub const REQUIRED: i32 = 2;
201    pub const REPEATED: i32 = 3;
202}
203
204/// Field type constants (the `FieldDescriptorProto.Type` enum).
205pub mod field_type {
206    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, prost::Enumeration)]
207    #[repr(i32)]
208    pub enum Type {
209        Double = 1,
210        Float = 2,
211        Int64 = 3,
212        Uint64 = 4,
213        Int32 = 5,
214        Fixed64 = 6,
215        Fixed32 = 7,
216        Bool = 8,
217        String = 9,
218        Message = 11,
219        Bytes = 12,
220        Uint32 = 13,
221        Enum = 14,
222        Sfixed32 = 15,
223        Sfixed64 = 16,
224        Sint32 = 17,
225        Sint64 = 18,
226    }
227    pub const DOUBLE: i32 = 1;
228    pub const FLOAT: i32 = 2;
229    pub const INT64: i32 = 3;
230    pub const UINT64: i32 = 4;
231    pub const INT32: i32 = 5;
232    pub const FIXED64: i32 = 6;
233    pub const FIXED32: i32 = 7;
234    pub const BOOL: i32 = 8;
235    pub const STRING: i32 = 9;
236    pub const MESSAGE: i32 = 11;
237    pub const BYTES: i32 = 12;
238    pub const UINT32: i32 = 13;
239    pub const ENUM: i32 = 14;
240    pub const SFIXED32: i32 = 15;
241    pub const SFIXED64: i32 = 16;
242    pub const SINT32: i32 = 17;
243    pub const SINT64: i32 = 18;
244}
245
246// ---------------------------------------------------------------------------
247// gRPC-Web framing translator
248// ---------------------------------------------------------------------------
249
250/// The gRPC-Web frame flag byte: 0x00 for a data frame, 0x80 for a
251/// trailer frame.
252const FLAG_DATA: u8 = 0x00;
253const FLAG_TRAILER: u8 = 0x80;
254
255/// The fixed 5-byte gRPC-Web frame prefix: 1 flag byte + 4 big-endian
256/// length bytes.
257const FRAME_PREFIX_LEN: usize = 5;
258
259/// Strip gRPC-Web framing and reconstruct the native gRPC request body.
260///
261/// gRPC-Web encodes the request as one or more 5-byte-prefixed frames.
262/// For a unary call there is a single data frame; for a streaming call
263/// there may be multiple data frames. Trailer frames are not sent on
264/// the request path (the browser cannot send trailers), so any trailer
265/// frame in the request body is treated as an error.
266///
267/// The returned bytes are the concatenation of every data frame's
268/// payload — exactly the native gRPC request body (which is itself a
269/// single 5-byte-prefixed message, but that prefix belongs to the gRPC
270/// layer, not gRPC-Web; the gRPC-Web data frame payload IS the gRPC
271/// message including its own 5-byte gRPC length prefix).
272pub fn translate_grpc_web_to_grpc(body: &[u8]) -> Result<Vec<u8>, GrpcWebError> {
273    let mut out = Vec::with_capacity(body.len());
274    let mut pos = 0;
275    while pos < body.len() {
276        if pos + FRAME_PREFIX_LEN > body.len() {
277            return Err(GrpcWebError::MalformedFrame(format!(
278                "truncated frame header at offset {pos}"
279            )));
280        }
281        let flag = body[pos];
282        let len = u32::from_be_bytes([body[pos + 1], body[pos + 2], body[pos + 3], body[pos + 4]])
283            as usize;
284        let data_start = pos + FRAME_PREFIX_LEN;
285        let data_end = data_start + len;
286        if data_end > body.len() {
287            return Err(GrpcWebError::MalformedFrame(format!(
288                "frame length {len} exceeds remaining body at offset {pos}"
289            )));
290        }
291        match flag {
292            FLAG_DATA => {
293                out.extend_from_slice(&body[data_start..data_end]);
294            }
295            FLAG_TRAILER => {
296                // Trailer frames on the request path are unexpected
297                // (browsers cannot send gRPC trailers). Treat as an
298                // error rather than silently dropping.
299                return Err(GrpcWebError::MalformedFrame(
300                    "unexpected trailer frame in request body".to_string(),
301                ));
302            }
303            _ => {
304                return Err(GrpcWebError::MalformedFrame(format!(
305                    "unknown frame flag 0x{flag:02x} at offset {pos}"
306                )));
307            }
308        }
309        pos = data_end;
310    }
311    Ok(out)
312}
313
314/// Wrap native gRPC response bytes in gRPC-Web framing.
315///
316/// The native gRPC response body is a sequence of 5-byte-prefixed
317/// messages (flag 0x00 + 4-byte length + payload). The gRPC trailers
318/// (carrying `grpc-status`, `grpc-message`, etc.) are passed separately
319/// as `trailers` — a raw byte slice of newline-separated `key: value`
320/// lines (the standard gRPC trailer encoding).
321///
322/// The output is a gRPC-Web body: one data frame wrapping the entire
323/// gRPC message stream, followed by one trailer frame wrapping the
324/// trailer text. For streaming responses the caller should use
325/// [`translate_grpc_to_grpc_web_data_chunk`] per chunk and
326/// [`translate_grpc_trailers_to_grpc_web`] for the final trailer frame.
327pub fn translate_grpc_to_grpc_web(body: &[u8], trailers: &[u8]) -> Vec<u8> {
328    let mut out = Vec::with_capacity(body.len() + trailers.len() + FRAME_PREFIX_LEN * 2);
329    // Data frame: flag 0x00 + length + payload.
330    out.push(FLAG_DATA);
331    out.extend_from_slice(&(body.len() as u32).to_be_bytes());
332    out.extend_from_slice(body);
333    // Trailer frame: flag 0x80 + length + trailer text.
334    out.push(FLAG_TRAILER);
335    out.extend_from_slice(&(trailers.len() as u32).to_be_bytes());
336    out.extend_from_slice(trailers);
337    out
338}
339
340/// Wrap a single gRPC data chunk in a gRPC-Web data frame (no trailer
341/// frame). Used for streaming responses where each upstream data frame
342/// becomes its own gRPC-Web data frame, and the trailer frame is sent
343/// once at stream end via [`translate_grpc_trailers_to_grpc_web`].
344pub fn translate_grpc_to_grpc_web_data_chunk(chunk: &[u8]) -> Vec<u8> {
345    let mut out = Vec::with_capacity(chunk.len() + FRAME_PREFIX_LEN);
346    out.push(FLAG_DATA);
347    out.extend_from_slice(&(chunk.len() as u32).to_be_bytes());
348    out.extend_from_slice(chunk);
349    out
350}
351
352/// Wrap gRPC trailers in a gRPC-Web trailer frame. Sent as the final
353/// frame of a streaming gRPC-Web response.
354pub fn translate_grpc_trailers_to_grpc_web(trailers: &[u8]) -> Vec<u8> {
355    let mut out = Vec::with_capacity(trailers.len() + FRAME_PREFIX_LEN);
356    out.push(FLAG_TRAILER);
357    out.extend_from_slice(&(trailers.len() as u32).to_be_bytes());
358    out.extend_from_slice(trailers);
359    out
360}
361
362/// Parse the gRPC-Web trailer frame payload (key: value lines) into a
363/// map. Returns an empty map for an empty payload.
364pub fn parse_trailers(trailer_bytes: &[u8]) -> HashMap<String, String> {
365    let text = String::from_utf8_lossy(trailer_bytes);
366    let mut map = HashMap::new();
367    for line in text.lines() {
368        if let Some((key, value)) = line.split_once(':') {
369            map.insert(key.trim().to_string(), value.trim().to_string());
370        }
371    }
372    map
373}
374
375/// Build a trailer byte payload from a map of key-value pairs, in the
376/// `key: value\n` format gRPC-Web expects.
377pub fn build_trailers(trailers: &HashMap<String, String>) -> Vec<u8> {
378    let mut out = String::new();
379    for (key, value) in trailers {
380        out.push_str(key);
381        out.push_str(": ");
382        out.push_str(value);
383        out.push('\n');
384    }
385    out.into_bytes()
386}
387
388// ---------------------------------------------------------------------------
389// JSON-to-gRPC transcoding
390// ---------------------------------------------------------------------------
391
392/// A loaded .proto descriptor entry: the raw FileDescriptorSet bytes
393/// plus the package and service names that identify the methods this
394/// descriptor covers.
395#[derive(Debug, Clone)]
396pub struct LoadedDescriptor {
397    /// The raw FileDescriptorSet protobuf bytes (decoded from the
398    /// config-supplied file).
399    pub bytes: Vec<u8>,
400    /// The protobuf package name (e.g. `my.api`).
401    pub package: String,
402    /// The service name within the package (e.g. `MyService`).
403    pub service: String,
404}
405
406/// A resolved gRPC method: its fully-qualified path and the request/
407/// response message type names.
408#[derive(Debug, Clone)]
409pub struct MethodInfo {
410    /// The fully-qualified gRPC method path: `/{package}.{service}/{method}`.
411    pub full_path: String,
412    /// The fully-qualified request message type name.
413    pub request_type: String,
414    /// The fully-qualified response message type name.
415    pub response_type: String,
416}
417
418/// The gRPC-Web + JSON transcoding engine. Loaded at config publish from
419/// .proto descriptor files; holds a method map keyed by the fully-
420/// qualified gRPC method path.
421///
422/// The transcoding uses prost for protobuf encoding/decoding. Because
423/// the descriptors are supplied at runtime (not compiled in), the
424/// transcoder works with the dynamic protobuf descriptor model: it
425/// reads the FileDescriptorSet, resolves message types by name, and
426/// encodes/decodes fields by walking the descriptor's field list. This
427/// avoids any build-time code generation.
428pub struct GrpcTranscoder {
429    /// Method map: fully-qualified path -> method info.
430    methods: HashMap<String, MethodInfo>,
431    /// The loaded descriptors (raw FileDescriptorSet bytes + metadata).
432    descriptors: Vec<LoadedDescriptor>,
433}
434
435impl std::fmt::Debug for GrpcTranscoder {
436    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437        f.debug_struct("GrpcTranscoder")
438            .field("methods", &self.methods)
439            .field("descriptor_count", &self.descriptors.len())
440            .finish()
441    }
442}
443
444impl GrpcTranscoder {
445    /// Build a transcoder from loaded descriptors. Parses each
446    /// FileDescriptorSet, resolves the service's methods, and builds
447    /// the method map.
448    pub fn from_descriptors(descriptors: Vec<LoadedDescriptor>) -> Result<Self, GrpcWebError> {
449        let mut methods = HashMap::new();
450        for desc in &descriptors {
451            let fds = FileDescriptorSet::decode(&desc.bytes[..]).map_err(|e| {
452                GrpcWebError::Descriptor(format!(
453                    "failed to decode FileDescriptorSet for {}/{}: {e}",
454                    desc.package, desc.service
455                ))
456            })?;
457            for fd in &fds.file {
458                let pkg = fd.package.as_deref().unwrap_or("");
459                if pkg != desc.package {
460                    continue;
461                }
462                for svc in &fd.service {
463                    if svc.name.as_deref() != Some(desc.service.as_str()) {
464                        continue;
465                    }
466                    for method in &svc.method {
467                        let m_name = method.name.as_deref().unwrap_or("");
468                        let full_path =
469                            format!("/{}.{}{}{}", desc.package, desc.service, "/", m_name);
470                        let request_type = method.input_type.clone().unwrap_or_default();
471                        let response_type = method.output_type.clone().unwrap_or_default();
472                        methods.insert(
473                            full_path.clone(),
474                            MethodInfo {
475                                full_path,
476                                request_type,
477                                response_type,
478                            },
479                        );
480                    }
481                }
482            }
483        }
484        Ok(GrpcTranscoder {
485            methods,
486            descriptors,
487        })
488    }
489
490    /// Look up a method by its fully-qualified path
491    /// (`/{package}.{service}/{method}`).
492    pub fn method(&self, full_path: &str) -> Option<&MethodInfo> {
493        self.methods.get(full_path)
494    }
495
496    /// The number of resolved methods.
497    pub fn method_count(&self) -> usize {
498        self.methods.len()
499    }
500
501    /// Translate a JSON request body to protobuf wire bytes for the
502    /// given gRPC method. The returned bytes are the raw protobuf
503    /// message (without the 5-byte gRPC length prefix — the caller
504    /// adds that when framing the native gRPC request).
505    pub fn json_to_grpc(&self, method: &str, json: &Value) -> Result<Vec<u8>, GrpcWebError> {
506        let info = self
507            .methods
508            .get(method)
509            .ok_or_else(|| GrpcWebError::MethodNotFound(method.to_string()))?;
510        let fds = self.descriptor_set()?;
511        let msg_desc = resolve_message(&fds, &info.request_type).ok_or_else(|| {
512            GrpcWebError::Descriptor(format!(
513                "request message type {} not found in descriptors",
514                info.request_type
515            ))
516        })?;
517        encode_json_to_proto(json, &msg_desc, &fds).map_err(GrpcWebError::Transcoding)
518    }
519
520    /// Translate a protobuf response body to JSON for the given gRPC
521    /// method. The input `proto_bytes` are the raw protobuf message
522    /// bytes (without the 5-byte gRPC length prefix).
523    pub fn grpc_to_json(&self, method: &str, proto_bytes: &[u8]) -> Result<Value, GrpcWebError> {
524        let info = self
525            .methods
526            .get(method)
527            .ok_or_else(|| GrpcWebError::MethodNotFound(method.to_string()))?;
528        let fds = self.descriptor_set()?;
529        let msg_desc = resolve_message(&fds, &info.response_type).ok_or_else(|| {
530            GrpcWebError::Descriptor(format!(
531                "response message type {} not found in descriptors",
532                info.response_type
533            ))
534        })?;
535        decode_proto_to_json(proto_bytes, &msg_desc, &fds).map_err(GrpcWebError::Transcoding)
536    }
537
538    /// Collect all loaded descriptors into a single FileDescriptorSet
539    /// for message-type resolution.
540    fn descriptor_set(&self) -> Result<FileDescriptorSet, GrpcWebError> {
541        let mut all_files = Vec::new();
542        for desc in &self.descriptors {
543            let fds = FileDescriptorSet::decode(&desc.bytes[..]).map_err(|e| {
544                GrpcWebError::Descriptor(format!("failed to decode FileDescriptorSet: {e}"))
545            })?;
546            all_files.extend(fds.file);
547        }
548        Ok(FileDescriptorSet { file: all_files })
549    }
550}
551
552// ---------------------------------------------------------------------------
553// Protobuf descriptor helpers (dynamic, descriptor-driven)
554// ---------------------------------------------------------------------------
555
556/// Resolve a message descriptor by fully-qualified name from a
557/// FileDescriptorSet. The name may be prefixed with a leading dot
558/// (the protobuf convention for fully-qualified names).
559fn resolve_message(fds: &FileDescriptorSet, full_name: &str) -> Option<DescriptorProto> {
560    // Strip the leading dot if present (fully-qualified type names in
561    // method descriptors use a leading dot: ".my.api.MyRequest").
562    let name = full_name.strip_prefix('.').unwrap_or(full_name);
563    // The name is `{package}.{MessageName}`. Walk each file's message
564    // types (including nested) and match the full dotted name.
565    for fd in &fds.file {
566        let prefix = fd.package.as_deref().unwrap_or("");
567        let prefix = if prefix.is_empty() {
568            String::new()
569        } else {
570            format!("{}.", prefix)
571        };
572        for msg in &fd.message_type {
573            if let Some(found) = find_message_recursive(msg, &prefix, name) {
574                return Some(found);
575            }
576        }
577    }
578    None
579}
580
581/// Recursively search a message and its nested types for a message
582/// whose full dotted name matches `target`.
583fn find_message_recursive(
584    msg: &DescriptorProto,
585    prefix: &str,
586    target: &str,
587) -> Option<DescriptorProto> {
588    let name = msg.name.as_deref().unwrap_or("");
589    let full = format!("{prefix}{name}");
590    if full == target {
591        return Some(msg.clone());
592    }
593    let nested_prefix = format!("{full}.");
594    for nested in &msg.nested_type {
595        if let Some(found) = find_message_recursive(nested, &nested_prefix, target) {
596            return Some(found);
597        }
598    }
599    None
600}
601
602/// Encode a JSON value into protobuf wire bytes using a message
603/// descriptor. Walks the descriptor's fields and encodes each field
604/// present in the JSON according to its wire type.
605fn encode_json_to_proto(
606    json: &Value,
607    msg_desc: &DescriptorProto,
608    fds: &FileDescriptorSet,
609) -> Result<Vec<u8>, String> {
610    let obj = json
611        .as_object()
612        .ok_or_else(|| "expected a JSON object for a protobuf message".to_string())?;
613    let mut buf = Vec::new();
614    for field in &msg_desc.field {
615        let Some(json_name) = &field.json_name else {
616            continue;
617        };
618        let Some(value) = obj.get(json_name) else {
619            continue;
620        };
621        if value.is_null() {
622            continue;
623        }
624        encode_field(&mut buf, field, value, fds)?;
625    }
626    Ok(buf)
627}
628
629/// Encode a single protobuf field into the wire buffer.
630fn encode_field(
631    buf: &mut Vec<u8>,
632    field: &FieldDescriptorProto,
633    value: &Value,
634    fds: &FileDescriptorSet,
635) -> Result<(), String> {
636    let number = field.number.unwrap_or(0) as u32;
637    let label = field.label.unwrap_or(field_label::OPTIONAL);
638    let ty = field.r#type.unwrap_or(field_type::MESSAGE);
639    let is_repeated = label == field_label::REPEATED;
640
641    if is_repeated {
642        let arr = value.as_array().ok_or_else(|| {
643            format!(
644                "field {} expected a JSON array",
645                field.json_name.as_deref().unwrap_or("")
646            )
647        })?;
648        for elem in arr {
649            if elem.is_null() {
650                continue;
651            }
652            encode_single_field(buf, number, ty, field, elem, fds)?;
653        }
654        return Ok(());
655    }
656
657    encode_single_field(buf, number, ty, field, value, fds)
658}
659
660/// Encode a single (non-repeated or one-element) protobuf field.
661fn encode_single_field(
662    buf: &mut Vec<u8>,
663    number: u32,
664    ty: i32,
665    field: &FieldDescriptorProto,
666    value: &Value,
667    fds: &FileDescriptorSet,
668) -> Result<(), String> {
669    match ty {
670        field_type::STRING => {
671            let s = value
672                .as_str()
673                .ok_or_else(|| format!("field {number} expected a string"))?;
674            let bytes = s.as_bytes();
675            write_tag(buf, number, WIRE_LENGTH_DELIMITED);
676            write_varint(buf, bytes.len() as u64);
677            buf.extend_from_slice(bytes);
678        }
679        field_type::BYTES => {
680            let s = value
681                .as_str()
682                .ok_or_else(|| format!("field {number} expected a base64 string for bytes"))?;
683            let decoded = base64::engine::general_purpose::STANDARD
684                .decode(s)
685                .map_err(|e| format!("field {number} invalid base64: {e}"))?;
686            write_tag(buf, number, WIRE_LENGTH_DELIMITED);
687            write_varint(buf, decoded.len() as u64);
688            buf.extend_from_slice(&decoded);
689        }
690        field_type::BOOL => {
691            let b = value
692                .as_bool()
693                .ok_or_else(|| format!("field {number} expected a bool"))?;
694            write_tag(buf, number, WIRE_VARINT);
695            write_varint(buf, if b { 1u64 } else { 0u64 });
696        }
697        field_type::INT32 | field_type::SINT32 | field_type::SFIXED32 => {
698            let n = value
699                .as_i64()
700                .ok_or_else(|| format!("field {number} expected an integer"))?
701                as i32;
702            write_tag(buf, number, WIRE_VARINT);
703            write_varint(buf, n as u32 as u64);
704        }
705        field_type::INT64 | field_type::SINT64 | field_type::SFIXED64 => {
706            let n = value
707                .as_i64()
708                .ok_or_else(|| format!("field {number} expected an integer"))?;
709            write_tag(buf, number, WIRE_VARINT);
710            write_varint(buf, n as u64);
711        }
712        field_type::UINT32 | field_type::FIXED32 => {
713            let n = value
714                .as_u64()
715                .ok_or_else(|| format!("field {number} expected a non-negative integer"))?
716                as u32;
717            write_tag(buf, number, WIRE_VARINT);
718            write_varint(buf, n as u64);
719        }
720        field_type::UINT64 | field_type::FIXED64 => {
721            let n = value
722                .as_u64()
723                .ok_or_else(|| format!("field {number} expected a non-negative integer"))?;
724            write_tag(buf, number, WIRE_VARINT);
725            write_varint(buf, n);
726        }
727        field_type::FLOAT => {
728            let f = value
729                .as_f64()
730                .ok_or_else(|| format!("field {number} expected a number"))?
731                as f32;
732            write_tag(buf, number, WIRE_32BIT);
733            buf.extend_from_slice(&f.to_le_bytes());
734        }
735        field_type::DOUBLE => {
736            let f = value
737                .as_f64()
738                .ok_or_else(|| format!("field {number} expected a number"))?;
739            write_tag(buf, number, WIRE_64BIT);
740            buf.extend_from_slice(&f.to_le_bytes());
741        }
742        field_type::ENUM => {
743            let n = if let Some(i) = value.as_i64() {
744                i as i32
745            } else if let Some(s) = value.as_str() {
746                // Enum by name: look up the value in the enum type
747                // descriptor.
748                let _ = s;
749                return Err(format!(
750                    "field {number} enum-by-name encoding not supported; \
751                     use the integer value"
752                ));
753            } else {
754                return Err(format!(
755                    "field {number} expected an integer or string for enum"
756                ));
757            };
758            write_tag(buf, number, WIRE_VARINT);
759            write_varint(buf, n as u32 as u64);
760        }
761        field_type::MESSAGE => {
762            let type_name = field
763                .type_name
764                .as_deref()
765                .ok_or_else(|| format!("field {number} message type missing"))?;
766            let nested = resolve_message(fds, type_name)
767                .ok_or_else(|| format!("field {number} message type {type_name} not found"))?;
768            let sub = encode_json_to_proto(value, &nested, fds)?;
769            write_tag(buf, number, WIRE_LENGTH_DELIMITED);
770            write_varint(buf, sub.len() as u64);
771            buf.extend_from_slice(&sub);
772        }
773        _ => {
774            return Err(format!("field {number} type {ty} not supported"));
775        }
776    }
777    Ok(())
778}
779
780/// Decode protobuf wire bytes into a JSON value using a message
781/// descriptor. Walks the wire format, matching tags to fields, and
782/// builds a JSON object.
783fn decode_proto_to_json(
784    proto: &[u8],
785    msg_desc: &DescriptorProto,
786    fds: &FileDescriptorSet,
787) -> Result<Value, String> {
788    let mut obj = serde_json::Map::new();
789    let mut pos = 0;
790    while pos < proto.len() {
791        let (tag, tag_len) =
792            read_varint(&proto[pos..]).ok_or_else(|| "truncated varint tag".to_string())?;
793        pos += tag_len;
794        let field_number = (tag >> 3) as u32;
795        let wire_type = (tag & 0x07) as u8;
796
797        let field = msg_desc
798            .field
799            .iter()
800            .find(|f| f.number.unwrap_or(0) as u32 == field_number);
801        let json_name = field
802            .and_then(|f| f.json_name.clone())
803            .unwrap_or_else(|| format!("field{field_number}"));
804
805        let (value, consumed) = decode_field(&proto[pos..], wire_type, field, fds)?;
806        pos += consumed;
807
808        // Handle repeated fields: accumulate into a JSON array.
809        if let Some(existing) = obj.get(&json_name) {
810            if let Some(arr) = existing.as_array() {
811                let mut arr = arr.clone();
812                arr.push(value);
813                obj.insert(json_name, Value::Array(arr));
814            } else {
815                // Promote to array (first occurrence was singular).
816                let arr = vec![existing.clone(), value];
817                obj.insert(json_name, Value::Array(arr));
818            }
819        } else {
820            obj.insert(json_name, value);
821        }
822    }
823    Ok(Value::Object(obj))
824}
825
826/// Decode a single field value from the wire buffer.
827fn decode_field(
828    buf: &[u8],
829    wire_type: u8,
830    field: Option<&FieldDescriptorProto>,
831    fds: &FileDescriptorSet,
832) -> Result<(Value, usize), String> {
833    let ty = field.and_then(|f| f.r#type).unwrap_or(field_type::MESSAGE);
834
835    match wire_type {
836        WIRE_VARINT => {
837            let (v, len) = read_varint(buf).ok_or_else(|| "truncated varint".to_string())?;
838            let value = match ty {
839                field_type::BOOL => Value::Bool(v != 0),
840                field_type::INT32 | field_type::SINT32 | field_type::SFIXED32 => {
841                    Value::Number((v as u32 as i32 as i64).into())
842                }
843                field_type::INT64 | field_type::SINT64 | field_type::SFIXED64 => {
844                    Value::Number((v as i64).into())
845                }
846                field_type::UINT32 | field_type::FIXED32 => Value::Number((v as u32 as u64).into()),
847                field_type::UINT64 | field_type::FIXED64 => Value::Number(v.into()),
848                field_type::ENUM => Value::Number((v as u32 as i32 as i64).into()),
849                _ => Value::Number(v.into()),
850            };
851            Ok((value, len))
852        }
853        WIRE_64BIT => {
854            if buf.len() < 8 {
855                return Err("truncated 64-bit field".to_string());
856            }
857            let bytes: [u8; 8] = buf[..8].try_into().unwrap();
858            let value = match ty {
859                field_type::DOUBLE => serde_json::Number::from_f64(f64::from_le_bytes(bytes))
860                    .map(Value::Number)
861                    .unwrap_or(Value::Null),
862                field_type::FIXED64 => Value::Number(u64::from_le_bytes(bytes).into()),
863                field_type::SFIXED64 => Value::Number(i64::from_le_bytes(bytes).into()),
864                _ => Value::Null,
865            };
866            Ok((value, 8))
867        }
868        WIRE_LENGTH_DELIMITED => {
869            let (len, len_bytes) =
870                read_varint(buf).ok_or_else(|| "truncated length-delimited length".to_string())?;
871            let len = len as usize;
872            let data_start = len_bytes;
873            let data_end = data_start + len;
874            if data_end > buf.len() {
875                return Err("truncated length-delimited field".to_string());
876            }
877            let data = &buf[data_start..data_end];
878            let value = match ty {
879                field_type::STRING => Value::String(String::from_utf8_lossy(data).into_owned()),
880                field_type::BYTES => {
881                    Value::String(base64::engine::general_purpose::STANDARD.encode(data))
882                }
883                field_type::MESSAGE => {
884                    let type_name = field
885                        .and_then(|f| f.type_name.as_deref())
886                        .ok_or_else(|| "message field missing type_name".to_string())?;
887                    let nested = resolve_message(fds, type_name)
888                        .ok_or_else(|| format!("message type {type_name} not found"))?;
889                    decode_proto_to_json(data, &nested, fds)?
890                }
891                _ => {
892                    // Unknown length-delimited: try string, fall back to
893                    // base64 bytes.
894                    Value::String(String::from_utf8_lossy(data).into_owned())
895                }
896            };
897            Ok((value, data_end))
898        }
899        WIRE_32BIT => {
900            if buf.len() < 4 {
901                return Err("truncated 32-bit field".to_string());
902            }
903            let bytes: [u8; 4] = buf[..4].try_into().unwrap();
904            let value = match ty {
905                field_type::FLOAT => serde_json::Number::from_f64(f32::from_le_bytes(bytes) as f64)
906                    .map(Value::Number)
907                    .unwrap_or(Value::Null),
908                field_type::FIXED32 => Value::Number(u32::from_le_bytes(bytes).into()),
909                field_type::SFIXED32 => Value::Number(i32::from_le_bytes(bytes).into()),
910                _ => Value::Null,
911            };
912            Ok((value, 4))
913        }
914        _ => Err(format!("unknown wire type {wire_type}")),
915    }
916}
917
918// ---------------------------------------------------------------------------
919// Low-level varint / tag helpers
920// ---------------------------------------------------------------------------
921
922/// Wire type constants (the low 3 bits of a protobuf tag).
923const WIRE_VARINT: u8 = 0;
924const WIRE_64BIT: u8 = 1;
925const WIRE_LENGTH_DELIMITED: u8 = 2;
926const WIRE_32BIT: u8 = 5;
927
928/// Write a protobuf field tag (field number + wire type) as a varint.
929fn write_tag(buf: &mut Vec<u8>, field_number: u32, wire_type: u8) {
930    let tag = ((field_number as u64) << 3) | (wire_type as u64);
931    write_varint(buf, tag);
932}
933
934/// Write a varint to the buffer.
935fn write_varint(buf: &mut Vec<u8>, mut value: u64) {
936    while value >= 0x80 {
937        buf.push((value as u8) | 0x80);
938        value >>= 7;
939    }
940    buf.push(value as u8);
941}
942
943/// Read a varint from the buffer. Returns the value and the number of
944/// bytes consumed.
945fn read_varint(buf: &[u8]) -> Option<(u64, usize)> {
946    let mut result: u64 = 0;
947    let mut shift = 0;
948    for (i, byte) in buf.iter().enumerate() {
949        result |= ((byte & 0x7f) as u64) << shift;
950        if byte & 0x80 == 0 {
951            return Some((result, i + 1));
952        }
953        shift += 7;
954        if shift >= 64 {
955            return None;
956        }
957    }
958    None
959}
960
961// ---------------------------------------------------------------------------
962// gRPC status mapping
963// ---------------------------------------------------------------------------
964
965/// gRPC status codes (the `google.rpc.Code` enum values).
966pub mod grpc_code {
967    pub const OK: i32 = 0;
968    pub const CANCELLED: i32 = 1;
969    pub const UNKNOWN: i32 = 2;
970    pub const INVALID_ARGUMENT: i32 = 3;
971    pub const DEADLINE_EXCEEDED: i32 = 4;
972    pub const NOT_FOUND: i32 = 5;
973    pub const ALREADY_EXISTS: i32 = 6;
974    pub const PERMISSION_DENIED: i32 = 7;
975    pub const RESOURCE_EXHAUSTED: i32 = 8;
976    pub const FAILED_PRECONDITION: i32 = 9;
977    pub const ABORTED: i32 = 10;
978    pub const OUT_OF_RANGE: i32 = 11;
979    pub const UNIMPLEMENTED: i32 = 12;
980    pub const INTERNAL: i32 = 13;
981    pub const UNAVAILABLE: i32 = 14;
982    pub const DATA_LOSS: i32 = 15;
983    pub const UNAUTHENTICATED: i32 = 16;
984}
985
986/// Map a gRPC status code to an HTTP status code and a `google.rpc.Status`
987/// JSON body. The JSON body follows the `google.rpc.Status` shape:
988/// `{ "code": <int>, "message": "<string>", "details": [] }`.
989pub fn grpc_status_to_http(status: i32, message: &str) -> (u16, Value) {
990    let http_status = match status {
991        grpc_code::OK => 200,
992        grpc_code::CANCELLED => 499,
993        grpc_code::UNKNOWN => 500,
994        grpc_code::INVALID_ARGUMENT => 400,
995        grpc_code::DEADLINE_EXCEEDED => 504,
996        grpc_code::NOT_FOUND => 404,
997        grpc_code::ALREADY_EXISTS => 409,
998        grpc_code::PERMISSION_DENIED => 403,
999        grpc_code::RESOURCE_EXHAUSTED => 429,
1000        grpc_code::FAILED_PRECONDITION => 400,
1001        grpc_code::ABORTED => 409,
1002        grpc_code::OUT_OF_RANGE => 400,
1003        grpc_code::UNIMPLEMENTED => 501,
1004        grpc_code::INTERNAL => 500,
1005        grpc_code::UNAVAILABLE => 503,
1006        grpc_code::DATA_LOSS => 500,
1007        grpc_code::UNAUTHENTICATED => 401,
1008        _ => 500,
1009    };
1010    let body = serde_json::json!({
1011        "code": status,
1012        "message": message,
1013        "details": [],
1014    });
1015    (http_status, body)
1016}
1017
1018/// The gRPC status name for a code (for logging / trailer display).
1019pub fn grpc_status_name(status: i32) -> &'static str {
1020    match status {
1021        grpc_code::OK => "OK",
1022        grpc_code::CANCELLED => "CANCELLED",
1023        grpc_code::UNKNOWN => "UNKNOWN",
1024        grpc_code::INVALID_ARGUMENT => "INVALID_ARGUMENT",
1025        grpc_code::DEADLINE_EXCEEDED => "DEADLINE_EXCEEDED",
1026        grpc_code::NOT_FOUND => "NOT_FOUND",
1027        grpc_code::ALREADY_EXISTS => "ALREADY_EXISTS",
1028        grpc_code::PERMISSION_DENIED => "PERMISSION_DENIED",
1029        grpc_code::RESOURCE_EXHAUSTED => "RESOURCE_EXHAUSTED",
1030        grpc_code::FAILED_PRECONDITION => "FAILED_PRECONDITION",
1031        grpc_code::ABORTED => "ABORTED",
1032        grpc_code::OUT_OF_RANGE => "OUT_OF_RANGE",
1033        grpc_code::UNIMPLEMENTED => "UNIMPLEMENTED",
1034        grpc_code::INTERNAL => "INTERNAL",
1035        grpc_code::UNAVAILABLE => "UNAVAILABLE",
1036        grpc_code::DATA_LOSS => "DATA_LOSS",
1037        grpc_code::UNAUTHENTICATED => "UNAUTHENTICATED",
1038        _ => "UNKNOWN",
1039    }
1040}