1use std::borrow::Cow;
39use std::fmt;
40
41use vitaminc_protected::MaybeEmpty;
42
43use crate::{pae, Context};
44
45const VALUE_DOMAIN: &[u8] = b"vitaminc/context/value/v1";
47
48mod 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#[derive(Debug, Clone, PartialEq, Eq)]
117#[non_exhaustive]
118pub enum ContextPiece<'a> {
119 Text(Cow<'a, str>),
121 Bytes(Cow<'a, [u8]>),
124 Unit,
128 U8(u8),
130 U16(u16),
132 U32(u32),
134 U64(u64),
136 U128(u128),
138 I8(i8),
140 I16(i16),
142 I32(i32),
144 I64(i64),
146 I128(i128),
149 Encoded(Cow<'a, [u8]>),
156 List(Vec<ContextPiece<'a>>),
159}
160
161impl<'a> ContextPiece<'a> {
162 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 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 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 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 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 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
295fn write_typed(buf: &mut Vec<u8>, tag: &[u8], value: &[u8]) {
297 pae::write(buf, &[VALUE_DOMAIN, tag, value]);
298}
299
300impl 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
338impl 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#[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#[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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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}