Skip to main content

reallyme_codec_proto/
lib.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! ReallyMe codec protobuf error envelopes with generated Buffa bindings.
6
7#[cfg(feature = "generated")]
8use buffa::{DecodeOptions, EnumValue, Enumeration, Message};
9#[cfg(feature = "generated")]
10use serde::de::DeserializeOwned;
11#[cfg(feature = "generated")]
12use thiserror::Error;
13
14/// Generated protobuf boundary.
15pub mod generated;
16
17/// Maximum accepted binary protobuf message size at codec wire boundaries.
18#[cfg(feature = "generated")]
19pub const MAX_CODEC_PROTO_MESSAGE_BYTES: usize = 1024 * 1024;
20
21/// Maximum accepted `CodecError` envelope size at codec wire boundaries.
22#[cfg(feature = "generated")]
23pub const MAX_CODEC_PROTO_ERROR_ENVELOPE_BYTES: usize = 4096;
24
25/// Maximum accepted proto3 JSON message size at codec wire boundaries.
26#[cfg(feature = "generated")]
27pub const MAX_CODEC_PROTO_JSON_BYTES: usize = 1_572_864;
28
29#[cfg(feature = "generated")]
30const CODEC_PROTO_RECURSION_LIMIT: u32 = 64;
31
32#[cfg(feature = "generated")]
33use generated::proto::reallyme::codec::v1::{
34    __buffa::oneof::codec_error::Error as CodecErrorBranchProto, CodecBackendError,
35    CodecBaseEncodingError, CodecCanonicalizationError, CodecError, CodecErrorReason,
36    CodecMultiformatError, CodecPemError,
37};
38
39/// Stable codec wire-error branch.
40#[cfg(feature = "generated")]
41#[derive(Debug, Clone, Copy, Eq, PartialEq)]
42#[non_exhaustive]
43pub enum CodecWireErrorBranch {
44    /// Base encodings such as base64, base64url, base58btc, and hex.
45    BaseEncoding,
46    /// PEM armor and DER envelope errors.
47    Pem,
48    /// Multibase, multicodec, and multikey errors.
49    Multiformat,
50    /// CBOR, DAG-CBOR, JCS, and JSON canonicalization errors.
51    Canonicalization,
52    /// Serialization, protobuf, dispatch, FFI, WASM, and internal failures.
53    Backend,
54}
55
56/// Validated public codec wire error.
57#[cfg(feature = "generated")]
58#[derive(Debug, Clone, Copy, Eq, Error, PartialEq)]
59#[error("codec wire boundary error")]
60pub struct CodecWireError {
61    branch: CodecWireErrorBranch,
62    reason: CodecErrorReason,
63}
64
65/// Error returned when constructing a codec wire error with an invalid reason.
66#[cfg(feature = "generated")]
67#[derive(Debug, Clone, Copy, Eq, Error, PartialEq)]
68#[non_exhaustive]
69pub enum CodecWireErrorConstructionError {
70    /// The reason is unspecified, unknown, or belongs to a different branch.
71    #[error("codec error reason does not belong to the selected branch")]
72    BranchReasonMismatch,
73}
74
75/// Result alias for codec protobuf boundary helpers.
76#[cfg(feature = "generated")]
77pub type CodecWireResult<T> = Result<T, CodecWireError>;
78
79#[cfg(feature = "generated")]
80impl CodecWireError {
81    /// Returns the stable error branch.
82    #[must_use]
83    pub const fn branch(self) -> CodecWireErrorBranch {
84        self.branch
85    }
86
87    /// Returns the stable branch-specific reason.
88    #[must_use]
89    pub const fn reason(self) -> CodecErrorReason {
90        self.reason
91    }
92
93    /// Constructs a public wire error only when branch and reason agree.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`CodecWireErrorConstructionError::BranchReasonMismatch`] when
98    /// the reason is unspecified, unknown, or owned by another branch.
99    pub fn try_new(
100        branch: CodecWireErrorBranch,
101        reason: CodecErrorReason,
102    ) -> Result<Self, CodecWireErrorConstructionError> {
103        if !reason_is_valid_for_branch(branch, reason) {
104            return Err(CodecWireErrorConstructionError::BranchReasonMismatch);
105        }
106        Ok(Self { branch, reason })
107    }
108
109    /// Internal constructor for crate-owned known-good backend mappings.
110    #[must_use]
111    const fn backend_internal(reason: CodecErrorReason) -> Self {
112        Self {
113            branch: CodecWireErrorBranch::Backend,
114            reason,
115        }
116    }
117}
118
119/// Encodes a protobuf message with Buffa.
120#[cfg(feature = "generated")]
121#[must_use]
122pub fn encode_protobuf<M: Message>(message: &M) -> Vec<u8> {
123    message.encode_to_vec()
124}
125
126/// Decodes a bounded protobuf message from untrusted bytes.
127///
128/// # Errors
129///
130/// Returns a backend wire error when input exceeds the boundary size limit or
131/// fails protobuf decoding.
132#[cfg(feature = "generated")]
133pub fn decode_protobuf<M: Message>(bytes: &[u8]) -> CodecWireResult<M> {
134    decode_protobuf_with_limit(bytes, MAX_CODEC_PROTO_MESSAGE_BYTES)
135}
136
137/// Decodes a generated protobuf message from proto3-compatible JSON bytes.
138///
139/// The decoded message is immediately re-encoded to protobuf so compact JSON
140/// that expands past the binary protobuf cap is rejected at the same boundary
141/// as native protobuf input.
142///
143/// # Errors
144///
145/// Returns a backend wire error for oversized or malformed
146/// JSON and for JSON messages whose binary encoding exceeds the protobuf cap.
147#[cfg(feature = "generated")]
148pub fn decode_json<M: DeserializeOwned + Message>(bytes: &[u8]) -> CodecWireResult<M> {
149    if bytes.len() > MAX_CODEC_PROTO_JSON_BYTES {
150        return Err(resource_limit_error());
151    }
152
153    let message: M = serde_json::from_slice(bytes).map_err(|_| {
154        CodecWireError::backend_internal(
155            CodecErrorReason::CODEC_ERROR_REASON_BACKEND_MALFORMED_JSON,
156        )
157    })?;
158    let encoded = encode_protobuf(&message);
159    if encoded.len() > MAX_CODEC_PROTO_MESSAGE_BYTES {
160        return Err(resource_limit_error());
161    }
162    Ok(message)
163}
164
165/// Builds the structured `CodecError` protobuf message for a validated error.
166#[cfg(feature = "generated")]
167#[must_use]
168pub fn codec_error(error: CodecWireError) -> CodecError {
169    let reason = EnumValue::from(error.reason());
170    let branch = match error.branch() {
171        CodecWireErrorBranch::BaseEncoding => {
172            CodecErrorBranchProto::BaseEncoding(Box::new(CodecBaseEncodingError {
173                reason,
174                __buffa_unknown_fields: Default::default(),
175            }))
176        }
177        CodecWireErrorBranch::Pem => CodecErrorBranchProto::Pem(Box::new(CodecPemError {
178            reason,
179            __buffa_unknown_fields: Default::default(),
180        })),
181        CodecWireErrorBranch::Multiformat => {
182            CodecErrorBranchProto::Multiformat(Box::new(CodecMultiformatError {
183                reason,
184                __buffa_unknown_fields: Default::default(),
185            }))
186        }
187        CodecWireErrorBranch::Canonicalization => {
188            CodecErrorBranchProto::Canonicalization(Box::new(CodecCanonicalizationError {
189                reason,
190                __buffa_unknown_fields: Default::default(),
191            }))
192        }
193        CodecWireErrorBranch::Backend => {
194            CodecErrorBranchProto::Backend(Box::new(CodecBackendError {
195                reason,
196                __buffa_unknown_fields: Default::default(),
197            }))
198        }
199    };
200
201    CodecError {
202        error: Some(branch),
203        __buffa_unknown_fields: Default::default(),
204    }
205}
206
207/// Encodes a structured `CodecError` as protobuf bytes.
208#[cfg(feature = "generated")]
209#[must_use]
210pub fn codec_error_bytes(error: CodecWireError) -> Vec<u8> {
211    encode_protobuf(&codec_error(error))
212}
213
214/// Decodes and validates a public `CodecError` payload.
215///
216/// # Errors
217///
218/// Returns a deterministic malformed-protobuf canonicalization error when the
219/// payload does not contain exactly one decodable branch with a concrete reason
220/// valid for that branch.
221#[cfg(feature = "generated")]
222pub fn decode_codec_error_payload(payload: &[u8]) -> CodecWireResult<CodecWireError> {
223    let error =
224        decode_protobuf_with_limit::<CodecError>(payload, MAX_CODEC_PROTO_ERROR_ENVELOPE_BYTES)?;
225    let (branch, reason) = match error.error {
226        Some(CodecErrorBranchProto::BaseEncoding(error)) => {
227            (CodecWireErrorBranch::BaseEncoding, error.reason)
228        }
229        Some(CodecErrorBranchProto::Pem(error)) => (CodecWireErrorBranch::Pem, error.reason),
230        Some(CodecErrorBranchProto::Multiformat(error)) => {
231            (CodecWireErrorBranch::Multiformat, error.reason)
232        }
233        Some(CodecErrorBranchProto::Canonicalization(error)) => {
234            (CodecWireErrorBranch::Canonicalization, error.reason)
235        }
236        Some(CodecErrorBranchProto::Backend(error)) => {
237            (CodecWireErrorBranch::Backend, error.reason)
238        }
239        None => return Err(malformed_protobuf_error()),
240    };
241
242    match reason.as_known() {
243        Some(reason) if reason_is_valid_for_branch(branch, reason) => {
244            Ok(CodecWireError { branch, reason })
245        }
246        Some(CodecErrorReason::CODEC_ERROR_REASON_UNSPECIFIED) | Some(_) | None => {
247            Err(malformed_protobuf_error())
248        }
249    }
250}
251
252#[cfg(feature = "generated")]
253fn decode_protobuf_with_limit<M: Message>(bytes: &[u8], max_bytes: usize) -> CodecWireResult<M> {
254    if bytes.len() > max_bytes {
255        return Err(resource_limit_error());
256    }
257
258    DecodeOptions::new()
259        .with_recursion_limit(CODEC_PROTO_RECURSION_LIMIT)
260        .with_max_message_size(max_bytes)
261        .decode_from_slice(bytes)
262        .map_err(|_| malformed_protobuf_error())
263}
264
265#[cfg(feature = "generated")]
266fn reason_is_valid_for_branch(branch: CodecWireErrorBranch, reason: CodecErrorReason) -> bool {
267    let value = reason.to_i32();
268    match branch {
269        CodecWireErrorBranch::BaseEncoding => (100..=199).contains(&value),
270        CodecWireErrorBranch::Pem => (200..=299).contains(&value),
271        CodecWireErrorBranch::Multiformat => (300..=399).contains(&value),
272        CodecWireErrorBranch::Canonicalization => (400..=499).contains(&value),
273        CodecWireErrorBranch::Backend => (500..=599).contains(&value),
274    }
275}
276
277#[cfg(feature = "generated")]
278fn malformed_protobuf_error() -> CodecWireError {
279    CodecWireError::backend_internal(
280        CodecErrorReason::CODEC_ERROR_REASON_BACKEND_MALFORMED_PROTOBUF,
281    )
282}
283
284#[cfg(feature = "generated")]
285fn resource_limit_error() -> CodecWireError {
286    CodecWireError::backend_internal(
287        CodecErrorReason::CODEC_ERROR_REASON_BACKEND_RESOURCE_LIMIT_EXCEEDED,
288    )
289}