use std::borrow::Cow;
use crate::{pae, ContextPiece, IntoContext};
const MAP_ENTRY_DOMAIN: &[u8] = b"vitaminc/context/map-entry/v1";
const MARKER_DOMAIN: &[u8] = b"vitaminc/context/marker/v1";
const LEAF_DOMAIN: &[u8] = b"vitaminc/context/leaf";
const SEQ_ELEMENT_DOMAIN: &[u8] = b"vitaminc/context/seq-element/v1";
const OPTION_SOME_DOMAIN: &[u8] = b"vitaminc/context/option-some/v1";
const REFINE_DOMAIN: &[u8] = b"vitaminc/context/refine/v1";
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Context<'a>(pub(crate) Cow<'a, [u8]>);
impl<'a> Context<'a> {
#[mutants::skip]
pub fn empty() -> Self {
Self::default()
}
pub fn from_encoded(bytes: impl Into<Cow<'a, [u8]>>) -> Self {
Self(bytes.into())
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_ref()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn into_owned(self) -> Context<'static> {
Context(Cow::Owned(self.0.into_owned()))
}
pub fn pae(pieces: &[&[u8]]) -> Context<'static> {
pae::encode(pieces)
}
pub fn refine<'b, C>(&self, component: C) -> Context<'static>
where
C: IntoContext<'b>,
{
let component = component.into_context().encode();
Self::pae(&[REFINE_DOMAIN, self.as_bytes(), component.as_bytes()])
}
pub fn for_map_entry(&self, key: &str) -> Context<'static> {
Self::pae(&[MAP_ENTRY_DOMAIN, self.as_bytes(), key.as_bytes()])
}
fn for_marker(&self, kind: &[u8]) -> Context<'static> {
Self::pae(&[MARKER_DOMAIN, self.as_bytes(), kind])
}
pub fn for_leaf(&self, version: u8) -> Context<'static> {
Self::pae(&[LEAF_DOMAIN, &[version], self.as_bytes()])
}
pub fn for_sequence_element(&self) -> Context<'static> {
Self::pae(&[SEQ_ELEMENT_DOMAIN, self.as_bytes(), b"element"])
}
pub fn for_empty_sequence(&self) -> Context<'static> {
self.for_marker(b"empty-sequence")
}
pub fn for_empty_map(&self) -> Context<'static> {
self.for_marker(b"empty-map")
}
pub fn for_none(&self) -> Context<'static> {
self.for_marker(b"none")
}
pub fn for_option_some(&self) -> Context<'static> {
Self::pae(&[OPTION_SOME_DOMAIN, self.as_bytes()])
}
}
impl<'a> IntoContext<'a> for Context<'a> {
fn into_context(self) -> ContextPiece<'a> {
ContextPiece::Encoded(self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use quickcheck_macros::quickcheck;
fn raw(bytes: &[u8]) -> Context<'_> {
Context::from_encoded(bytes)
}
mod given_raw_bytes {
use super::*;
#[test]
fn from_encoded_keeps_them_verbatim() {
let borrowed = Context::from_encoded(&[1u8, 2, 3][..]);
let owned = Context::from_encoded(vec![1u8, 2, 3]);
assert_eq!(borrowed.as_bytes(), &[1, 2, 3]);
assert_eq!(owned, borrowed, "ownership does not change the context");
assert_eq!(
borrowed.into_owned().as_bytes(),
&[1, 2, 3],
"into_owned keeps the bytes"
);
}
#[test]
fn re_encoding_is_the_identity() {
let stored = ("users", 7u64).into_context().encode();
let restored = Context::from_encoded(stored.as_bytes());
assert_eq!(
restored.into_context().encode(),
stored,
"an encoded context re-encodes to itself"
);
}
#[test]
fn from_encoded_is_not_a_typed_value() {
assert_ne!(
raw(b"7").into_context().encode(),
"7".into_context().encode()
);
}
#[test]
fn emptiness_is_judged_on_the_bytes_only() {
assert!(Context::empty().is_empty());
assert!(Context::default().is_empty());
assert!(!raw(b"raw").is_empty());
assert!(!"".into_context().encode().is_empty());
assert!(!Some("").into_context().encode().is_empty());
}
}
mod given_a_derived_context {
use super::*;
#[test]
fn map_entry_pins_its_encoding() {
assert_eq!(
raw(b"ctx").for_map_entry("name"),
Context::pae(&[b"vitaminc/context/map-entry/v1", b"ctx", b"name"])
);
}
#[test]
fn markers_pin_their_encoding() {
let ctx = raw(b"ctx");
for (derived, kind) in [
(ctx.for_empty_sequence(), b"empty-sequence".as_slice()),
(ctx.for_empty_map(), b"empty-map"),
(ctx.for_none(), b"none"),
] {
assert_eq!(
derived,
Context::pae(&[b"vitaminc/context/marker/v1", b"ctx", kind])
);
}
}
#[test]
fn leaf_pins_its_encoding() {
assert_eq!(
raw(b"ctx").for_leaf(1),
Context::pae(&[b"vitaminc/context/leaf", &[1u8], b"ctx"])
);
}
#[test]
fn sequence_element_pins_its_encoding() {
assert_eq!(
raw(b"ctx").for_sequence_element(),
Context::pae(&[b"vitaminc/context/seq-element/v1", b"ctx", b"element"])
);
}
#[test]
fn option_some_pins_its_encoding() {
assert_eq!(
raw(b"ctx").for_option_some(),
Context::pae(&[b"vitaminc/context/option-some/v1", b"ctx"])
);
}
#[test]
fn refine_pins_its_encoding() {
assert_eq!(
raw(b"parent").refine("child"),
Context::pae(&[
b"vitaminc/context/refine/v1",
b"parent",
"child".into_context().encode().as_bytes(),
])
);
}
#[test]
fn every_derivation_differs_from_the_bare_context_and_each_other() {
let ctx = raw(b"ctx");
let derived = [
ctx.for_map_entry("element"),
ctx.for_leaf(1),
ctx.for_sequence_element(),
ctx.for_empty_sequence(),
ctx.for_empty_map(),
ctx.for_none(),
ctx.for_option_some(),
ctx.refine("element"),
];
for (i, left) in derived.iter().enumerate() {
assert_ne!(left, &ctx, "a derivation is never the bare context");
for right in &derived[i + 1..] {
assert_ne!(left, right, "two derivations never coincide");
}
}
}
#[test]
fn every_derivation_differs_from_a_tuple_of_the_same_parts() {
let ctx = raw(b"ctx");
assert_ne!(
ctx.for_map_entry("name"),
(ctx.clone(), "name").into_context().encode()
);
assert_ne!(
ctx.for_leaf(1),
(b"vitaminc/context/leaf".as_slice(), (1u8, ctx.clone()))
.into_context()
.encode()
);
assert_ne!(
ctx.for_none(),
(b"vitaminc/context/marker/v1".as_slice(), ctx.clone())
.into_context()
.encode()
);
assert_ne!(
ctx.for_sequence_element(),
(b"vitaminc/context/seq-element/v1".as_slice(), ctx.clone())
.into_context()
.encode()
);
}
#[test]
fn leaf_is_version_sensitive() {
let ctx = raw(b"ctx");
assert_ne!(
ctx.for_leaf(1),
ctx.for_leaf(2),
"a relabelled version byte changes the context; that is the downgrade defence"
);
}
#[test]
fn refine_is_disjoint_from_the_reserved_derivations() {
assert_ne!(
raw(OPTION_SOME_DOMAIN).refine("child"),
raw(b"child").for_option_some()
);
assert_ne!(
raw(MAP_ENTRY_DOMAIN).refine("child"),
raw(b"parent").for_map_entry("child")
);
}
#[quickcheck]
fn map_keys_are_separated(context: Vec<u8>, a: String, b: String) -> bool {
let context = Context::from_encoded(context);
a == b || context.for_map_entry(&a) != context.for_map_entry(&b)
}
#[test]
fn map_entry_bytes_cannot_move_between_context_and_key() {
assert_ne!(
raw(b"ctxa").for_map_entry(""),
raw(b"ctx").for_map_entry("a")
);
assert_ne!(
Context::empty().for_map_entry("ab"),
Context::empty().for_map_entry("a")
);
}
}
}