use std::panic::{catch_unwind, AssertUnwindSafe};
use verit::{
dump_json, encode, Dt, Message, Ref, Resolver, Schema, SchemaBuilder, SchemaMode, StructReader,
Value,
};
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn byte(&mut self) -> u8 {
(self.next() & 0xff) as u8
}
}
fn corpus() -> Vec<(Schema, Vec<u8>)> {
let mut out = Vec::new();
let s1 = SchemaBuilder::new()
.add_enum("Color", vec![(0, "Red"), (1, "Green")])
.add_dense_struct("Point", vec![(1, "x", Dt::F64), (2, "y", Dt::F64)])
.add_struct(
"Rec",
vec![
(1, "name", Dt::Str),
(2, "n", Dt::U64),
(3, "blob", Dt::Bytes),
(4, "color", Dt::named("Color")),
(5, "at", Dt::named("Point")),
(6, "tags", Dt::list(Dt::Str)),
(7, "path", Dt::list(Dt::named("Point"))),
(8, "grid", Dt::list(Dt::list(Dt::U16))),
],
)
.build("Rec")
.unwrap();
let v1 = Value::Struct(vec![
(1, Value::str("hello")),
(2, Value::U64(123456)),
(3, Value::Bytes(vec![1, 2, 3, 4, 5])),
(4, Value::Enum(1)),
(
5,
Value::Struct(vec![(1, Value::F64(1.5)), (2, Value::F64(2.5))]),
),
(6, Value::List(vec![Value::str("a"), Value::str("bb")])),
(
7,
Value::List(vec![Value::Struct(vec![
(1, Value::F64(0.0)),
(2, Value::F64(1.0)),
])]),
),
(
8,
Value::List(vec![Value::List(vec![Value::U16(7), Value::U16(8)])]),
),
]);
let s2 = SchemaBuilder::new()
.add_packed_struct(
"Wide",
vec![
(1, "a", Dt::U8),
(2, "b", Dt::U64),
(3, "c", Dt::Str),
(4, "d", Dt::U32),
(5, "e", Dt::Bool),
(6, "f", Dt::F64),
],
)
.build("Wide")
.unwrap();
let v2 = Value::Struct(vec![
(2, Value::U64(9)),
(3, Value::str("packed")),
(6, Value::F64(3.25)),
]);
for (s, v) in [(&s1, &v1), (&s2, &v2)] {
for mode in [SchemaMode::Inline, SchemaMode::HashOnly] {
out.push((s.clone(), encode(s, v, mode).unwrap()));
}
}
out
}
const MAX_DEPTH: u32 = 40;
fn walk(r: &StructReader, depth: u32, budget: &mut u32) {
if depth > MAX_DEPTH || *budget == 0 {
return;
}
let ids: Vec<u16> = r.struct_def().fields.iter().map(|f| f.id).collect();
for id in ids {
if *budget == 0 {
return;
}
*budget -= 1;
if let Ok(Some(v)) = r.get(id) {
walk_ref(&v, depth, budget);
}
}
}
fn walk_ref(v: &Ref, depth: u32, budget: &mut u32) {
match v {
Ref::Struct(s) => walk(s, depth + 1, budget),
Ref::List(l) => {
let _ = l.as_u8_slice();
let _ = l.to_vec_u8();
let _ = l.to_vec_u16();
let _ = l.to_vec_u32();
let _ = l.to_vec_u64();
let _ = l.to_vec_i8();
let _ = l.to_vec_i16();
let _ = l.to_vec_i32();
let _ = l.to_vec_i64();
let _ = l.to_vec_f32();
let _ = l.to_vec_f64();
let mut scratch = [0f32; 16];
let _ = l.copy_f32(&mut scratch);
let n = l.len().min(128);
for i in 0..n {
if *budget == 0 {
return;
}
*budget -= 1;
if let Ok(e) = l.get(i) {
walk_ref(&e, depth + 1, budget);
}
}
}
_ => {}
}
}
fn exercise(buf: &[u8], schema: Option<&Schema>) {
let msg = match Message::parse(buf) {
Ok(m) => m,
Err(_) => return,
};
let _ = msg.schema_id();
let _ = msg.root_offset();
let _ = msg.has_inline_schema();
let _ = msg.writer_schema();
let _ = dump_json(buf);
if let Some(schema) = schema {
if let Ok(resolver) = Resolver::identity(schema) {
if let Ok(root) = msg.root(&resolver) {
let mut budget = 200_000u32;
walk(&root, 0, &mut budget);
}
}
}
if let Ok(Some(inline)) = msg.writer_schema() {
if let Ok(resolver) = Resolver::identity(&inline) {
if let Ok(root) = msg.root(&resolver) {
let mut budget = 200_000u32;
walk(&root, 0, &mut budget);
}
}
}
if let Some(schema) = schema {
if let Ok(resolver) = Resolver::identity(schema) {
for limit in [msg.suggested_budget(), 4] {
let b = verit::Budget::new(limit);
let _ = msg.verify(&resolver, &b);
if let Ok(root) = msg.root_bounded(&resolver, &b) {
let mut w = 200_000u32;
walk(&root, 0, &mut w);
}
}
}
}
}
fn run_guarded(input: &[u8], schema: Option<&Schema>) -> bool {
catch_unwind(AssertUnwindSafe(|| exercise(input, schema))).is_ok()
}
#[test]
fn no_panic_on_mutated_messages() {
let corpus = corpus();
let mut rng = Rng(0x9E3779B97F4A7C15);
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let mut failure: Option<(Vec<u8>, usize)> = None;
for iter in 0..40_000 {
let (schema, base) = &corpus[rng.below(corpus.len())];
let mut bytes = base.clone();
let muts = 1 + rng.below(6);
for _ in 0..muts {
if bytes.is_empty() {
break;
}
let i = rng.below(bytes.len());
bytes[i] = rng.byte();
}
match rng.below(8) {
0 if !bytes.is_empty() => bytes.truncate(rng.below(bytes.len())),
1 => {
let (n, b) = (rng.below(32), rng.byte());
for _ in 0..n {
bytes.push(b);
}
}
_ => {}
}
if !run_guarded(&bytes, Some(schema)) {
failure = Some((bytes, iter));
break;
}
}
std::panic::set_hook(hook);
if let Some((bytes, iter)) = failure {
panic!("panic on mutated input at iter {iter}: {bytes:02x?}");
}
}
#[test]
fn no_panic_on_random_bytes() {
let mut rng = Rng(0xD1B54A32D192ED03);
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let mut failure: Option<Vec<u8>> = None;
for _ in 0..40_000 {
let len = rng.below(96);
let bytes: Vec<u8> = (0..len).map(|_| rng.byte()).collect();
let mut bytes = bytes;
if bytes.len() >= 4 && rng.below(2) == 0 {
bytes[0..4].copy_from_slice(b"VRT2");
}
let ok_schema = catch_unwind(AssertUnwindSafe(|| {
let _ = Schema::from_canonical(&bytes);
}))
.is_ok();
let ok_container = catch_unwind(AssertUnwindSafe(|| {
let mut b = bytes.clone();
if b.len() >= 4 && rng.below(2) == 0 {
b[0..4].copy_from_slice(b"VRTC");
}
#[allow(deprecated)]
if let Ok(c) = verit::Container::parse(&b) {
for i in 0..c.len() {
let _ = c.get(i);
}
}
}))
.is_ok();
if !ok_schema || !ok_container || !run_guarded(&bytes, None) {
failure = Some(bytes);
break;
}
}
std::panic::set_hook(hook);
if let Some(bytes) = failure {
panic!("panic on random input: {bytes:02x?}");
}
}
#[test]
fn offset_cycle_is_bounded_not_infinite() {
let schema = SchemaBuilder::new()
.add_struct(
"Node",
vec![(1, "next", Dt::named("Node")), (2, "v", Dt::U8)],
)
.build("Node")
.unwrap();
let bytes = encode(
&schema,
&Value::Struct(vec![
(
1,
Value::Struct(vec![(2, Value::U8(1))]), ),
(2, Value::U8(9)),
]),
SchemaMode::Inline,
)
.unwrap();
assert!(dump_json(&bytes).is_ok());
let root_off = u32::from_le_bytes(bytes[24..28].try_into().unwrap());
for slot in (root_off as usize..bytes.len().saturating_sub(4)).step_by(4) {
let mut m = bytes.clone();
m[slot..slot + 4].copy_from_slice(&root_off.to_le_bytes());
let _ = dump_json(&m);
}
}
#[test]
fn hostile_nested_schema_rejected() {
let mut s = Vec::new();
s.extend_from_slice(b"VSC1");
s.extend_from_slice(&1u16.to_le_bytes()); s.push(0); s.extend_from_slice(&1u16.to_le_bytes()); s.push(b'S'); s.extend_from_slice(&1u16.to_le_bytes()); s.extend_from_slice(&1u16.to_le_bytes()); s.extend_from_slice(&1u16.to_le_bytes()); s.push(b'f'); s.resize(s.len() + 5000, 0x22);
s.push(0x02); s.extend_from_slice(&0u16.to_le_bytes()); assert!(Schema::from_canonical(&s).is_err());
}
#[test]
fn deep_struct_reference_chain_is_bounded_not_overflow() {
let n = 40_000usize;
let mut b = SchemaBuilder::new();
for i in 0..n {
let name = format!("S{i}");
let field = if i + 1 < n {
(1u16, "f", Dt::named(&format!("S{}", i + 1)))
} else {
(1u16, "f", Dt::U32)
};
b = b.add_struct(&name, vec![field]);
}
let schema = b.build("S0").expect("chain schema builds");
assert!(matches!(
Resolver::identity(&schema),
Err(verit::Error::DepthLimitExceeded)
));
let bytes =
encode(&schema, &Value::Struct(vec![]), SchemaMode::Inline).expect("empty root encodes");
assert!(
bytes.len() < 1_000_000,
"chain message stays small on the wire"
);
assert!(
dump_json(&bytes).is_err(),
"dump_json must reject a deep-chain schema, not overflow"
);
let mut ok = SchemaBuilder::new();
let deepish = 30usize;
for i in 0..deepish {
let field = if i + 1 < deepish {
(1u16, "f", Dt::named(&format!("T{}", i + 1)))
} else {
(1u16, "f", Dt::U32)
};
ok = ok.add_struct(&format!("T{i}"), vec![field]);
}
let ok_schema = ok.build("T0").expect("deepish chain builds");
assert!(
Resolver::identity(&ok_schema).is_ok(),
"a 30-deep struct chain (well under the limit) must still resolve"
);
}
#[test]
fn empty_defaults_section_rejected_as_noncanonical() {
let schema = SchemaBuilder::new()
.add_struct("S", vec![(1, "a", Dt::U32)])
.build("S")
.unwrap();
let ok = Schema::from_canonical(schema.canonical_bytes()).unwrap();
assert_eq!(ok.canonical_bytes(), schema.canonical_bytes());
let mut noncanonical = schema.canonical_bytes().to_vec();
noncanonical.extend_from_slice(&0u16.to_le_bytes());
assert!(Schema::from_canonical(&noncanonical).is_err());
}
#[test]
fn noncanonical_scalar_default_bits_rejected() {
let schema = SchemaBuilder::new()
.add_struct("R", vec![(1, "b", Dt::Bool)])
.set_default("R", 1, Value::Bool(true))
.build("R")
.unwrap();
let ok = Schema::from_canonical(schema.canonical_bytes()).unwrap();
assert_eq!(ok.canonical_bytes(), schema.canonical_bytes());
let mut noncanonical = schema.canonical_bytes().to_vec();
let last = noncanonical.len() - 1;
assert_eq!(noncanonical[last], 0x01);
noncanonical[last] = 0x02;
assert!(Schema::from_canonical(&noncanonical).is_err());
}