use std::collections::BTreeMap;
use cachekit::interop::{canonical_args, interop_key, serialize_value, InteropValue};
use serde_json::Value as Json;
const VECTORS_JSON: &str = include_str!("vectors/interop-mode.json");
const VECTORS_SHA256: &str = "e1ca6c2361509f347d17f3352e0d7ab4d4b61488737bdf0056bb5769d9794e72";
fn vectors() -> Json {
serde_json::from_str(VECTORS_JSON).expect("vendored vector file must be valid JSON")
}
#[test]
fn vendored_fixture_matches_the_pinned_sha256() {
use sha2::{Digest, Sha256};
assert_eq!(
hex::encode(Sha256::digest(VECTORS_JSON.as_bytes())),
VECTORS_SHA256,
"tests/vectors/interop-mode.json differs from the pinned protocol copy: \
re-vendor it from protocol and update VECTORS_SHA256"
);
}
fn parse_input(v: &Json) -> Result<InteropValue, String> {
match v {
Json::Null => Ok(InteropValue::Null),
Json::Bool(b) => Ok(InteropValue::from(*b)),
Json::Number(n) => {
n.as_i64()
.map(InteropValue::from)
.or_else(|| n.as_u64().map(InteropValue::from))
.ok_or_else(|| format!("unexpected non-integer JSON number: {n}"))
}
Json::String(s) => Ok(InteropValue::from(s.as_str())),
Json::Array(items) => Ok(InteropValue::Array(
items.iter().map(parse_input).collect::<Result<_, _>>()?,
)),
Json::Object(obj) => {
if obj.len() == 1 {
let (tag, val) = obj.iter().next().ok_or("empty object")?;
if let Some(parsed) = parse_tagged(tag, val)? {
return Ok(parsed);
}
}
let map: BTreeMap<String, InteropValue> = obj
.iter()
.map(|(k, v)| Ok((k.clone(), parse_input(v)?)))
.collect::<Result<_, String>>()?;
Ok(InteropValue::Map(map))
}
}
}
fn parse_tagged(tag: &str, val: &Json) -> Result<Option<InteropValue>, String> {
let as_str = || {
val.as_str()
.ok_or_else(|| format!("{tag} payload must be a string"))
};
match tag {
"$int" => {
let i: i128 = as_str()?
.parse()
.map_err(|e| format!("bad $int literal: {e}"))?;
Ok(Some(InteropValue::from(i)))
}
"$float" => {
let f: f64 = as_str()?
.parse()
.map_err(|e| format!("bad $float literal: {e}"))?;
Ok(Some(InteropValue::from(f)))
}
"$bytes" => {
let bytes = hex::decode(as_str()?).map_err(|e| format!("bad $bytes hex: {e}"))?;
Ok(Some(InteropValue::bytes(bytes)))
}
"$set" => {
let items = val
.as_array()
.ok_or("$set payload must be an array")?
.iter()
.map(parse_input)
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(InteropValue::Set(items)))
}
"$uuid" => {
let u = uuid::Uuid::parse_str(as_str()?).map_err(|e| format!("bad $uuid: {e}"))?;
Ok(Some(InteropValue::from(u)))
}
"$datetime" => Ok(Some(InteropValue::datetime_from_unix_micros(
iso8601_to_unix_micros(as_str()?)?,
))),
_ => Ok(None),
}
}
fn iso8601_to_unix_micros(s: &str) -> Result<i64, String> {
let bad = || format!("unparseable ISO 8601 datetime: {s:?}");
let (date, rest) = s.split_at_checked(10).ok_or_else(bad)?;
let mut date_parts = date.split('-');
let year: i64 = date_parts
.next()
.ok_or_else(bad)?
.parse()
.map_err(|_| bad())?;
let month: i64 = date_parts
.next()
.ok_or_else(bad)?
.parse()
.map_err(|_| bad())?;
let day: i64 = date_parts
.next()
.ok_or_else(bad)?
.parse()
.map_err(|_| bad())?;
let rest = rest.strip_prefix('T').ok_or_else(bad)?;
let (time, rest) = rest.split_at_checked(8).ok_or_else(bad)?;
let mut time_parts = time.split(':');
let hour: i64 = time_parts
.next()
.ok_or_else(bad)?
.parse()
.map_err(|_| bad())?;
let minute: i64 = time_parts
.next()
.ok_or_else(bad)?
.parse()
.map_err(|_| bad())?;
let second: i64 = time_parts
.next()
.ok_or_else(bad)?
.parse()
.map_err(|_| bad())?;
let (frac_micros, rest) = if let Some(frac) = rest.strip_prefix('.') {
let digits: String = frac.chars().take_while(char::is_ascii_digit).collect();
if digits.is_empty() || digits.len() > 6 {
return Err(bad());
}
let micros: i64 = format!("{digits:0<6}").parse().map_err(|_| bad())?;
(micros, &frac[digits.len()..])
} else {
(0, rest)
};
let offset_seconds = match rest {
"" => return Err(format!("naive datetime (no UTC offset): {s:?}")),
"Z" | "z" => 0,
_ => {
let sign = match rest.as_bytes().first() {
Some(b'+') => 1,
Some(b'-') => -1,
_ => return Err(bad()),
};
let (hh, mm) = rest[1..].split_once(':').ok_or_else(bad)?;
let hh: i64 = hh.parse().map_err(|_| bad())?;
let mm: i64 = mm.parse().map_err(|_| bad())?;
sign * (hh * 3600 + mm * 60)
}
};
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146_097 + doe - 719_468;
let utc_seconds = days * 86_400 + hour * 3600 + minute * 60 + second - offset_seconds;
Ok(utc_seconds * 1_000_000 + frac_micros)
}
#[test]
fn key_vectors_all_pass() {
let doc = vectors();
let key_vectors = doc["key_vectors"].as_array().expect("key_vectors array");
assert_eq!(key_vectors.len(), 44, "expected 44 key vectors");
for vector in key_vectors {
let name = vector["name"].as_str().expect("vector name");
let namespace = vector["namespace"].as_str().expect("namespace");
let operation = vector["operation"].as_str().expect("operation");
let args: Vec<InteropValue> = vector["args"]
.as_array()
.expect("args array")
.iter()
.map(|a| parse_input(a).unwrap_or_else(|e| panic!("[{name}] input parse: {e}")))
.collect();
let encoded =
canonical_args(&args).unwrap_or_else(|e| panic!("[{name}] canonical encode: {e}"));
assert_eq!(
hex::encode(&encoded),
vector["canonical_args_hex"].as_str().expect("hex"),
"[{name}] canonical argument bytes mismatch"
);
let key = interop_key(namespace, operation, &args)
.unwrap_or_else(|e| panic!("[{name}] interop_key: {e}"));
assert_eq!(
key,
vector["expected_key"].as_str().expect("expected_key"),
"[{name}] key mismatch"
);
assert_eq!(
key.rsplit(':').next().expect("key has segments"),
vector["args_hash"].as_str().expect("args_hash"),
"[{name}] args_hash mismatch"
);
}
}
#[test]
fn value_vectors_all_pass() {
let doc = vectors();
let value_vectors = doc["value_vectors"].as_array().expect("value_vectors");
assert_eq!(value_vectors.len(), 6, "expected 6 value vectors");
for vector in value_vectors {
let name = vector["name"].as_str().expect("vector name");
let value =
parse_input(&vector["value"]).unwrap_or_else(|e| panic!("[{name}] input parse: {e}"));
let encoded =
serialize_value(&value).unwrap_or_else(|e| panic!("[{name}] value encode: {e}"));
assert_eq!(
hex::encode(&encoded),
vector["canonical_msgpack_hex"].as_str().expect("hex"),
"[{name}] canonical value bytes mismatch"
);
let decoded = cachekit::interop::deserialize::<Msgpack>(&encoded).unwrap_or_else(|e| {
panic!("[{name}] strict deserialize failed on canonical bytes: {e}")
});
assert!(
decoded.is(&vector["value"]),
"[{name}] read back {decoded:?}"
);
}
}
#[derive(Debug)]
enum Msgpack {
Nil,
Bool(bool),
Int(i128),
Float(f64),
Str(String),
Bin(Vec<u8>),
Array(Vec<Msgpack>),
Map(Vec<(Msgpack, Msgpack)>),
Ext,
}
impl<'de> serde::Deserialize<'de> for Msgpack {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
d.deserialize_any(MsgpackVisitor)
}
}
struct MsgpackVisitor;
impl<'de> serde::de::Visitor<'de> for MsgpackVisitor {
type Value = Msgpack;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("any MessagePack value")
}
fn visit_unit<E>(self) -> Result<Msgpack, E> {
Ok(Msgpack::Nil)
}
fn visit_bool<E>(self, v: bool) -> Result<Msgpack, E> {
Ok(Msgpack::Bool(v))
}
fn visit_i64<E>(self, v: i64) -> Result<Msgpack, E> {
Ok(Msgpack::Int(v.into()))
}
fn visit_u64<E>(self, v: u64) -> Result<Msgpack, E> {
Ok(Msgpack::Int(v.into()))
}
fn visit_f64<E>(self, v: f64) -> Result<Msgpack, E> {
Ok(Msgpack::Float(v))
}
fn visit_str<E>(self, v: &str) -> Result<Msgpack, E> {
Ok(Msgpack::Str(v.to_owned()))
}
fn visit_bytes<E>(self, v: &[u8]) -> Result<Msgpack, E> {
Ok(Msgpack::Bin(v.to_vec()))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Msgpack, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element()? {
items.push(item);
}
Ok(Msgpack::Array(items))
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Msgpack, A::Error> {
let mut entries = Vec::new();
while let Some(entry) = map.next_entry()? {
entries.push(entry);
}
Ok(Msgpack::Map(entries))
}
fn visit_newtype_struct<D: serde::Deserializer<'de>>(self, d: D) -> Result<Msgpack, D::Error> {
<serde::de::IgnoredAny as serde::Deserialize>::deserialize(d)?;
Ok(Msgpack::Ext)
}
}
impl Msgpack {
fn is(&self, expected: &Json) -> bool {
match (self, expected) {
(Msgpack::Nil, Json::Null) => true,
(Msgpack::Bool(a), Json::Bool(b)) => a == b,
(Msgpack::Int(a), Json::Number(n)) => {
n.as_i64().map(i128::from).or(n.as_u64().map(i128::from)) == Some(*a)
}
(Msgpack::Str(a), Json::String(b)) => a == b,
(Msgpack::Array(a), Json::Array(b)) => {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.is(y))
}
(_, Json::Object(obj)) => match (self, obj.iter().next().filter(|_| obj.len() == 1)) {
(Msgpack::Float(a), Some((tag, v))) if tag == "$float" => {
v.as_str().and_then(|s| s.parse::<f64>().ok()) == Some(*a)
}
(Msgpack::Bin(a), Some((tag, v))) if tag == "$bytes" => {
v.as_str().and_then(|s| hex::decode(s).ok()).as_ref() == Some(a)
}
(Msgpack::Map(entries), _) => {
entries.len() == obj.len()
&& obj.iter().all(|(k, v)| {
entries.iter().any(|(key, value)| {
matches!(key, Msgpack::Str(s) if s == k) && value.is(v)
})
})
}
_ => false,
},
_ => false,
}
}
}
#[test]
fn reader_accept_vectors_all_decode() {
let doc = vectors();
let accept = doc["reader_accept_vectors"]
.as_array()
.expect("reader_accept_vectors");
assert_eq!(accept.len(), 6, "expected 6 reader accept vectors");
for vector in accept {
let name = vector["name"].as_str().expect("vector name");
let bytes = hex::decode(vector["input_hex"].as_str().expect("input_hex")).expect("hex");
let decoded = cachekit::interop::deserialize::<Msgpack>(&bytes)
.unwrap_or_else(|e| panic!("[{name}] reader rejected a well-formed document: {e}"));
if let Some(value) = vector.get("value") {
assert!(
decoded.is(value),
"[{name}] decoded {decoded:?}, expected {value}"
);
}
}
}
#[test]
fn reader_reject_vectors_all_reject() {
let doc = vectors();
let reject = doc["reader_reject_vectors"]
.as_array()
.expect("reader_reject_vectors");
assert_eq!(reject.len(), 1, "expected 1 reader reject vector");
for vector in reject {
let name = vector["name"].as_str().expect("vector name");
let bytes = hex::decode(vector["input_hex"].as_str().expect("input_hex")).expect("hex");
let result = cachekit::interop::deserialize::<Msgpack>(&bytes);
assert!(
result.is_err(),
"[{name}] MUST be rejected, but decoded {result:?}"
);
}
}
#[test]
fn error_vectors_all_reject() {
let doc = vectors();
let error_vectors = doc["error_vectors"].as_array().expect("error_vectors");
assert_eq!(error_vectors.len(), 34, "expected 34 error vectors");
for vector in error_vectors {
let name = vector["name"].as_str().expect("vector name");
let namespace = vector["namespace"].as_str().unwrap_or("t");
let operation = vector["operation"].as_str().unwrap_or("op");
let args: Result<Vec<InteropValue>, String> = vector["args"]
.as_array()
.expect("args array")
.iter()
.map(parse_input)
.collect();
match args {
Err(reason) => {
assert!(
["reject_naive_datetime", "reject_naive_datetime_nested"].contains(&name)
&& reason.starts_with("naive datetime"),
"[{name}] unexpected harness-level rejection: {reason}"
);
}
Ok(args) => {
let result = interop_key(namespace, operation, &args);
assert!(
result.is_err(),
"[{name}] MUST be rejected, but produced key: {result:?}"
);
}
}
}
}
#[cfg(feature = "encryption")]
#[test]
fn aad_vectors_all_pass() {
use cachekit::EncryptionLayer;
let doc = vectors();
let aad_vectors = doc["aad_vectors"].as_array().expect("aad_vectors");
assert_eq!(aad_vectors.len(), 1, "expected 1 AAD vector");
for vector in aad_vectors {
let name = vector["name"].as_str().expect("vector name");
let tenant_id = vector["tenant_id"].as_str().expect("tenant_id");
let cache_key = vector["cache_key"].as_str().expect("cache_key");
assert!(!vector["compressed"].as_bool().expect("compressed flag"));
let layer = EncryptionLayer::new(&[0u8; 32], tenant_id).expect("layer");
let aad = layer.build_aad(cache_key, false);
assert_eq!(
hex::encode(&aad),
vector["aad_hex"].as_str().expect("aad_hex"),
"[{name}] AAD bytes mismatch"
);
}
}
#[cfg(feature = "encryption")]
#[test]
fn encryption_vectors_decrypt() {
use cachekit::EncryptionLayer;
let doc = vectors();
let enc_vectors = doc["encryption_vectors"]
.as_array()
.expect("encryption_vectors");
assert_eq!(enc_vectors.len(), 1, "expected 1 encryption vector");
for vector in enc_vectors {
let name = vector["name"].as_str().expect("vector name");
let master_key = hex::decode(vector["master_key_hex"].as_str().expect("master key"))
.expect("master key hex");
let tenant_id = vector["tenant_id"].as_str().expect("tenant_id");
let cache_key = vector["cache_key"].as_str().expect("cache_key");
let ciphertext = hex::decode(vector["ciphertext_hex"].as_str().expect("ciphertext"))
.expect("ciphertext hex");
let expected_plaintext =
hex::decode(vector["plaintext_hex"].as_str().expect("plaintext")).expect("hex");
let layer = EncryptionLayer::new(&master_key, tenant_id).expect("layer");
let plaintext = layer
.decrypt(&ciphertext, cache_key)
.unwrap_or_else(|e| panic!("[{name}] decrypt failed: {e}"));
assert_eq!(plaintext, expected_plaintext, "[{name}] plaintext mismatch");
let decoded: BTreeMap<String, serde_json::Value> =
cachekit::interop::deserialize(&plaintext)
.unwrap_or_else(|e| panic!("[{name}] plaintext is not one msgpack doc: {e}"));
assert_eq!(decoded["name"], serde_json::json!("alice"));
assert_eq!(decoded["age"], serde_json::json!(30));
}
}
#[test]
fn collapse_pair_produces_identical_keys() {
let float_key = interop_key("t", "op", &[InteropValue::from(2.0f64)]).unwrap();
let int_key = interop_key("t", "op", &[InteropValue::from(2i64)]).unwrap();
assert_eq!(float_key, int_key);
}
#[test]
fn non_utc_offset_matches_utc_instant() {
let a = iso8601_to_unix_micros("2024-01-01T18:00:45.123456+05:30").unwrap();
let b = iso8601_to_unix_micros("2024-01-01T12:30:45.123456+00:00").unwrap();
assert_eq!(a, b);
}