use std::fmt;
use sha2::{Digest, Sha256};
use crate::key::Key;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum ContentId {
Store {
store_id: [u8; 32],
},
Root {
store_id: [u8; 32],
root: [u8; 32],
},
Resource {
store_id: [u8; 32],
root: [u8; 32],
retrieval_key: [u8; 32],
},
}
const TAG_STORE: u8 = 0x01;
const TAG_ROOT: u8 = 0x02;
const TAG_RESOURCE: u8 = 0x03;
impl ContentId {
pub const fn store(store_id: [u8; 32]) -> Self {
ContentId::Store { store_id }
}
pub const fn root(store_id: [u8; 32], root: [u8; 32]) -> Self {
ContentId::Root { store_id, root }
}
pub const fn capsule(store_id: [u8; 32], root: [u8; 32]) -> Self {
ContentId::Root { store_id, root }
}
pub const fn resource(store_id: [u8; 32], root: [u8; 32], retrieval_key: [u8; 32]) -> Self {
ContentId::Resource {
store_id,
root,
retrieval_key,
}
}
pub const fn store_id(&self) -> &[u8; 32] {
match self {
ContentId::Store { store_id }
| ContentId::Root { store_id, .. }
| ContentId::Resource { store_id, .. } => store_id,
}
}
fn canonical_bytes(&self) -> Vec<u8> {
match self {
ContentId::Store { store_id } => {
let mut v = Vec::with_capacity(1 + 32);
v.push(TAG_STORE);
v.extend_from_slice(store_id);
v
}
ContentId::Root { store_id, root } => {
let mut v = Vec::with_capacity(1 + 64);
v.push(TAG_ROOT);
v.extend_from_slice(store_id);
v.extend_from_slice(root);
v
}
ContentId::Resource {
store_id,
root,
retrieval_key,
} => {
let mut v = Vec::with_capacity(1 + 96);
v.push(TAG_RESOURCE);
v.extend_from_slice(store_id);
v.extend_from_slice(root);
v.extend_from_slice(retrieval_key);
v
}
}
}
pub fn to_key(&self) -> Key {
let digest = Sha256::digest(self.canonical_bytes());
let bytes: [u8; 32] = digest.into();
Key::from_bytes(bytes)
}
}
impl fmt::Debug for ContentId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContentId::Store { store_id } => f
.debug_struct("ContentId::Store")
.field("store_id", &hex32(store_id))
.finish(),
ContentId::Root { store_id, root } => f
.debug_struct("ContentId::Root")
.field("store_id", &hex32(store_id))
.field("root", &hex32(root))
.finish(),
ContentId::Resource {
store_id,
root,
retrieval_key,
} => f
.debug_struct("ContentId::Resource")
.field("store_id", &hex32(store_id))
.field("root", &hex32(root))
.field("retrieval_key", &hex32(retrieval_key))
.finish(),
}
}
}
fn hex32(b: &[u8; 32]) -> String {
let mut s = String::with_capacity(64);
for x in b {
s.push(char::from_digit((x >> 4) as u32, 16).unwrap());
s.push(char::from_digit((x & 0x0f) as u32, 16).unwrap());
}
s
}
#[cfg(test)]
mod tests {
use super::*;
const S: [u8; 32] = [0x11; 32];
const R: [u8; 32] = [0x22; 32];
const RK: [u8; 32] = [0x33; 32];
#[test]
fn to_key_is_deterministic() {
assert_eq!(ContentId::store(S).to_key(), ContentId::store(S).to_key());
assert_eq!(
ContentId::resource(S, R, RK).to_key(),
ContentId::resource(S, R, RK).to_key()
);
}
#[test]
fn different_granularities_of_same_store_have_distinct_keys() {
let k_store = ContentId::store(S).to_key();
let k_root = ContentId::root(S, R).to_key();
let k_res = ContentId::resource(S, R, RK).to_key();
assert_ne!(k_store, k_root);
assert_ne!(k_root, k_res);
assert_ne!(k_store, k_res);
}
#[test]
fn capsule_is_an_alias_of_root() {
assert_eq!(ContentId::capsule(S, R), ContentId::root(S, R));
assert_eq!(
ContentId::capsule(S, R).to_key(),
ContentId::root(S, R).to_key()
);
}
#[test]
fn store_id_accessor_present_at_every_granularity() {
assert_eq!(ContentId::store(S).store_id(), &S);
assert_eq!(ContentId::root(S, R).store_id(), &S);
assert_eq!(ContentId::resource(S, R, RK).store_id(), &S);
}
#[test]
fn distinct_stores_have_distinct_keys() {
let other = [0x99; 32];
assert_ne!(
ContentId::store(S).to_key(),
ContentId::store(other).to_key()
);
}
#[test]
fn tag_prefix_prevents_field_shifting_collision() {
let a = ContentId::root(S, R).to_key();
let b = ContentId::resource(S, R, [0u8; 32]).to_key();
assert_ne!(a, b);
}
#[test]
fn canonical_bytes_have_the_expected_tag_and_length() {
let store = ContentId::store(S).canonical_bytes();
assert_eq!(store.len(), 1 + 32);
assert_eq!(store[0], TAG_STORE);
let root = ContentId::root(S, R).canonical_bytes();
assert_eq!(root.len(), 1 + 64);
assert_eq!(root[0], TAG_ROOT);
let res = ContentId::resource(S, R, RK).canonical_bytes();
assert_eq!(res.len(), 1 + 96);
assert_eq!(res[0], TAG_RESOURCE);
}
#[test]
fn debug_renders_hex_for_every_variant() {
let store = format!("{:?}", ContentId::store(S));
assert!(store.contains("ContentId::Store"));
assert!(store.contains(&"11".repeat(32)));
let root = format!("{:?}", ContentId::root(S, R));
assert!(root.contains("ContentId::Root"));
assert!(root.contains(&"22".repeat(32)));
let res = format!("{:?}", ContentId::resource(S, R, RK));
assert!(res.contains("ContentId::Resource"));
assert!(res.contains(&"33".repeat(32)));
}
}