use crate::errors::AtomicResult;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub const PACK_FORMAT: u8 = 2;
const READABLE_FORMATS: &[u8] = &[1, 2];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackEntry {
pub subject: String,
pub update: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pack {
pub format: u8,
pub entries: Vec<PackEntry>,
pub tombstones: Vec<String>,
#[serde(default)]
pub coverage: BTreeMap<String, u32>,
#[serde(default)]
pub observed: BTreeMap<String, u32>,
}
impl Pack {
pub fn new(entries: Vec<PackEntry>, tombstones: Vec<String>) -> Self {
Self {
format: PACK_FORMAT,
entries,
tombstones,
coverage: BTreeMap::new(),
observed: BTreeMap::new(),
}
}
pub fn checkpoint(
entries: Vec<PackEntry>,
tombstones: Vec<String>,
coverage: BTreeMap<String, u32>,
observed: BTreeMap<String, u32>,
) -> Self {
Self {
format: PACK_FORMAT,
entries,
tombstones,
coverage,
observed,
}
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty() && self.tombstones.is_empty()
}
pub fn encode(&self) -> AtomicResult<Vec<u8>> {
rmp_serde::to_vec_named(self)
.map_err(|e| format!("failed to encode vault pack: {e}").into())
}
pub fn decode(bytes: &[u8]) -> AtomicResult<Self> {
let pack: Pack = rmp_serde::from_slice(bytes)
.map_err(|e| format!("failed to decode vault pack: {e}"))?;
if !READABLE_FORMATS.contains(&pack.format) {
return Err(format!(
"unsupported vault pack format {}, this build reads {READABLE_FORMATS:?}",
pack.format
)
.into());
}
Ok(pack)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Pack {
Pack::new(
vec![
PackEntry {
subject: "did:ad:drive/resource-a".to_string(),
update: vec![1, 2, 3, 4],
},
PackEntry {
subject: "did:ad:drive/resource-b".to_string(),
update: vec![5, 6],
},
],
vec!["did:ad:drive/deleted".to_string()],
)
}
#[test]
fn round_trips() {
let pack = sample();
let decoded = Pack::decode(&pack.encode().unwrap()).unwrap();
assert_eq!(decoded, pack);
}
#[test]
fn preserves_update_bytes_exactly() {
let update: Vec<u8> = (0u8..=255).collect();
let pack = Pack::new(
vec![PackEntry {
subject: "s".into(),
update: update.clone(),
}],
vec![],
);
let decoded = Pack::decode(&pack.encode().unwrap()).unwrap();
assert_eq!(decoded.entries[0].update, update);
}
#[test]
fn an_unknown_format_is_refused() {
let mut pack = sample();
pack.format = 99;
let encoded = pack.encode().unwrap();
let err = Pack::decode(&encoded).unwrap_err().to_string();
assert!(err.contains("format"), "{err}");
}
#[test]
fn a_format_1_pack_still_opens() {
let mut pack = sample();
pack.format = 1;
let decoded = Pack::decode(&pack.encode().unwrap()).unwrap();
assert_eq!(decoded.format, 1);
assert_eq!(decoded.entries.len(), 2);
assert!(
decoded.coverage.is_empty() && decoded.observed.is_empty(),
"the lane maps did not exist in format 1 and must default to empty"
);
}
#[test]
fn a_checkpoint_carries_its_lane_maps() {
let coverage = BTreeMap::from([("dev-a".to_string(), 7u32)]);
let observed = BTreeMap::from([("dev-a".to_string(), 7u32), ("dev-b".to_string(), 3)]);
let pack = Pack::checkpoint(vec![], vec!["gone".into()], coverage, observed);
let decoded = Pack::decode(&pack.encode().unwrap()).unwrap();
assert_eq!(decoded.coverage.get("dev-a"), Some(&7));
assert_eq!(
decoded.observed.get("dev-b"),
Some(&3),
"a lane can be observed without being covered: seen in the vault, \
not provably held by the publisher"
);
assert_eq!(
decoded.coverage.get("dev-b"),
None,
"claiming coverage the publisher cannot prove would let the control \
plane delete segments nothing else holds"
);
}
#[test]
fn garbage_errors_rather_than_panics() {
assert!(Pack::decode(b"not a pack").is_err());
assert!(Pack::decode(&[]).is_err());
}
#[test]
fn empty_is_detected() {
assert!(Pack::new(vec![], vec![]).is_empty());
assert!(!sample().is_empty());
assert!(!Pack::new(vec![], vec!["gone".into()]).is_empty());
}
}