mod common;
use cachekit::interop;
use cachekit::serializer::{self, MAX_DECODE_DEPTH};
use cachekit::CachekitError;
use serde::de::IgnoredAny;
use serde::Deserialize;
use serde_json::Value as Json;
use crate::common::{decode_both, read_every_path};
const VECTORS_JSON: &str = include_str!("vectors/decode-bounds.json");
const VECTORS_SHA256: &str = "c52c27f724fe138e63440dc0306936b48fe389e2bd823c058b86b30d854e2e2e";
fn vectors() -> Json {
serde_json::from_str(VECTORS_JSON).expect("vendored vector file must be valid JSON")
}
fn unhex(s: &str) -> Vec<u8> {
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("vector input_hex must be hex"))
.collect()
}
fn assert_guard_rejected(what: &str, path: &str, result: Result<Json, CachekitError>) {
let e = result
.err()
.unwrap_or_else(|| panic!("{what}: {path} decoded a document it must reject"));
assert!(
matches!(e, CachekitError::Serialization(ref m) if m.starts_with("decode bound:")),
"{what}: {path} must fail in the structural guard, got {e:?}"
);
}
fn on_default_stack<F: FnOnce() + Send + 'static>(f: F) {
std::thread::Builder::new()
.stack_size(2 << 20)
.spawn(f)
.expect("spawn")
.join()
.expect("decode must not panic or overflow the stack");
}
fn nest(level: &[u8], depth: usize) -> Vec<u8> {
let mut v = level.repeat(depth);
v.push(0xc0);
v
}
fn nested_fixarray(depth: usize) -> Vec<u8> {
nest(&[0x91], depth)
}
const LEVELS: [(&str, &[u8]); 6] = [
("fixarray", &[0x91]),
("array16", &[0xdc, 0x00, 0x01]),
("array32", &[0xdd, 0x00, 0x00, 0x00, 0x01]),
("fixmap", &[0x81, 0xa0]),
("map16", &[0xde, 0x00, 0x01, 0xa0]),
("map32", &[0xdf, 0x00, 0x00, 0x00, 0x01, 0xa0]),
];
fn nested_fixarray_empty_leaf(depth: usize) -> Vec<u8> {
let mut v = vec![0x91u8; depth - 1];
v.push(0x90);
v
}
#[test]
fn vector_file_shape_is_the_vendored_one() {
let v = vectors();
assert_eq!(v["version"], "1.2.0");
assert_eq!(v["reject_vectors"].as_array().map(Vec::len), Some(19));
assert_eq!(v["accept_vectors"].as_array().map(Vec::len), Some(3));
assert_eq!(v["spec"], "spec/interop-mode.md#decode-bounds");
}
#[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/decode-bounds.json differs from the pinned protocol copy: \
re-vendor it from protocol and update VECTORS_SHA256"
);
}
#[test]
fn every_reject_vector_is_rejected_by_the_guard_on_every_read_path() {
on_default_stack(|| {
for vector in vectors()["reject_vectors"].as_array().unwrap() {
let name = vector["name"].as_str().unwrap();
let bytes = unhex(vector["input_hex"].as_str().unwrap());
for (path, result) in read_every_path(&bytes) {
assert_guard_rejected(name, path, result);
}
}
});
}
#[test]
fn every_accept_vector_decodes_on_every_read_path() {
on_default_stack(|| {
for vector in vectors()["accept_vectors"].as_array().unwrap() {
let name = vector["name"].as_str().unwrap();
let bytes = unhex(vector["input_hex"].as_str().unwrap());
let depth = vector["nesting_depth"].as_u64().unwrap();
for (path, result) in read_every_path(&bytes) {
let value = result
.unwrap_or_else(|e| panic!("{name}: {path} rejected an accept vector: {e}"));
assert_eq!(
nesting_depth(&value) as u64,
depth,
"{name}: {path} decoded to the wrong shape"
);
}
}
});
}
fn nesting_depth(v: &Json) -> usize {
match v {
Json::Array(items) => 1 + items.iter().map(nesting_depth).max().unwrap_or(0),
Json::Object(map) => 1 + map.values().map(nesting_depth).max().unwrap_or(0),
_ => 0,
}
}
#[test]
fn depth_bound_is_owned_and_matches_the_typescript_sdk() {
assert_eq!(MAX_DECODE_DEPTH, 100);
let at_depth = |depth: usize| {
let mut shapes: Vec<(String, Vec<u8>)> = LEVELS
.iter()
.map(|(shape, level)| (shape.to_string(), nest(level, depth)))
.collect();
shapes.push((
"empty-leaf fixarray".into(),
nested_fixarray_empty_leaf(depth),
));
shapes
};
on_default_stack(move || {
for (shape, bytes) in at_depth(MAX_DECODE_DEPTH) {
for (path, result) in read_every_path(&bytes) {
let value = result
.unwrap_or_else(|e| panic!("{shape}: {path}: depth == bound must decode: {e}"));
assert_eq!(
nesting_depth(&value),
MAX_DECODE_DEPTH,
"{shape}: {path}: decoded to the wrong shape"
);
}
}
for (shape, bytes) in at_depth(MAX_DECODE_DEPTH + 1) {
for (path, result) in read_every_path(&bytes) {
assert_guard_rejected(&format!("{shape} at depth bound+1"), path, result);
}
}
});
}
#[test]
fn ext_leaf_at_the_bound_decodes() {
const FIXEXT1: [u8; 3] = [0xd4, 0x01, 0x02];
let ext_at = |depth: usize| {
let mut v = vec![0x91u8; depth];
v.extend_from_slice(&FIXEXT1);
v
};
on_default_stack(move || {
let bytes = ext_at(MAX_DECODE_DEPTH);
for (path, result) in [
(
"serializer::deserialize",
serializer::deserialize::<IgnoredAny>(&bytes),
),
(
"interop::deserialize",
interop::deserialize::<IgnoredAny>(&bytes),
),
] {
result
.unwrap_or_else(|e| panic!("{path}: ext leaf at depth == bound must decode: {e}"));
}
let bytes = ext_at(MAX_DECODE_DEPTH + 1);
for (path, result) in [
(
"serializer::deserialize",
serializer::deserialize::<IgnoredAny>(&bytes),
),
(
"interop::deserialize",
interop::deserialize::<IgnoredAny>(&bytes),
),
] {
let e = result
.err()
.unwrap_or_else(|| panic!("{path}: ext leaf at depth bound+1 decoded"));
assert!(
matches!(e, CachekitError::Serialization(ref m) if m.starts_with("decode bound:")),
"{path}: must fail in the structural guard, got {e:?}"
);
}
});
}
#[test]
fn closed_collections_release_their_depth() {
let mut wide = vec![0xdc, 0x00, 0xc8];
wide.extend(std::iter::repeat_n(0x90, 200));
let mut siblings = vec![0x92];
siblings.extend(nested_fixarray(MAX_DECODE_DEPTH - 1));
siblings.extend(nested_fixarray_empty_leaf(MAX_DECODE_DEPTH - 1));
on_default_stack(move || {
for (shape, bytes, depth) in [("wide", wide, 2), ("siblings", siblings, MAX_DECODE_DEPTH)] {
for (path, result) in decode_both(&bytes) {
let value = result.unwrap_or_else(|e| panic!("{shape}: {path}: {e}"));
assert_eq!(nesting_depth(&value), depth, "{shape}: {path}");
}
}
});
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Tree {
Leaf(u8),
Node(Vec<Tree>),
}
#[test]
fn vec_target_amplifier_is_rejected_before_any_allocation() {
let mut bomb = Vec::new();
for _ in 0..50 {
bomb.extend_from_slice(&[0xdd, 0xff, 0xff, 0xff, 0xff]);
}
for (path, result) in [
("serializer", serializer::deserialize::<Tree>(&bomb)),
("interop", interop::deserialize::<Tree>(&bomb)),
] {
let msg = result
.err()
.unwrap_or_else(|| panic!("{path}: decoded the amplifier"))
.to_string();
assert!(
msg.contains("more elements than the input can back"),
"{path}: {msg}"
);
}
let legit = [0x92, 0x92, 0x01, 0x02, 0x91, 0x03]; for tree in [
serializer::deserialize::<Tree>(&legit).unwrap(),
interop::deserialize::<Tree>(&legit).unwrap(),
] {
let Tree::Node(children) = tree else {
panic!("root must be a node")
};
let leaves: Vec<Vec<u8>> = children
.iter()
.map(|c| match c {
Tree::Node(n) => n
.iter()
.map(|l| match l {
Tree::Leaf(v) => *v,
Tree::Node(_) => u8::MAX,
})
.collect(),
Tree::Leaf(v) => vec![*v],
})
.collect();
assert_eq!(leaves, vec![vec![1, 2], vec![3]]);
}
}