Skip to main content

vr_jcs/
canonical_bytes.rs

1//! Newtype boundary over RFC 8785 canonical output bytes.
2//!
3//! [`CanonicalBytes`] makes "these bytes came out of JCS" a type-level
4//! fact that digest, signature, and receipt APIs can statically require.
5//! Construction is crate-private — the only way to obtain one is to
6//! route through [`crate::canonical_bytes_from_slice`] (or the
7//! re-export in `vertrule-core::determinism`).
8
9use std::fmt;
10
11/// Newtype wrapper over canonical JCS output bytes.
12///
13/// Construction is restricted to this crate — callers obtain a
14/// [`CanonicalBytes`] only by routing through
15/// [`crate::canonical_bytes_from_slice`] (or the wrappers in
16/// `vertrule-core::determinism`). The type exists so digest, signature,
17/// and receipt APIs can statically require "bytes that came out of JCS"
18/// rather than accepting any `&[u8]`. Every coercion back to `&[u8]`
19/// goes through the explicit [`Self::as_slice`] method — there is no
20/// `AsRef<[u8]>` or `Deref` impl, so escapes are greppable.
21///
22/// The `Debug` impl deliberately shows the byte length and not the
23/// bytes. Dumping raw canonical JSON into a log is a common way to
24/// accidentally leak receipt contents; callers that want the bytes
25/// must ask for them.
26#[derive(Clone, PartialEq, Eq)]
27pub struct CanonicalBytes(Vec<u8>);
28
29impl CanonicalBytes {
30    /// Construct from already-canonicalized bytes. Crate-private: the
31    /// only way to get a [`CanonicalBytes`] is to feed input through
32    /// [`crate::canonical_bytes_from_slice`] (or the re-export in
33    /// `vertrule-core::determinism::to_canon_bytes_wrapped`).
34    pub(crate) const fn from_jcs(bytes: Vec<u8>) -> Self {
35        Self(bytes)
36    }
37
38    /// Explicit escape hatch to a byte slice. Named so reviewers can
39    /// grep for the boundary where canonical-bytes discipline is
40    /// dropped (e.g., feeding wire bytes to `blake3::hash`).
41    #[must_use]
42    pub fn as_slice(&self) -> &[u8] {
43        &self.0
44    }
45
46    /// Length in bytes.
47    #[must_use]
48    pub fn len(&self) -> usize {
49        self.0.len()
50    }
51
52    /// True when the canonical output is empty (only possible for an
53    /// empty input in degenerate paths; not reachable from the primary
54    /// API).
55    #[must_use]
56    pub fn is_empty(&self) -> bool {
57        self.0.is_empty()
58    }
59
60    /// Consume the wrapper and return the underlying byte buffer. Use
61    /// at wire boundaries where ownership is transferred (file write,
62    /// network send). Not an implicit coercion.
63    #[must_use]
64    pub fn into_vec(self) -> Vec<u8> {
65        self.0
66    }
67}
68
69impl fmt::Debug for CanonicalBytes {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.debug_struct("CanonicalBytes")
72            .field("len", &self.0.len())
73            .finish_non_exhaustive()
74    }
75}