Skip to main content

vitaminc_context/
piece.rs

1//! A context's parts, and the one encoding of them.
2//!
3//! [`ContextPiece`] is the tree a context is made of: text, bytes, integers
4//! and unit at the leaves, lists at the branches. Every context type
5//! describes itself as one through [`IntoContext`], and
6//! [`ContextPiece::encode`] is the only place that turns a tree into bytes.
7//! That is what makes the AEAD's associated data and the PRF's context the
8//! same bytes for the same value: both are views of this encoding, and
9//! neither has an encoder of its own.
10//!
11//! # Encoding
12//!
13//! A typed leaf (text, bytes, an integer) encodes as a three-piece frame
14//! naming what it is:
15//!
16//! ```text
17//! PAE(b"vitaminc/context/value/v1", type_tag, value_bytes)
18//! ```
19//!
20//! where the type tag is `vitaminc/context/utf8/v1`,
21//! `vitaminc/context/bytes/v1`, `vitaminc/context/u64-le/v1`, and so on,
22//! and the value bytes are UTF-8 for text, the bytes themselves for bytes,
23//! and little-endian two's complement for an integer. Two values of
24//! different types therefore never encode alike, even when their bytes do:
25//! `7u32` and `7i32` are different contexts, and so are `"ab"` and `b"ab"`.
26//!
27//! A list encodes as the PAE of its parts' encodings, at every level.
28//! `Some(x)` is the one-element list, `None` the empty list, `(a, b)` the
29//! two-element list, and `nonempty!(a).with(b).with(c)` the nested list
30//! `((a, b), c)`.
31//!
32//! Two leaves are not typed. [`Unit`](ContextPiece::Unit) is `()`, the
33//! empty context, and encodes as no bytes at all. [`Encoded`](ContextPiece::Encoded)
34//! is bytes this encoder already produced, and encodes as itself. Neither
35//! can be confused with a typed leaf, which is never empty and always
36//! begins with the value frame.
37
38use std::borrow::Cow;
39use std::fmt;
40
41use vitaminc_protected::MaybeEmpty;
42
43use crate::{pae, Context};
44
45/// The first piece of every typed leaf.
46const VALUE_DOMAIN: &[u8] = b"vitaminc/context/value/v1";
47
48/// The type tag of a typed leaf, its second piece.
49mod tag {
50    pub(super) const UTF8: &[u8] = b"vitaminc/context/utf8/v1";
51    pub(super) const BYTES: &[u8] = b"vitaminc/context/bytes/v1";
52    pub(super) const U8: &[u8] = b"vitaminc/context/u8-le/v1";
53    pub(super) const U16: &[u8] = b"vitaminc/context/u16-le/v1";
54    pub(super) const U32: &[u8] = b"vitaminc/context/u32-le/v1";
55    pub(super) const U64: &[u8] = b"vitaminc/context/u64-le/v1";
56    pub(super) const U128: &[u8] = b"vitaminc/context/u128-le/v1";
57    pub(super) const I8: &[u8] = b"vitaminc/context/i8-le/v1";
58    pub(super) const I16: &[u8] = b"vitaminc/context/i16-le/v1";
59    pub(super) const I32: &[u8] = b"vitaminc/context/i32-le/v1";
60    pub(super) const I64: &[u8] = b"vitaminc/context/i64-le/v1";
61    pub(super) const I128: &[u8] = b"vitaminc/context/i128-le/v1";
62}
63
64/// One part of a context, or a list of parts.
65///
66/// [`IntoContext::into_context`](crate::IntoContext::into_context) builds
67/// one from any context type, and [`encode`](Self::encode) turns it into the
68/// bytes both the AEAD and the PRF use. The tree is a stand-in for the value
69/// it was built from: a runtime list with the same parts is the same context
70/// as the static value, on both sides, because there is only one encoding.
71///
72/// ```rust
73/// use std::borrow::Cow;
74/// use vitaminc_context::{ContextPiece, IntoContext};
75///
76/// let value = ("users/email", 7u64);
77/// let runtime = ContextPiece::List(vec![
78///     ContextPiece::Text(Cow::Borrowed("users/email")),
79///     ContextPiece::U64(7),
80/// ]);
81/// assert_eq!(runtime.encode(), value.into_context().encode());
82/// ```
83///
84/// This matters for a context that arrives as data rather than as a Rust
85/// type, for example across an FFI boundary. It needs no mirror type of its
86/// own:
87///
88/// - `Some(x)` is the one-element list and `None` is the empty list;
89/// - `(a, b)` is the two-element list;
90/// - `nonempty!(a).with(b).with(c)` is the nested list `((a, b), c)`;
91/// - `()` is [`Unit`](Self::Unit).
92///
93/// A flat list of three or more parts is also a valid context. It has no
94/// tuple spelling in Rust, so build it with [`List`](Self::List) directly.
95///
96/// [`Display`](fmt::Display) renders a tree so that different trees never
97/// print the same, for example `("users/email", 7u64)`, and
98/// [`leaves`](Self::leaves) walks the parts in encoding order for a caller
99/// that wants to render or bind them itself.
100///
101/// `PartialEq` compares trees, not encodings, and since every typed leaf is
102/// tagged, trees that differ encode differently too. The one exception is
103/// [`Encoded`](Self::Encoded): a `Bytes` leaf and an `Encoded` leaf holding
104/// that leaf's encoding are unequal as trees and equal as bytes, which is
105/// what `Encoded` is for.
106///
107/// [`MaybeEmpty`] is implemented by the same rule the static types use, so
108/// a tree can be wrapped in [`NonEmpty`](vitaminc_protected::NonEmpty):
109/// text and bytes are empty at zero length, unit is empty, an integer never
110/// is, and a list is empty only when every part is.
111/// [`Encoded`](Self::Encoded) counts as empty whatever its bytes, because
112/// framing hides whether the value behind them carried anything.
113///
114/// The enum is `#[non_exhaustive]`, so a new kind of leaf must not break a
115/// downstream `match`.
116#[derive(Debug, Clone, PartialEq, Eq)]
117#[non_exhaustive]
118pub enum ContextPiece<'a> {
119    /// Text; a typed leaf of its UTF-8 bytes. `&str`, `String`.
120    Text(Cow<'a, str>),
121    /// Opaque bytes; a typed leaf of the bytes themselves. Byte slices and
122    /// arrays, `Vec<u8>`, `Cow<[u8]>`.
123    Bytes(Cow<'a, [u8]>),
124    /// The unit context, `()`. Encodes as no bytes at all. Not the same as
125    /// empty bytes, which are a typed leaf, or the empty list, which is
126    /// framed.
127    Unit,
128    /// A `u8`; a typed leaf of one little-endian byte.
129    U8(u8),
130    /// A `u16`; a typed leaf of two little-endian bytes.
131    U16(u16),
132    /// A `u32`; a typed leaf of four little-endian bytes.
133    U32(u32),
134    /// A `u64`; a typed leaf of eight little-endian bytes.
135    U64(u64),
136    /// A `u128`; a typed leaf of sixteen little-endian bytes.
137    U128(u128),
138    /// An `i8`; a typed leaf of one two's-complement byte.
139    I8(i8),
140    /// An `i16`; a typed leaf of two little-endian two's-complement bytes.
141    I16(i16),
142    /// An `i32`; a typed leaf of four little-endian two's-complement bytes.
143    I32(i32),
144    /// An `i64`; a typed leaf of eight little-endian two's-complement bytes.
145    I64(i64),
146    /// An `i128`; a typed leaf of sixteen little-endian two's-complement
147    /// bytes.
148    I128(i128),
149    /// Bytes this encoder already produced; encodes as itself, untagged. The
150    /// parts view of a [`Context`], which is how a stored or derived context
151    /// is passed back in as a value. Only [`Context::from_encoded`] and the
152    /// derived-context methods on [`Context`] produce one. It counts as
153    /// [empty](MaybeEmpty) whatever its bytes, because framing hides
154    /// whether the value behind them carried anything.
155    Encoded(Cow<'a, [u8]>),
156    /// A list of parts; encodes as their PAE. A tuple is the list of its
157    /// halves, `Some(x)` the one-element list, `None` the empty list.
158    List(Vec<ContextPiece<'a>>),
159}
160
161impl<'a> ContextPiece<'a> {
162    /// The canonical bytes of this context.
163    ///
164    /// A typed leaf and a list allocate exactly once, sized up front. `Unit`
165    /// allocates nothing. `Encoded` hands its bytes through as they are, so
166    /// a borrowed encoded context stays borrowed.
167    pub fn encode(self) -> Context<'a> {
168        match self {
169            ContextPiece::Unit => Context::empty(),
170            ContextPiece::Encoded(bytes) => Context(bytes),
171            piece => {
172                let len = piece.encoded_len();
173                let mut buf = Vec::with_capacity(len);
174                piece.write_into(&mut buf);
175                debug_assert_eq!(buf.len(), len, "encoded_len must equal the bytes written");
176                Context(Cow::Owned(buf))
177            }
178        }
179    }
180
181    /// Copy every borrowed part, so the tree can outlive its source.
182    pub fn into_owned(self) -> ContextPiece<'static> {
183        match self {
184            ContextPiece::Text(text) => ContextPiece::Text(Cow::Owned(text.into_owned())),
185            ContextPiece::Bytes(bytes) => ContextPiece::Bytes(Cow::Owned(bytes.into_owned())),
186            ContextPiece::Unit => ContextPiece::Unit,
187            ContextPiece::U8(v) => ContextPiece::U8(v),
188            ContextPiece::U16(v) => ContextPiece::U16(v),
189            ContextPiece::U32(v) => ContextPiece::U32(v),
190            ContextPiece::U64(v) => ContextPiece::U64(v),
191            ContextPiece::U128(v) => ContextPiece::U128(v),
192            ContextPiece::I8(v) => ContextPiece::I8(v),
193            ContextPiece::I16(v) => ContextPiece::I16(v),
194            ContextPiece::I32(v) => ContextPiece::I32(v),
195            ContextPiece::I64(v) => ContextPiece::I64(v),
196            ContextPiece::I128(v) => ContextPiece::I128(v),
197            ContextPiece::Encoded(bytes) => ContextPiece::Encoded(Cow::Owned(bytes.into_owned())),
198            ContextPiece::List(parts) => {
199                ContextPiece::List(parts.into_iter().map(ContextPiece::into_owned).collect())
200            }
201        }
202    }
203
204    /// The non-list parts, depth first, in the order they are encoded.
205    /// A leaf piece yields itself; an empty list yields nothing.
206    ///
207    /// Nesting is dropped, so distinct contexts can share a leaf sequence:
208    /// `(("a", 1u8), "b")` and `("a", (1u8, "b"))` both yield `a, 1, b`
209    /// while encoding to different bytes. Use this to render or bind the
210    /// parts, not to identify the context; the bytes from
211    /// [`encode`](Self::encode) are its identity.
212    pub fn leaves(&self) -> impl Iterator<Item = &ContextPiece<'a>> {
213        fn walk<'p, 'a>(piece: &'p ContextPiece<'a>, out: &mut Vec<&'p ContextPiece<'a>>) {
214            match piece {
215                ContextPiece::List(parts) => parts.iter().for_each(|part| walk(part, out)),
216                leaf => out.push(leaf),
217            }
218        }
219        let mut out = Vec::new();
220        walk(self, &mut out);
221        out.into_iter()
222    }
223
224    /// The type tag and value length of a typed leaf, or `None` for the
225    /// three kinds that are not typed leaves.
226    fn typed(&self) -> Option<(&'static [u8], usize)> {
227        Some(match self {
228            ContextPiece::Text(text) => (tag::UTF8, text.len()),
229            ContextPiece::Bytes(bytes) => (tag::BYTES, bytes.len()),
230            ContextPiece::U8(_) => (tag::U8, 1),
231            ContextPiece::U16(_) => (tag::U16, 2),
232            ContextPiece::U32(_) => (tag::U32, 4),
233            ContextPiece::U64(_) => (tag::U64, 8),
234            ContextPiece::U128(_) => (tag::U128, 16),
235            ContextPiece::I8(_) => (tag::I8, 1),
236            ContextPiece::I16(_) => (tag::I16, 2),
237            ContextPiece::I32(_) => (tag::I32, 4),
238            ContextPiece::I64(_) => (tag::I64, 8),
239            ContextPiece::I128(_) => (tag::I128, 16),
240            ContextPiece::Unit | ContextPiece::Encoded(_) | ContextPiece::List(_) => return None,
241        })
242    }
243
244    /// The length of this piece's encoding, without producing it.
245    fn encoded_len(&self) -> usize {
246        match self {
247            ContextPiece::Unit => 0,
248            ContextPiece::Encoded(bytes) => bytes.len(),
249            ContextPiece::List(parts) => pae::encoded_len(parts.iter().map(Self::encoded_len)),
250            typed => {
251                let (tag, value_len) = typed
252                    .typed()
253                    .expect("every other kind of piece is a typed leaf");
254                pae::encoded_len([VALUE_DOMAIN.len(), tag.len(), value_len].into_iter())
255            }
256        }
257    }
258
259    /// Appends this piece's encoding to `buf`. A typed leaf is written as
260    /// its three-piece frame. A list is written in one pass: each part's
261    /// length word is reserved before the part is written and filled in
262    /// after, from the bytes actually produced, so a tree of any depth is
263    /// written without intermediate buffers and without rescanning subtrees.
264    fn write_into(&self, buf: &mut Vec<u8>) {
265        match self {
266            ContextPiece::Unit => {}
267            ContextPiece::Encoded(bytes) => buf.extend_from_slice(bytes),
268            ContextPiece::List(parts) => {
269                buf.extend_from_slice(&(parts.len() as u64).to_le_bytes());
270                for part in parts {
271                    let length_word = buf.len();
272                    buf.extend_from_slice(&[0u8; 8]);
273                    let start = buf.len();
274                    part.write_into(buf);
275                    let written = (buf.len() - start) as u64;
276                    buf[length_word..start].copy_from_slice(&written.to_le_bytes());
277                }
278            }
279            ContextPiece::Text(text) => write_typed(buf, tag::UTF8, text.as_bytes()),
280            ContextPiece::Bytes(bytes) => write_typed(buf, tag::BYTES, bytes),
281            ContextPiece::U8(v) => write_typed(buf, tag::U8, &v.to_le_bytes()),
282            ContextPiece::U16(v) => write_typed(buf, tag::U16, &v.to_le_bytes()),
283            ContextPiece::U32(v) => write_typed(buf, tag::U32, &v.to_le_bytes()),
284            ContextPiece::U64(v) => write_typed(buf, tag::U64, &v.to_le_bytes()),
285            ContextPiece::U128(v) => write_typed(buf, tag::U128, &v.to_le_bytes()),
286            ContextPiece::I8(v) => write_typed(buf, tag::I8, &v.to_le_bytes()),
287            ContextPiece::I16(v) => write_typed(buf, tag::I16, &v.to_le_bytes()),
288            ContextPiece::I32(v) => write_typed(buf, tag::I32, &v.to_le_bytes()),
289            ContextPiece::I64(v) => write_typed(buf, tag::I64, &v.to_le_bytes()),
290            ContextPiece::I128(v) => write_typed(buf, tag::I128, &v.to_le_bytes()),
291        }
292    }
293}
294
295/// Appends the typed-leaf frame `PAE(VALUE_DOMAIN, tag, value)` to `buf`.
296fn write_typed(buf: &mut Vec<u8>, tag: &[u8], value: &[u8]) {
297    pae::write(buf, &[VALUE_DOMAIN, tag, value]);
298}
299
300/// The same emptiness rule the static types use, applied to the tree. Text
301/// and bytes are empty at zero length. Unit is empty. An integer is never
302/// empty, because even zero is information the caller chose. A list is
303/// empty only when every part is, so `None` and `Some("")` are empty and
304/// `("", 7u64)` is not, matching what `Option<T>` and `(A, B)` decide.
305///
306/// An [`Encoded`](ContextPiece::Encoded) leaf counts as empty whatever its
307/// bytes, because those bytes cannot say whether the value behind them
308/// carried anything: framing gives an empty value a non-empty encoding, so
309/// `None` arrives back as an eight-byte count word and a byte check would
310/// certify exactly the degenerate context [`NonEmpty`](vitaminc_protected::NonEmpty)
311/// exists to exclude.
312/// It is the same reason [`Context`] has no `MaybeEmpty` impl of its own.
313/// An encoded context therefore never contributes to a proof: prove the
314/// value non-empty before it is encoded, or extend a proven head with
315/// [`NonEmpty::with`](vitaminc_protected::NonEmpty::with), which pairs a
316/// tail in without checking it.
317impl MaybeEmpty for ContextPiece<'_> {
318    fn is_empty(&self) -> bool {
319        match self {
320            ContextPiece::Text(text) => text.is_empty(),
321            ContextPiece::Bytes(bytes) => bytes.is_empty(),
322            ContextPiece::Unit | ContextPiece::Encoded(_) => true,
323            ContextPiece::U8(_)
324            | ContextPiece::U16(_)
325            | ContextPiece::U32(_)
326            | ContextPiece::U64(_)
327            | ContextPiece::U128(_)
328            | ContextPiece::I8(_)
329            | ContextPiece::I16(_)
330            | ContextPiece::I32(_)
331            | ContextPiece::I64(_)
332            | ContextPiece::I128(_) => false,
333            ContextPiece::List(parts) => parts.iter().all(MaybeEmpty::is_empty),
334        }
335    }
336}
337
338/// Renders the tree in Rust literal syntax, and different trees never
339/// render the same. Text is quoted and escaped as `Debug` does, integers
340/// carry their type suffix, bytes print as `0x`-prefixed hex, encoded bytes
341/// print as `Encoded(0x…)`, unit prints as `()`, a list is parenthesised and
342/// comma-separated, and the empty list prints as `None` (the context it
343/// is). So `("users/email", 7u64)` prints as `("users/email", 7u64)`.
344///
345/// Each kind starts differently: `"` for text, a digit or `-` for an
346/// integer, `0x` for bytes, `E` for encoded bytes, `()` for unit, `(`
347/// followed by a part for a list, and `None` for the empty list. Integer
348/// suffixes keep types of the same width apart, and quoting keeps
349/// separators inside text from reading as structure. Two contexts that
350/// encode to different bytes therefore never share a log line.
351impl fmt::Display for ContextPiece<'_> {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        fn hex(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
354            f.write_str("0x")?;
355            bytes.iter().try_for_each(|byte| write!(f, "{byte:02x}"))
356        }
357        match self {
358            ContextPiece::Text(text) => write!(f, "{text:?}"),
359            ContextPiece::Bytes(bytes) => hex(f, bytes),
360            ContextPiece::Encoded(bytes) => {
361                f.write_str("Encoded(")?;
362                hex(f, bytes)?;
363                f.write_str(")")
364            }
365            ContextPiece::Unit => f.write_str("()"),
366            ContextPiece::U8(v) => write!(f, "{v}u8"),
367            ContextPiece::U16(v) => write!(f, "{v}u16"),
368            ContextPiece::U32(v) => write!(f, "{v}u32"),
369            ContextPiece::U64(v) => write!(f, "{v}u64"),
370            ContextPiece::U128(v) => write!(f, "{v}u128"),
371            ContextPiece::I8(v) => write!(f, "{v}i8"),
372            ContextPiece::I16(v) => write!(f, "{v}i16"),
373            ContextPiece::I32(v) => write!(f, "{v}i32"),
374            ContextPiece::I64(v) => write!(f, "{v}i64"),
375            ContextPiece::I128(v) => write!(f, "{v}i128"),
376            ContextPiece::List(parts) if parts.is_empty() => f.write_str("None"),
377            ContextPiece::List(parts) => {
378                f.write_str("(")?;
379                for (i, part) in parts.iter().enumerate() {
380                    if i > 0 {
381                        f.write_str(", ")?;
382                    }
383                    fmt::Display::fmt(part, f)?;
384                }
385                f.write_str(")")
386            }
387        }
388    }
389}
390
391/// A random tree over every kind of leaf, at most three lists deep and at
392/// most three parts wide. Available with the `arbitrary` feature, so a
393/// downstream crate can state a property over every tree this crate can
394/// encode.
395#[cfg(any(test, feature = "arbitrary"))]
396impl quickcheck::Arbitrary for ContextPiece<'static> {
397    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
398        arbitrary_piece(g, 3)
399    }
400}
401
402/// A random piece. A list may appear only while `depth` is above zero, and
403/// its parts are drawn one level shallower, so `depth` bounds the nesting.
404#[cfg(any(test, feature = "arbitrary"))]
405fn arbitrary_piece(g: &mut quickcheck::Gen, depth: u8) -> ContextPiece<'static> {
406    use quickcheck::Arbitrary;
407    let kinds = if depth == 0 { 14 } else { 15 };
408    match u8::arbitrary(g) % kinds {
409        0 => ContextPiece::Text(Cow::Owned(String::arbitrary(g))),
410        1 => ContextPiece::Bytes(Cow::Owned(Vec::arbitrary(g))),
411        2 => ContextPiece::Unit,
412        3 => ContextPiece::U8(u8::arbitrary(g)),
413        4 => ContextPiece::U16(u16::arbitrary(g)),
414        5 => ContextPiece::U32(u32::arbitrary(g)),
415        6 => ContextPiece::U64(u64::arbitrary(g)),
416        7 => ContextPiece::U128(u128::arbitrary(g)),
417        8 => ContextPiece::I8(i8::arbitrary(g)),
418        9 => ContextPiece::I16(i16::arbitrary(g)),
419        10 => ContextPiece::I32(i32::arbitrary(g)),
420        11 => ContextPiece::I64(i64::arbitrary(g)),
421        12 => ContextPiece::I128(i128::arbitrary(g)),
422        13 => ContextPiece::Encoded(Cow::Owned(Vec::arbitrary(g))),
423        _ => {
424            let n = usize::arbitrary(g) % 4;
425            ContextPiece::List((0..n).map(|_| arbitrary_piece(g, depth - 1)).collect())
426        }
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    #![allow(clippy::unwrap_used)]
433
434    use quickcheck_macros::quickcheck;
435    use vitaminc_protected::NonEmpty;
436
437    use super::*;
438    use crate::IntoContext;
439
440    fn text(s: &'static str) -> ContextPiece<'static> {
441        ContextPiece::Text(Cow::Borrowed(s))
442    }
443
444    fn bytes(b: &'static [u8]) -> ContextPiece<'static> {
445        ContextPiece::Bytes(Cow::Borrowed(b))
446    }
447
448    /// The typed-leaf frame, built by hand so the test shares no code with
449    /// the encoder it checks.
450    fn typed_frame(tag: &[u8], value: &[u8]) -> Vec<u8> {
451        let pieces: [&[u8]; 3] = [b"vitaminc/context/value/v1", tag, value];
452        let mut out = (pieces.len() as u64).to_le_bytes().to_vec();
453        for piece in pieces {
454            out.extend_from_slice(&(piece.len() as u64).to_le_bytes());
455            out.extend_from_slice(piece);
456        }
457        out
458    }
459
460    /// One of each kind, for the exhaustive per-arm checks below.
461    fn every_kind() -> Vec<ContextPiece<'static>> {
462        vec![
463            text("t"),
464            bytes(b"b"),
465            ContextPiece::Unit,
466            ContextPiece::U8(1),
467            ContextPiece::U16(2),
468            ContextPiece::U32(3),
469            ContextPiece::U64(4),
470            ContextPiece::U128(5),
471            ContextPiece::I8(-1),
472            ContextPiece::I16(-2),
473            ContextPiece::I32(-3),
474            ContextPiece::I64(-4),
475            ContextPiece::I128(-5),
476            ContextPiece::Encoded(Cow::Borrowed(b"e")),
477            ContextPiece::List(vec![ContextPiece::U8(9)]),
478        ]
479    }
480
481    /// A random tree for the property tests, from the `Arbitrary` impl above,
482    /// so the generator every downstream property relies on is the one
483    /// exercised here.
484    #[derive(Debug, Clone)]
485    struct Tree(ContextPiece<'static>);
486
487    impl quickcheck::Arbitrary for Tree {
488        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
489            Tree(ContextPiece::arbitrary(g))
490        }
491    }
492
493    /// Which kind of piece this is, as the index the generator draws.
494    fn kind(piece: &ContextPiece<'_>) -> usize {
495        match piece {
496            ContextPiece::Text(_) => 0,
497            ContextPiece::Bytes(_) => 1,
498            ContextPiece::Unit => 2,
499            ContextPiece::U8(_) => 3,
500            ContextPiece::U16(_) => 4,
501            ContextPiece::U32(_) => 5,
502            ContextPiece::U64(_) => 6,
503            ContextPiece::U128(_) => 7,
504            ContextPiece::I8(_) => 8,
505            ContextPiece::I16(_) => 9,
506            ContextPiece::I32(_) => 10,
507            ContextPiece::I64(_) => 11,
508            ContextPiece::I128(_) => 12,
509            ContextPiece::Encoded(_) => 13,
510            ContextPiece::List(_) => 14,
511        }
512    }
513
514    /// How many lists deep the tree goes: a leaf is 0, a list is one more
515    /// than its deepest part.
516    fn depth(piece: &ContextPiece<'_>) -> usize {
517        match piece {
518            ContextPiece::List(parts) => 1 + parts.iter().map(depth).max().unwrap_or(0),
519            _ => 0,
520        }
521    }
522
523    mod given_the_arbitrary_generator {
524        use super::*;
525        use quickcheck::Arbitrary;
526
527        #[test]
528        fn produces_every_kind_of_piece_at_every_depth() {
529            // Every leaf kind must show up both at the top and inside a
530            // list, and lists must show up at all, or a property stated over
531            // `ContextPiece` is weaker than it claims.
532            let mut g = quickcheck::Gen::new(64);
533            let mut at_top = [false; 15];
534            let mut nested = [false; 15];
535            for _ in 0..4000 {
536                let tree = ContextPiece::arbitrary(&mut g);
537                at_top[kind(&tree)] = true;
538                if let ContextPiece::List(parts) = &tree {
539                    for part in parts {
540                        nested[kind(part)] = true;
541                    }
542                }
543            }
544            assert!(at_top.iter().all(|seen| *seen), "top level: {at_top:?}");
545            assert!(nested.iter().all(|seen| *seen), "inside a list: {nested:?}");
546        }
547
548        #[test]
549        fn depth_zero_never_draws_a_list() {
550            let mut g = quickcheck::Gen::new(64);
551            for _ in 0..4000 {
552                let piece = arbitrary_piece(&mut g, 0);
553                assert!(!matches!(piece, ContextPiece::List(_)), "{piece}");
554            }
555        }
556
557        #[test]
558        fn each_level_draws_its_parts_one_level_shallower() {
559            // At depth 1 a list may appear, but nothing inside it may be a
560            // list, and no list is wider than three parts.
561            let mut g = quickcheck::Gen::new(64);
562            let mut saw_a_list = false;
563            for _ in 0..4000 {
564                let piece = arbitrary_piece(&mut g, 1);
565                if let ContextPiece::List(parts) = &piece {
566                    saw_a_list = true;
567                    assert!(parts.len() <= 3, "{piece}");
568                    assert!(
569                        parts
570                            .iter()
571                            .all(|part| !matches!(part, ContextPiece::List(_))),
572                        "{piece}"
573                    );
574                }
575            }
576            assert!(saw_a_list, "depth 1 must be able to draw a list");
577        }
578
579        #[test]
580        fn the_impl_starts_three_deep() {
581            let mut g = quickcheck::Gen::new(64);
582            let deepest = (0..4000)
583                .map(|_| depth(&ContextPiece::arbitrary(&mut g)))
584                .max()
585                .unwrap();
586            assert!((2..=3).contains(&deepest), "deepest was {deepest}");
587        }
588    }
589
590    mod given_a_typed_leaf {
591        use super::*;
592
593        #[test]
594        fn encodes_as_the_documented_frame() {
595            assert_eq!(
596                text("ab").encode().as_bytes(),
597                typed_frame(b"vitaminc/context/utf8/v1", b"ab"),
598                "text is the utf8 frame"
599            );
600            assert_eq!(
601                bytes(b"ab").encode().as_bytes(),
602                typed_frame(b"vitaminc/context/bytes/v1", b"ab"),
603                "bytes are the bytes frame"
604            );
605            assert_eq!(
606                ContextPiece::U16(7).encode().as_bytes(),
607                typed_frame(b"vitaminc/context/u16-le/v1", &7u16.to_le_bytes()),
608                "an integer is its width's frame over little-endian bytes"
609            );
610            assert_eq!(
611                ContextPiece::I128(-7).encode().as_bytes(),
612                typed_frame(b"vitaminc/context/i128-le/v1", &(-7i128).to_le_bytes()),
613                "a signed integer is two's complement"
614            );
615        }
616
617        #[test]
618        fn every_width_and_signedness_has_its_own_tag() {
619            let tags = [
620                (ContextPiece::U8(0), "vitaminc/context/u8-le/v1"),
621                (ContextPiece::U16(0), "vitaminc/context/u16-le/v1"),
622                (ContextPiece::U32(0), "vitaminc/context/u32-le/v1"),
623                (ContextPiece::U64(0), "vitaminc/context/u64-le/v1"),
624                (ContextPiece::U128(0), "vitaminc/context/u128-le/v1"),
625                (ContextPiece::I8(0), "vitaminc/context/i8-le/v1"),
626                (ContextPiece::I16(0), "vitaminc/context/i16-le/v1"),
627                (ContextPiece::I32(0), "vitaminc/context/i32-le/v1"),
628                (ContextPiece::I64(0), "vitaminc/context/i64-le/v1"),
629                (ContextPiece::I128(0), "vitaminc/context/i128-le/v1"),
630            ];
631            for (piece, tag) in tags {
632                let encoded = piece.clone().encode();
633                let needle = tag.as_bytes();
634                assert!(
635                    encoded
636                        .as_bytes()
637                        .windows(needle.len())
638                        .any(|window| window == needle),
639                    "{piece} must carry the tag {tag}"
640                );
641            }
642        }
643
644        #[test]
645        fn same_bytes_different_type_is_a_different_context() {
646            // The collisions #315 found on the AAD side, now gone.
647            assert_ne!(text("ab").encode(), bytes(b"ab").encode());
648            assert_ne!(ContextPiece::U32(7).encode(), ContextPiece::I32(7).encode());
649            assert_ne!(ContextPiece::U8(1).encode(), ContextPiece::I8(1).encode());
650            assert_ne!(ContextPiece::U16(1).encode(), bytes(&[1, 0]).encode());
651            assert_ne!(
652                ContextPiece::U64(0).encode(),
653                ContextPiece::List(vec![]).encode(),
654                "`0u64` is not `None`"
655            );
656        }
657
658        #[test]
659        fn is_never_empty_as_bytes() {
660            assert!(!text("").encode().is_empty(), "empty text is still framed");
661            assert!(
662                !bytes(b"").encode().is_empty(),
663                "empty bytes are still framed"
664            );
665        }
666    }
667
668    mod given_the_unit_leaf {
669        use super::*;
670
671        #[test]
672        fn encodes_as_no_bytes() {
673            assert!(ContextPiece::Unit.encode().is_empty());
674            assert_eq!(ContextPiece::Unit.encode(), Context::empty());
675            assert_eq!(ContextPiece::Unit.to_string(), "()");
676        }
677
678        #[test]
679        fn is_not_empty_bytes_and_not_the_empty_list() {
680            // Three different contexts: `()` encodes as no bytes, empty bytes
681            // are a typed leaf, and the empty list is framed. The tree keeps
682            // them apart, and so do the bytes and `Display`.
683            let empty_bytes = bytes(b"");
684            let none = ContextPiece::List(vec![]);
685            assert_ne!(ContextPiece::Unit, empty_bytes);
686            assert_ne!(ContextPiece::Unit, none);
687            assert_ne!(ContextPiece::Unit.encode(), empty_bytes.clone().encode());
688            assert_ne!(ContextPiece::Unit.encode(), none.clone().encode());
689            assert_ne!(ContextPiece::Unit.to_string(), empty_bytes.to_string());
690            assert_ne!(ContextPiece::Unit.to_string(), none.to_string());
691        }
692
693        #[test]
694        fn inside_a_list_frames_a_zero_length_part() {
695            let mut expected = 1u64.to_le_bytes().to_vec();
696            expected.extend_from_slice(&0u64.to_le_bytes());
697            assert_eq!(
698                ContextPiece::List(vec![ContextPiece::Unit])
699                    .encode()
700                    .as_bytes(),
701                expected
702            );
703        }
704    }
705
706    mod given_the_encoded_leaf {
707        use super::*;
708
709        #[test]
710        fn encodes_as_itself() {
711            let piece = ContextPiece::Encoded(Cow::Borrowed(b"anything"));
712            assert_eq!(piece.encode().as_bytes(), b"anything");
713        }
714
715        #[test]
716        fn is_the_only_untagged_leaf() {
717            // `Bytes` of a frame and `Encoded` of that same frame are
718            // different trees, and only the `Encoded` one is that frame.
719            let frame = text("t").encode();
720            let as_encoded = ContextPiece::Encoded(Cow::Borrowed(frame.as_bytes()));
721            let as_bytes = ContextPiece::Bytes(Cow::Borrowed(frame.as_bytes()));
722            assert_ne!(as_encoded, as_bytes);
723            assert_eq!(as_encoded.encode(), frame);
724            assert_ne!(as_bytes.encode(), frame);
725        }
726
727        #[test]
728        fn stays_borrowed() {
729            let stored = vec![1u8, 2, 3];
730            let piece = ContextPiece::Encoded(Cow::Borrowed(&stored));
731            assert!(
732                matches!(piece.encode().0, Cow::Borrowed(_)),
733                "an encoded context is handed through without a copy"
734            );
735        }
736
737        #[test]
738        fn renders_apart_from_bytes() {
739            assert_eq!(
740                ContextPiece::Encoded(Cow::Borrowed(b"\x01\x02")).to_string(),
741                "Encoded(0x0102)"
742            );
743            assert_ne!(
744                ContextPiece::Encoded(Cow::Borrowed(b"\x01\x02")).to_string(),
745                bytes(b"\x01\x02").to_string()
746            );
747        }
748    }
749
750    mod given_a_runtime_list {
751        use super::*;
752
753        #[test]
754        fn one_part_spells_some() {
755            let some = ContextPiece::List(vec![ContextPiece::U64(7)]);
756            assert_eq!(
757                some.clone(),
758                Some(7u64).into_context(),
759                "a list of one is `Some` as a tree"
760            );
761            assert_eq!(
762                some.encode(),
763                Some(7u64).into_context().encode(),
764                "a list of one is `Some` as bytes"
765            );
766        }
767
768        #[test]
769        fn no_parts_spells_none() {
770            let none = ContextPiece::List(vec![]);
771            assert_eq!(none.encode(), Option::<u64>::None.into_context().encode());
772        }
773
774        #[test]
775        fn two_parts_spell_the_pair() {
776            let pair = ContextPiece::List(vec![text("users/age"), ContextPiece::U64(7)]);
777            assert_eq!(pair.encode(), ("users/age", 7u64).into_context().encode());
778        }
779
780        #[test]
781        fn two_parts_spell_the_proven_chain() {
782            let pair = ContextPiece::List(vec![text("users/age"), ContextPiece::U64(7)]);
783            assert_eq!(
784                pair.encode(),
785                NonEmpty::new("users/age")
786                    .unwrap()
787                    .with(7u64)
788                    .into_context()
789                    .encode(),
790                "a list of two is `nonempty!(a).with(b)`"
791            );
792        }
793
794        #[test]
795        fn a_nested_list_spells_the_left_nested_chain() {
796            let chained = ContextPiece::List(vec![
797                ContextPiece::List(vec![text("users/age"), ContextPiece::U64(7)]),
798                text("eu"),
799            ]);
800            assert_eq!(
801                chained.encode(),
802                NonEmpty::new("users/age")
803                    .unwrap()
804                    .with(7u64)
805                    .with("eu")
806                    .into_context()
807                    .encode(),
808                "`with` chains nest to the left"
809            );
810        }
811
812        #[test]
813        fn a_flat_list_is_its_own_context_not_a_chain() {
814            let flat =
815                ContextPiece::List(vec![text("users/age"), ContextPiece::U64(7), text("eu")]);
816            assert_ne!(
817                flat.encode(),
818                NonEmpty::new("users/age")
819                    .unwrap()
820                    .with(7u64)
821                    .with("eu")
822                    .into_context()
823                    .encode(),
824                "a flat n-ary list is its own context, not a chain"
825            );
826        }
827    }
828
829    mod given_a_tree {
830        use super::*;
831
832        #[quickcheck]
833        fn a_list_encodes_as_the_pae_of_its_parts(tree: Tree) -> bool {
834            // The single-pass writer must produce `LE64(count) || (LE64(len)
835            // || part)*` over the separately encoded parts, at every level.
836            // The expected value is framed by hand rather than with
837            // `Context::pae`, so the test shares no code with the writer.
838            fn expected(piece: &ContextPiece<'_>) -> Vec<u8> {
839                match piece {
840                    ContextPiece::List(parts) => {
841                        let parts: Vec<Vec<u8>> = parts.iter().map(expected).collect();
842                        let mut out = (parts.len() as u64).to_le_bytes().to_vec();
843                        for part in parts {
844                            out.extend_from_slice(&(part.len() as u64).to_le_bytes());
845                            out.extend_from_slice(&part);
846                        }
847                        out
848                    }
849                    leaf => leaf.clone().encode().as_bytes().to_vec(),
850                }
851            }
852            let len = tree.0.encoded_len();
853            let actual = tree.0.clone().encode();
854            actual.as_bytes() == expected(&tree.0).as_slice() && actual.as_bytes().len() == len
855        }
856
857        #[quickcheck]
858        fn into_owned_preserves_the_tree_and_the_bytes(tree: Tree) -> bool {
859            let owned = tree.0.clone().into_owned();
860            owned == tree.0 && owned.encode() == tree.0.encode()
861        }
862
863        #[quickcheck]
864        fn display_is_injective(a: Tree, b: Tree) -> bool {
865            a.0 == b.0 || a.0.to_string() != b.0.to_string()
866        }
867
868        #[quickcheck]
869        fn a_pae_of_encoded_parts_is_the_list_of_those_parts(parts: Vec<Vec<u8>>) -> bool {
870            // `Context::pae` and a list of `Encoded` leaves are the same
871            // framing, which is what lets a crate build its own composite
872            // shapes with `pae` and still be a list as a tree.
873            let refs: Vec<&[u8]> = parts.iter().map(Vec::as_slice).collect();
874            let list = ContextPiece::List(
875                parts
876                    .iter()
877                    .map(|p| ContextPiece::Encoded(Cow::Borrowed(p)))
878                    .collect(),
879            );
880            Context::pae(&refs) == list.encode()
881        }
882
883        #[test]
884        fn deep_left_nested_lists_encode_in_one_pass() {
885            let mut piece = ContextPiece::U8(1);
886            for i in 0..=255u8 {
887                piece = ContextPiece::List(vec![piece, ContextPiece::U8(i)]);
888            }
889            // Reference: encode bottom-up with `Context::pae`, one level at a
890            // time.
891            let mut expected = ContextPiece::U8(1).encode();
892            for i in 0..=255u8 {
893                let leaf = ContextPiece::U8(i).encode();
894                expected = Context::pae(&[expected.as_bytes(), leaf.as_bytes()]);
895            }
896            let len = piece.encoded_len();
897            let actual = piece.encode();
898            assert_eq!(actual, expected);
899            assert_eq!(actual.as_bytes().len(), len);
900        }
901
902        #[test]
903        fn every_kind_round_trips_through_into_owned_and_writes_its_own_length() {
904            for piece in every_kind() {
905                let owned = piece.clone().into_owned();
906                assert_eq!(owned, piece, "into_owned must not change the tree");
907                assert_eq!(
908                    owned.encode(),
909                    piece.clone().encode(),
910                    "into_owned must not change the bytes"
911                );
912                let mut buf = Vec::new();
913                piece.write_into(&mut buf);
914                assert_eq!(buf.len(), piece.encoded_len(), "{piece}");
915                assert_eq!(buf, piece.clone().encode().as_bytes(), "{piece}");
916            }
917        }
918
919        #[test]
920        fn every_kind_renders_as_documented() {
921            let rendered: Vec<String> = every_kind().iter().map(ToString::to_string).collect();
922            assert_eq!(
923                rendered,
924                [
925                    "\"t\"",
926                    "0x62",
927                    "()",
928                    "1u8",
929                    "2u16",
930                    "3u32",
931                    "4u64",
932                    "5u128",
933                    "-1i8",
934                    "-2i16",
935                    "-3i32",
936                    "-4i64",
937                    "-5i128",
938                    "Encoded(0x65)",
939                    "(9u8)",
940                ]
941            );
942        }
943
944        #[test]
945        fn display_is_injective_where_it_could_collide() {
946            // Same width, different type; text that looks like an integer;
947            // text that looks like hex; `Some("")` against `None`; `()`
948            // against `None`; text containing the separators; bytes against
949            // encoded bytes.
950            let pairs: [(ContextPiece<'_>, ContextPiece<'_>); 7] = [
951                (7u64.into_context(), 7i64.into_context()),
952                (("x", 7u64).into_context(), ("x", "7").into_context()),
953                ("0xdead".into_context(), [0xdeu8, 0xad].into_context()),
954                (Some("").into_context(), Option::<&str>::None.into_context()),
955                (().into_context(), Option::<&str>::None.into_context()),
956                (("a, b", "c").into_context(), ("a", "b, c").into_context()),
957                (
958                    bytes(b"\x01"),
959                    ContextPiece::Encoded(Cow::Borrowed(b"\x01")),
960                ),
961            ];
962            for (left, right) in pairs {
963                assert_ne!(left.to_string(), right.to_string());
964            }
965            assert_eq!(
966                ("a, b", (7u8, [1u8])).into_context().to_string(),
967                "(\"a, b\", (7u8, 0x01))"
968            );
969            assert_eq!(Some("").into_context().to_string(), "(\"\")");
970            assert_eq!((-7i16).into_context().to_string(), "-7i16");
971            assert_eq!(Option::<&str>::None.into_context().to_string(), "None");
972            assert_eq!(Some("a").into_context().to_string(), "(\"a\")");
973        }
974
975        #[test]
976        fn leaves_walk_in_encoding_order_and_drop_nesting() {
977            let piece = ((("a", 1u8), Option::<&str>::None), (Some("b"), [9u8])).into_context();
978            let leaves: Vec<String> = piece.leaves().map(ToString::to_string).collect();
979            assert_eq!(leaves, ["\"a\"", "1u8", "\"b\"", "0x09"]);
980            assert_eq!(
981                "x".into_context().leaves().count(),
982                1,
983                "a leaf is its own only leaf"
984            );
985            assert_eq!(Option::<&str>::None.into_context().leaves().count(), 0);
986            // Nesting is not recoverable from the leaves, as the doc says.
987            let left = (("a", 1u8), "b").into_context();
988            let right = ("a", (1u8, "b")).into_context();
989            assert!(left.leaves().eq(right.leaves()));
990            assert_ne!(left.encode(), right.encode());
991        }
992    }
993
994    mod given_emptiness {
995        use super::*;
996
997        #[quickcheck]
998        fn agrees_with_the_static_types(s: String, n: u64, o: Option<String>) -> bool {
999            s.is_empty() == s.as_str().into_context().is_empty()
1000                && !n.into_context().is_empty()
1001                && o.is_empty() == o.clone().into_context().is_empty()
1002                && (s.as_str(), n).is_empty() == (s.as_str(), n).into_context().is_empty()
1003                && (o.clone(), s.as_str()).is_empty() == (o, s.as_str()).into_context().is_empty()
1004        }
1005
1006        #[test]
1007        fn fixed_shapes_follow_the_static_rule() {
1008            assert!(
1009                ContextPiece::List(vec![]).is_empty(),
1010                "the empty list is empty"
1011            );
1012            assert!(
1013                ContextPiece::List(vec![text("")]).is_empty(),
1014                "a list of empty parts is empty"
1015            );
1016            assert!(
1017                !ContextPiece::List(vec![text(""), ContextPiece::U64(0)]).is_empty(),
1018                "an integer part makes a list non-empty, even zero"
1019            );
1020            assert!(
1021                !ContextPiece::List(vec![ContextPiece::List(vec![bytes(b"x")])]).is_empty(),
1022                "emptiness looks through nested lists"
1023            );
1024            assert!(ContextPiece::Unit.is_empty(), "unit is empty");
1025            assert!(
1026                NonEmpty::new(ContextPiece::List(vec![ContextPiece::U8(0)])).is_ok(),
1027                "a tree with an integer is provable non-empty"
1028            );
1029            assert!(
1030                NonEmpty::new(text("")).is_err(),
1031                "an empty text leaf is not provable non-empty"
1032            );
1033        }
1034
1035        #[test]
1036        fn an_encoded_part_proves_nothing() {
1037            // Framing gives an empty value non-empty bytes, so an encoded
1038            // context cannot be judged on its length: `None` comes back as
1039            // an eight-byte count word. Counting it as empty is what keeps
1040            // `NonEmpty` from certifying the degenerate context.
1041            let stored = Option::<u8>::None.into_context().encode();
1042            assert!(!stored.is_empty(), "the empty list is framed, not empty");
1043            let restored = Context::from_encoded(stored.as_bytes()).into_context();
1044            assert!(
1045                restored.is_empty(),
1046                "an encoded context carries no proof of its own"
1047            );
1048            assert!(
1049                NonEmpty::new(restored).is_err(),
1050                "and so cannot be certified non-empty"
1051            );
1052            assert!(
1053                ContextPiece::Encoded(Cow::Borrowed(b"users/email")).is_empty(),
1054                "bytes that look like content prove nothing either"
1055            );
1056            assert!(
1057                ContextPiece::List(vec![
1058                    ContextPiece::Encoded(Cow::Borrowed(b"x")),
1059                    ContextPiece::Encoded(Cow::Borrowed(b"y")),
1060                ])
1061                .is_empty(),
1062                "nor does a list of them"
1063            );
1064            assert!(
1065                NonEmpty::new(text("users"))
1066                    .expect("text is not empty")
1067                    .with(Context::from_encoded(b"x".as_slice()))
1068                    .get()
1069                    .1
1070                    .as_bytes()
1071                    == b"x",
1072                "a proven head still carries an encoded tail, unchecked"
1073            );
1074        }
1075    }
1076}