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}