#![forbid(unsafe_code)]
use proptest::prelude::*;
use crate::core::limits::Limits;
use crate::format::descriptor;
use crate::tests::hostile_media::corpus::{descriptor_seeds, exhibits};
use crate::tests::hostile_media::{ExhibitKind, Expect, LIMIT_SETS, tight_limits};
const MAX_FUZZ_INPUT: usize = 1024;
pub fn run_descriptor_oracle(bytes: &[u8], limits: &Limits) -> Result<(), String> {
let decoded = descriptor::decode(bytes, limits);
match decoded {
Err(_e) => Ok(()), Ok(rep) => {
rep.validate(limits).map_err(|e| {
format!(
"decode-ok but validate failed: {e:?} (input {} bytes, {})",
bytes.len(),
hex_tail(bytes)
)
})?;
if rep.encoded_size() > limits.max_descriptor_bytes {
return Err(format!(
"decode-ok but encoded_size {} exceeds the {} descriptor cap",
rep.encoded_size(),
limits.max_descriptor_bytes
));
}
if rep.len() > limits.max_chunk_size {
return Err(format!(
"decode-ok but len {} exceeds the {} chunk cap",
rep.len(),
limits.max_chunk_size
));
}
let re = descriptor::encode(&rep).map_err(|e| format!("re-encode failed: {e:?}"))?;
if re != bytes {
return Err(format!(
"decode is not canonical: re-encode ({} bytes) differs from the input ({} bytes)",
re.len(),
bytes.len()
));
}
Ok(())
}
}
}
fn hex_tail(bytes: &[u8]) -> String {
let n = bytes.len().min(16);
let mut s = String::with_capacity(n * 2);
for b in &bytes[..n] {
s.push_str(&format!("{b:02x}"));
}
if bytes.len() > n {
s.push('…');
}
s
}
pub fn limits_for(set: &str) -> Limits {
match set {
"tight" => tight_limits(),
_ => Limits::default(),
}
}
#[test]
fn seeds_are_canonical_and_valid() {
let limits = Limits::default();
let seeds = descriptor_seeds();
assert!(
seeds.len() >= 20,
"one descriptor of every family + every residual kind (got {})",
seeds.len()
);
for (name, bytes) in &seeds {
run_descriptor_oracle(bytes, &limits).unwrap_or_else(|e| panic!("seed {name}: {e}"));
}
}
#[test]
fn seeds_bounded_under_tight_limits() {
let limits = tight_limits();
for (name, bytes) in descriptor_seeds() {
let r = descriptor::decode(&bytes, &limits);
if let Ok(rep) = r {
rep.validate(&limits)
.unwrap_or_else(|e| panic!("seed {name}: tight validate failed: {e:?}"));
let re = descriptor::encode(&rep).expect("re-encode");
assert_eq!(re, bytes, "seed {name}: not canonical under tight limits");
}
}
}
#[test]
fn truncation_at_every_boundary_of_every_seed() {
let limits = Limits::default();
let seeds = descriptor_seeds();
let mut checked = 0usize;
for (name, bytes) in &seeds {
for cut in 0..bytes.len() {
assert!(
descriptor::decode(&bytes[..cut], &limits).is_err(),
"seed {name}: cut at {cut} of {} decoded successfully",
bytes.len()
);
checked += 1;
}
}
assert!(checked >= 20 * 5, "truncation sweep too small: {checked}");
}
#[test]
fn descriptor_cap_boundary() {
let limits = Limits::default();
let mut ok = vec![0x07u8, 0x00, 0x20, 0x00, 0x00];
ok.extend_from_slice(&8167u32.to_le_bytes());
ok.extend_from_slice(&0u128.to_le_bytes());
ok.extend_from_slice(&vec![0xABu8; 8167]);
assert_eq!(ok.len(), 8192);
run_descriptor_oracle(&ok, &limits)
.unwrap_or_else(|e| panic!("8192-byte descriptor must decode: {e}"));
let mut over = ok.clone();
over.push(0x00);
assert_eq!(over.len(), 8193);
assert!(
descriptor::decode(&over, &limits).is_err(),
"8193-byte descriptor must be rejected"
);
}
#[test]
fn descriptor_exhibits_pass() {
for set in LIMIT_SETS {
let limits = limits_for(set);
for ex in exhibits()
.into_iter()
.filter(|e| e.kind == ExhibitKind::Descriptor)
{
let outcome = run_descriptor_oracle(&ex.bytes, &limits);
match ex.expect {
Expect::MustReject => {
assert!(
descriptor::decode(&ex.bytes, &limits).is_err(),
"[{set}] exhibit {} must be rejected",
ex.name
);
}
Expect::MustAccept => {
outcome.unwrap_or_else(|e| panic!("[{set}] exhibit {}: {e}", ex.name));
}
Expect::Either => {
let _ = outcome; }
}
}
}
}
#[test]
fn exhibits_never_panic() {
for ex in exhibits() {
if ex.kind != ExhibitKind::Descriptor {
continue;
}
let _ = descriptor::decode(&ex.bytes, &Limits::default());
let _ = descriptor::decode(&ex.bytes, &tight_limits());
}
}
fn apply_op(bytes: &mut Vec<u8>, op: u8, a: usize, b: u8) {
match op % 6 {
0 => {
if !bytes.is_empty() {
let i = a % bytes.len();
bytes[i] ^= b | 1;
}
}
1 => {
if !bytes.is_empty() {
let i = a % bytes.len();
bytes[i] = b;
}
}
2 => {
let i = if bytes.is_empty() {
0
} else {
a % (bytes.len() + 1)
};
bytes.insert(i, b);
}
3 => {
if !bytes.is_empty() {
let i = a % bytes.len();
bytes.remove(i);
}
}
4 => {
if !bytes.is_empty() {
let i = a % bytes.len();
bytes.truncate(i);
}
}
_ => {
if !bytes.is_empty() {
let start = a % bytes.len();
let mut rng = a as u64 ^ (b as u64) << 8;
for k in start..bytes.len().min(start + 8) {
rng = rng
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
bytes[k] ^= (rng >> 32) as u8 | 1;
}
}
}
}
}
fn mutated_seed_strategy() -> impl Strategy<Value = Vec<u8>> {
let seeds = descriptor_seeds();
let seed_bytes: Vec<Vec<u8>> = seeds.iter().map(|(_, b)| b.clone()).collect();
prop::sample::select(seed_bytes).prop_flat_map(|seed| {
(
prop::collection::vec(any::<(u8, u8, u8)>(), 0..=8),
proptest::bool::ANY,
)
.prop_map(move |(ops, append_garbage)| {
let mut bytes = seed.clone();
for (op, a, b) in ops {
apply_op(&mut bytes, op, a as usize, b);
}
if append_garbage && !bytes.is_empty() {
let i = (bytes.len() / 2).min(bytes.len() - 1);
bytes.splice(i..i, vec![0xAA, 0x55, 0xFF, 0x00, 0x42]);
}
bytes
})
})
}
fn noise_strategy() -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(any::<u8>(), 0..=MAX_FUZZ_INPUT)
}
fn trailing_garbage_strategy() -> impl Strategy<Value = Vec<u8>> {
let seeds = descriptor_seeds();
let seed_bytes: Vec<Vec<u8>> = seeds.iter().map(|(_, b)| b.clone()).collect();
(
prop::sample::select(seed_bytes),
prop::collection::vec(any::<u8>(), 1..=16),
)
.prop_map(|(mut seed, garbage)| {
seed.extend_from_slice(&garbage);
seed
})
}
fn slice_strategy() -> impl Strategy<Value = Vec<u8>> {
let seeds = descriptor_seeds();
let seed_bytes: Vec<Vec<u8>> = seeds.iter().map(|(_, b)| b.clone()).collect();
prop::sample::select(seed_bytes).prop_flat_map(|seed| {
(0usize..seed.len(), 0usize..seed.len())
.prop_map(move |(a, b)| seed[a.min(b)..a.max(b)].to_vec())
})
}
proptest! {
#[test]
fn uniform_noise_oracle(bytes in noise_strategy()) {
for set in LIMIT_SETS {
let limits = limits_for(set);
run_descriptor_oracle(&bytes, &limits)
.unwrap_or_else(|e| panic!("[{set}] noise {} bytes: {e}", bytes.len()));
}
}
#[test]
fn mutated_seeds_oracle(bytes in mutated_seed_strategy()) {
for set in LIMIT_SETS {
let limits = limits_for(set);
run_descriptor_oracle(&bytes, &limits)
.unwrap_or_else(|e| panic!("[{set}] mutated {} bytes: {e}", bytes.len()));
}
}
#[test]
fn trailing_garbage_oracle(bytes in trailing_garbage_strategy()) {
for set in LIMIT_SETS {
let limits = limits_for(set);
assert!(
descriptor::decode(&bytes, &limits).is_err(),
"[{set}] trailing garbage decoded: {} bytes",
bytes.len()
);
}
}
#[test]
fn slice_oracle(bytes in slice_strategy()) {
for set in LIMIT_SETS {
let limits = limits_for(set);
run_descriptor_oracle(&bytes, &limits)
.unwrap_or_else(|e| panic!("[{set}] slice {} bytes: {e}", bytes.len()));
}
}
}