Skip to main content

vitaminc_context/
into_context.rs

1//! The one trait a context type implements.
2
3use std::borrow::Cow;
4
5use vitaminc_protected::NonEmpty;
6
7use crate::ContextPiece;
8
9/// Types that describe themselves as a context.
10///
11/// This is the only trait a context type implements. It names the parts,
12/// as a [`ContextPiece`] tree, and never the bytes: the bytes come from
13/// [`ContextPiece::encode`], which is the one encoder both the AEAD's
14/// `IntoAad` and the PRF's `IntoPrfContext` are built on. A type that
15/// implements `IntoContext` gets both of those for free, and cannot be
16/// given a different encoding on one side than on the other.
17///
18/// Implementations are provided for text, bytes, every fixed-width integer,
19/// `()`, `Option<T>`, pairs, `NonEmpty<T>`, an already-encoded
20/// [`Context`](crate::Context) and the tree itself. Compose them: a
21/// context type of your own describes itself in terms of the built-ins.
22///
23/// ```rust
24/// use vitaminc_context::{ContextPiece, IntoContext};
25///
26/// struct TenantId(u64);
27///
28/// impl<'a> IntoContext<'a> for TenantId {
29///     fn into_context(self) -> ContextPiece<'a> {
30///         ("tenant", self.0).into_context()
31///     }
32/// }
33///
34/// let piece = TenantId(7).into_context();
35/// assert_eq!(piece.to_string(), "(\"tenant\", 7u64)");
36/// assert_eq!(piece.encode(), ("tenant", 7u64).into_context().encode());
37/// ```
38pub trait IntoContext<'a> {
39    /// Describe `self` as a tree of parts.
40    fn into_context(self) -> ContextPiece<'a>;
41}
42
43/// The tree of a tree is itself.
44impl<'a> IntoContext<'a> for ContextPiece<'a> {
45    fn into_context(self) -> ContextPiece<'a> {
46        self
47    }
48}
49
50/// `NonEmpty<T>` is transparent: it describes itself exactly as `T` does.
51/// The wrapper changes what the type promises, never the context.
52impl<'a, T> IntoContext<'a> for NonEmpty<T>
53where
54    T: IntoContext<'a>,
55{
56    fn into_context(self) -> ContextPiece<'a> {
57        self.into_inner().into_context()
58    }
59}
60
61/// The unit context, [`ContextPiece::Unit`]: no bytes at all.
62impl<'a> IntoContext<'a> for () {
63    fn into_context(self) -> ContextPiece<'a> {
64        ContextPiece::Unit
65    }
66}
67
68impl<'a> IntoContext<'a> for &'a str {
69    fn into_context(self) -> ContextPiece<'a> {
70        ContextPiece::Text(Cow::Borrowed(self))
71    }
72}
73
74impl<'a> IntoContext<'a> for String {
75    fn into_context(self) -> ContextPiece<'a> {
76        ContextPiece::Text(Cow::Owned(self))
77    }
78}
79
80impl<'a> IntoContext<'a> for &'a [u8] {
81    fn into_context(self) -> ContextPiece<'a> {
82        ContextPiece::Bytes(Cow::Borrowed(self))
83    }
84}
85
86impl<'a> IntoContext<'a> for Vec<u8> {
87    fn into_context(self) -> ContextPiece<'a> {
88        ContextPiece::Bytes(Cow::Owned(self))
89    }
90}
91
92impl<'a, const N: usize> IntoContext<'a> for [u8; N] {
93    fn into_context(self) -> ContextPiece<'a> {
94        ContextPiece::Bytes(Cow::Owned(self.to_vec()))
95    }
96}
97
98impl<'a, const N: usize> IntoContext<'a> for &'a [u8; N] {
99    fn into_context(self) -> ContextPiece<'a> {
100        ContextPiece::Bytes(Cow::Borrowed(self.as_slice()))
101    }
102}
103
104impl<'a> IntoContext<'a> for Cow<'a, [u8]> {
105    fn into_context(self) -> ContextPiece<'a> {
106        ContextPiece::Bytes(self)
107    }
108}
109
110macro_rules! integer_context {
111    ($($ty:ty => $variant:ident),+ $(,)?) => {$(
112        /// An integer is its own kind of leaf, so its width and signedness
113        /// are part of the context: `7u32` and `7i32` never encode alike.
114        impl<'a> IntoContext<'a> for $ty {
115            fn into_context(self) -> ContextPiece<'a> {
116                ContextPiece::$variant(self)
117            }
118        }
119    )+};
120}
121
122integer_context!(
123    u8 => U8, u16 => U16, u32 => U32, u64 => U64, u128 => U128,
124    i8 => I8, i16 => I16, i32 => I32, i64 => I64, i128 => I128,
125);
126
127/// `Some(x)` is the one-element list and `None` is the empty list, so a
128/// runtime list of one part is the same context as the static `Some`.
129impl<'a, T> IntoContext<'a> for Option<T>
130where
131    T: IntoContext<'a>,
132{
133    fn into_context(self) -> ContextPiece<'a> {
134        ContextPiece::List(self.into_iter().map(T::into_context).collect())
135    }
136}
137
138/// A pair is the two-element list.
139impl<'a, A, B> IntoContext<'a> for (A, B)
140where
141    A: IntoContext<'a>,
142    B: IntoContext<'a>,
143{
144    fn into_context(self) -> ContextPiece<'a> {
145        let (a, b) = self;
146        ContextPiece::List(vec![a.into_context(), b.into_context()])
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    #![allow(clippy::unwrap_used)]
153
154    use quickcheck_macros::quickcheck;
155
156    use super::*;
157    use crate::Context;
158
159    fn encoded<'a>(value: impl IntoContext<'a>) -> Context<'a> {
160        value.into_context().encode()
161    }
162
163    mod given_a_text_context {
164        use super::*;
165
166        #[test]
167        fn is_a_text_leaf_however_it_is_owned() {
168            assert_eq!("a".into_context(), ContextPiece::Text(Cow::Borrowed("a")));
169            assert_eq!(
170                String::from("a").into_context(),
171                ContextPiece::Text(Cow::Owned(String::from("a")))
172            );
173            assert_eq!(encoded("a"), encoded(String::from("a")));
174        }
175    }
176
177    mod given_a_bytes_context {
178        use super::*;
179
180        #[test]
181        fn every_byte_container_is_the_same_bytes_leaf() {
182            let expected = encoded(b"abc".as_slice());
183            assert_eq!(encoded(*b"abc"), expected, "an owned array");
184            assert_eq!(encoded(b"abc"), expected, "a borrowed array");
185            assert_eq!(encoded(b"abc".to_vec()), expected, "a vector");
186            assert_eq!(
187                encoded(Cow::<[u8]>::Borrowed(b"abc")),
188                expected,
189                "a borrowed cow"
190            );
191            assert_eq!(
192                encoded(Cow::<[u8]>::Owned(b"abc".to_vec())),
193                expected,
194                "an owned cow"
195            );
196        }
197
198        #[test]
199        fn text_and_bytes_are_different_contexts() {
200            assert_eq!("ab".into_context().leaves().count(), 1);
201            assert_ne!(encoded("ab"), encoded(b"ab".as_slice()));
202        }
203    }
204
205    mod given_an_integer_context {
206        use super::*;
207
208        #[test]
209        fn keeps_its_type() {
210            assert_eq!(7u32.into_context(), ContextPiece::U32(7));
211            assert_eq!((-7i16).into_context(), ContextPiece::I16(-7));
212        }
213
214        #[test]
215        fn width_and_signedness_are_part_of_the_context() {
216            assert_ne!(encoded(7u32), encoded(7u64), "different widths");
217            assert_ne!(encoded(7u32), encoded(7i32), "different signedness");
218            assert_ne!(encoded(1u16), encoded([1u8, 0]), "not its raw bytes");
219            assert_ne!(
220                encoded(0u64),
221                encoded(Option::<u64>::None),
222                "`0u64` is not `None`"
223            );
224        }
225    }
226
227    mod given_the_unit_context {
228        use super::*;
229
230        #[test]
231        fn is_the_empty_context() {
232            assert_eq!(().into_context(), ContextPiece::Unit);
233            assert_eq!(encoded(()), Context::empty());
234        }
235
236        #[test]
237        fn differs_from_none_and_from_empty_text() {
238            assert_ne!(encoded(()), encoded(Option::<&str>::None));
239            assert_ne!(encoded(()), encoded(""));
240        }
241    }
242
243    mod given_an_option_context {
244        use super::*;
245
246        #[quickcheck]
247        fn some_is_the_one_element_list(bytes: Vec<u8>) -> bool {
248            let inner = encoded(bytes.clone());
249            let some = encoded(Some(bytes));
250            some == Context::pae(&[inner.as_bytes()]) && some != inner
251        }
252
253        #[test]
254        fn none_is_the_empty_list() {
255            assert_eq!(None::<&str>.into_context(), ContextPiece::List(vec![]));
256            assert_eq!(encoded(None::<&str>), Context::pae(&[]));
257            assert_eq!(encoded(None::<&str>).as_bytes(), &[0u8; 8]);
258        }
259
260        #[test]
261        fn none_differs_from_some_of_an_empty_value() {
262            assert_ne!(encoded(None::<&str>), encoded(Some("")));
263        }
264
265        #[test]
266        fn some_does_not_carry_the_value_side_domain() {
267            // The value side of `Option` tags `Some` with its own domain; the
268            // context side does not, so a runtime list of one part matches.
269            assert_ne!(encoded(Some("value")), encoded("value").for_option_some());
270        }
271    }
272
273    mod given_a_pair_context {
274        use super::*;
275
276        #[test]
277        fn is_the_two_element_list_of_typed_parts() {
278            assert_eq!(
279                ("users/email", 7u64).into_context(),
280                ContextPiece::List(vec![
281                    ContextPiece::Text(Cow::Borrowed("users/email")),
282                    ContextPiece::U64(7),
283                ])
284            );
285            let left = encoded("left");
286            let right = encoded(7u16);
287            assert_eq!(
288                encoded(("left", 7u16)),
289                Context::pae(&[left.as_bytes(), right.as_bytes()])
290            );
291        }
292
293        #[test]
294        fn is_injective_across_its_boundary() {
295            assert_ne!(encoded(("ab", "cd")), encoded(("a", "bcd")));
296            assert_ne!(encoded(("foobar", ())), encoded(("foo", "bar")));
297        }
298
299        #[test]
300        fn unit_inside_a_pair_is_a_zero_length_part() {
301            assert_eq!(
302                encoded(("x", ())),
303                Context::pae(&[encoded("x").as_bytes(), b""])
304            );
305            assert_ne!(encoded(("x", ())), encoded("x"));
306        }
307    }
308
309    mod given_a_proven_context {
310        use super::*;
311
312        #[test]
313        fn non_empty_is_transparent() {
314            assert_eq!(
315                NonEmpty::new("users/email").unwrap().into_context(),
316                "users/email".into_context()
317            );
318            assert_eq!(
319                encoded(Some(NonEmpty::new("users/email").unwrap())),
320                encoded(Some("users/email"))
321            );
322            assert_eq!(
323                encoded(NonEmpty::new(("users", "email")).unwrap()),
324                encoded(("users", "email"))
325            );
326            assert_eq!(
327                encoded(vitaminc_protected::nonempty!("users/email").with(42u64)),
328                encoded(("users/email", 42u64)),
329                "a pair extended from a proven head is the bare pair"
330            );
331        }
332    }
333
334    mod given_an_encoded_context {
335        use super::*;
336
337        #[test]
338        fn is_the_encoded_leaf() {
339            let stored = encoded(("a", 1u8));
340            assert_eq!(
341                stored.clone().into_context(),
342                ContextPiece::Encoded(Cow::Borrowed(stored.as_bytes())).into_owned()
343            );
344            assert_eq!(encoded(stored.clone()), stored);
345        }
346
347        #[test]
348        fn inside_a_composite_is_written_verbatim() {
349            // A derived context re-passed as a value, as a map cipher does
350            // with an entry's context, is framed as itself.
351            let derived = Context::empty().for_map_entry("name");
352            assert_eq!(
353                encoded((derived.clone(), 7u8)),
354                Context::pae(&[derived.as_bytes(), encoded(7u8).as_bytes()])
355            );
356        }
357    }
358}