Skip to main content

codec_cbor/
deterministic.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use std::fmt;
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8mod error;
9mod limits;
10
11pub use error::{DeterministicCborError, DeterministicCborProfileError};
12pub use limits::{
13    DETERMINISTIC_CBOR_NEGATIVE_MAX, DETERMINISTIC_CBOR_NEGATIVE_MIN,
14    MAX_DETERMINISTIC_CBOR_AGGREGATE_BYTE_STRING_BYTES,
15    MAX_DETERMINISTIC_CBOR_AGGREGATE_TEXT_BYTES, MAX_DETERMINISTIC_CBOR_CONTAINER_ENTRIES,
16    MAX_DETERMINISTIC_CBOR_INPUT_LEN, MAX_DETERMINISTIC_CBOR_NESTING_DEPTH,
17    MAX_DETERMINISTIC_CBOR_NODES, MAX_DETERMINISTIC_CBOR_OUTPUT_LEN,
18};
19
20/// Integer domain supported by the deterministic generic-CBOR profile.
21///
22/// The enum deliberately distinguishes unsigned and negative values so the
23/// full `u64` range is preserved without admitting two representations for
24/// nonnegative integers.
25#[non_exhaustive]
26pub enum DeterministicCborInteger {
27    /// Unsigned integer in the inclusive range `0..=u64::MAX`.
28    Unsigned(u64),
29    /// Negative integer in the inclusive range `i64::MIN..=-1`.
30    Negative(DeterministicCborNegativeInteger),
31}
32
33impl DeterministicCborInteger {
34    /// Construct an unsigned deterministic-CBOR integer.
35    pub const fn unsigned(value: u64) -> Self {
36        Self::Unsigned(value)
37    }
38
39    /// Construct a negative deterministic-CBOR integer.
40    ///
41    /// Zero and positive values are rejected so callers cannot create a second
42    /// semantic spelling for a value that belongs in [`Self::Unsigned`].
43    pub const fn negative(value: i64) -> Result<Self, DeterministicCborProfileError> {
44        if value < 0 {
45            Ok(Self::Negative(DeterministicCborNegativeInteger { value }))
46        } else {
47            Err(DeterministicCborProfileError::NegativeIntegerMustBeNegative)
48        }
49    }
50}
51
52impl fmt::Debug for DeterministicCborInteger {
53    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::Unsigned(_) => formatter
56                .debug_tuple("DeterministicCborInteger::Unsigned")
57                .field(&Redacted)
58                .finish(),
59            Self::Negative(value) => formatter
60                .debug_tuple("DeterministicCborInteger::Negative")
61                .field(value)
62                .finish(),
63        }
64    }
65}
66
67impl DeterministicCborInteger {
68    fn zeroize_owned(&mut self) {
69        match self {
70            Self::Unsigned(value) => value.zeroize(),
71            Self::Negative(value) => value.zeroize_owned(),
72        }
73    }
74}
75
76impl Drop for DeterministicCborInteger {
77    fn drop(&mut self) {
78        self.zeroize_owned();
79    }
80}
81
82impl ZeroizeOnDrop for DeterministicCborInteger {}
83
84/// Validated negative integer in the deterministic-CBOR supported range.
85///
86/// The field is private so callers cannot bypass
87/// [`DeterministicCborInteger::negative`] and create a nonnegative value in the
88/// negative-integer variant.
89pub struct DeterministicCborNegativeInteger {
90    value: i64,
91}
92
93impl DeterministicCborNegativeInteger {
94    /// Return the validated negative value.
95    pub const fn value(&self) -> i64 {
96        self.value
97    }
98}
99
100impl fmt::Debug for DeterministicCborNegativeInteger {
101    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102        formatter
103            .debug_tuple("DeterministicCborNegativeInteger")
104            .field(&Redacted)
105            .finish()
106    }
107}
108
109impl DeterministicCborNegativeInteger {
110    fn zeroize_owned(&mut self) {
111        self.value.zeroize();
112    }
113}
114
115impl Drop for DeterministicCborNegativeInteger {
116    fn drop(&mut self) {
117        self.zeroize_owned();
118    }
119}
120
121impl ZeroizeOnDrop for DeterministicCborNegativeInteger {}
122
123/// Map key domain supported by deterministic generic CBOR.
124///
125/// Text keys are treated as potentially sensitive because the codec is a
126/// generic infrastructure layer and cannot know whether a key contains PII or
127/// credential material.
128#[non_exhaustive]
129pub enum DeterministicCborMapKey {
130    /// Integer map key.
131    Integer(DeterministicCborInteger),
132    /// UTF-8 text map key.
133    Text(String),
134}
135
136impl DeterministicCborMapKey {
137    /// Construct a text key without Unicode normalization.
138    ///
139    /// Exact UTF-8 bytes define text-key equality in this profile. Validation
140    /// and encoding must not normalize, case-fold, or apply locale-sensitive
141    /// comparison to this value.
142    pub fn text(value: String) -> Self {
143        Self::Text(value)
144    }
145}
146
147impl fmt::Debug for DeterministicCborMapKey {
148    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
149        match self {
150            Self::Integer(value) => formatter
151                .debug_tuple("DeterministicCborMapKey::Integer")
152                .field(value)
153                .finish(),
154            Self::Text(value) => formatter
155                .debug_struct("DeterministicCborMapKey::Text")
156                .field("byte_len", &value.len())
157                .field("value", &Redacted)
158                .finish(),
159        }
160    }
161}
162
163impl Drop for DeterministicCborMapKey {
164    fn drop(&mut self) {
165        self.zeroize_owned();
166    }
167}
168
169impl DeterministicCborMapKey {
170    fn zeroize_owned(&mut self) {
171        match self {
172            Self::Integer(value) => value.zeroize_owned(),
173            Self::Text(value) => value.zeroize(),
174        }
175    }
176}
177
178impl ZeroizeOnDrop for DeterministicCborMapKey {}
179
180/// Entry in a deterministic-CBOR map.
181///
182/// Maps use entry lists rather than host hash maps so duplicate keys can be
183/// rejected by the profile validator and ordering never depends on runtime
184/// iteration behavior. Construction intentionally preserves duplicates and
185/// input order; it does not establish that an entry belongs to a validated
186/// deterministic-CBOR tree.
187pub struct DeterministicCborMapEntry {
188    key: DeterministicCborMapKey,
189    value: DeterministicCborValue,
190}
191
192impl DeterministicCborMapEntry {
193    /// Construct a deterministic-CBOR map entry.
194    pub fn new(key: DeterministicCborMapKey, value: DeterministicCborValue) -> Self {
195        Self { key, value }
196    }
197
198    /// Borrow the map key.
199    pub const fn key(&self) -> &DeterministicCborMapKey {
200        &self.key
201    }
202
203    /// Borrow the map value.
204    pub const fn value(&self) -> &DeterministicCborValue {
205        &self.value
206    }
207}
208
209impl fmt::Debug for DeterministicCborMapEntry {
210    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
211        formatter
212            .debug_struct("DeterministicCborMapEntry")
213            .field("key", &self.key)
214            .field("value", &self.value)
215            .finish()
216    }
217}
218
219impl DeterministicCborMapEntry {
220    fn zeroize_owned(&mut self) {
221        self.key.zeroize_owned();
222        self.value.zeroize_owned();
223    }
224}
225
226impl Drop for DeterministicCborMapEntry {
227    fn drop(&mut self) {
228        self.zeroize_owned();
229    }
230}
231
232impl ZeroizeOnDrop for DeterministicCborMapEntry {}
233
234/// Value domain supported by deterministic generic CBOR.
235///
236/// Construction alone does not enforce aggregate size, node, nesting,
237/// duplicate-key, or map-order rules. Boundary adapters must validate
238/// untrusted transport data before constructing this recursive owner, and the
239/// semantic encoder must validate caller-constructed values before allocating
240/// output.
241///
242/// All owned payloads and container backing allocations are treated as
243/// potentially sensitive and recursively zeroized on drop. Debug output is
244/// structural and redacted.
245#[non_exhaustive]
246pub enum DeterministicCborValue {
247    /// CBOR null.
248    Null,
249    /// CBOR boolean.
250    Bool(bool),
251    /// Supported deterministic-CBOR integer.
252    Integer(DeterministicCborInteger),
253    /// UTF-8 text string.
254    Text(String),
255    /// Byte string.
256    Bytes(Vec<u8>),
257    /// Array of deterministic-CBOR values.
258    Array(Vec<DeterministicCborValue>),
259    /// Map represented as an ordered entry list.
260    Map(Vec<DeterministicCborMapEntry>),
261}
262
263impl fmt::Debug for DeterministicCborValue {
264    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
265        match self {
266            Self::Null => formatter.write_str("DeterministicCborValue::Null"),
267            Self::Bool(_) => formatter
268                .debug_tuple("DeterministicCborValue::Bool")
269                .field(&Redacted)
270                .finish(),
271            Self::Integer(value) => formatter
272                .debug_tuple("DeterministicCborValue::Integer")
273                .field(value)
274                .finish(),
275            Self::Text(value) => formatter
276                .debug_struct("DeterministicCborValue::Text")
277                .field("byte_len", &value.len())
278                .field("value", &Redacted)
279                .finish(),
280            Self::Bytes(value) => formatter
281                .debug_struct("DeterministicCborValue::Bytes")
282                .field("byte_len", &value.len())
283                .field("value", &Redacted)
284                .finish(),
285            Self::Array(values) => formatter
286                .debug_struct("DeterministicCborValue::Array")
287                .field("len", &values.len())
288                .finish(),
289            Self::Map(entries) => formatter
290                .debug_struct("DeterministicCborValue::Map")
291                .field("len", &entries.len())
292                .finish(),
293        }
294    }
295}
296
297impl Drop for DeterministicCborValue {
298    fn drop(&mut self) {
299        self.zeroize_owned();
300    }
301}
302
303impl DeterministicCborValue {
304    fn zeroize_owned(&mut self) {
305        match self {
306            Self::Bool(value) => value.zeroize(),
307            Self::Integer(value) => value.zeroize_owned(),
308            Self::Text(value) => value.zeroize(),
309            Self::Bytes(value) => value.zeroize(),
310            Self::Array(values) => zeroize_values(values),
311            Self::Map(entries) => zeroize_entries(entries),
312            Self::Null => {}
313        }
314    }
315}
316
317impl ZeroizeOnDrop for DeterministicCborValue {}
318
319pub(crate) fn try_vec_with_capacity<T>(capacity: usize) -> Result<Vec<T>, DeterministicCborError> {
320    let mut values = Vec::new();
321    values
322        .try_reserve_exact(capacity)
323        .map_err(|_| DeterministicCborError::AllocationFailure)?;
324    Ok(values)
325}
326
327pub(crate) fn try_string_copy(value: &str) -> Result<String, DeterministicCborError> {
328    let mut copy = String::new();
329    copy.try_reserve_exact(value.len())
330        .map_err(|_| DeterministicCborError::AllocationFailure)?;
331    copy.push_str(value);
332    Ok(copy)
333}
334
335fn zeroize_values(values: &mut Vec<DeterministicCborValue>) {
336    for value in values.iter_mut() {
337        value.zeroize_owned();
338    }
339    values.clear();
340    values.spare_capacity_mut().zeroize();
341}
342
343fn zeroize_entries(entries: &mut Vec<DeterministicCborMapEntry>) {
344    for entry in entries.iter_mut() {
345        entry.zeroize_owned();
346    }
347    entries.clear();
348    entries.spare_capacity_mut().zeroize();
349}
350
351struct Redacted;
352
353impl fmt::Debug for Redacted {
354    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
355        formatter.write_str("<redacted>")
356    }
357}
358
359#[cfg(test)]
360mod tests;