use mountos_native::frame::{self, Frame, HEADER_SIZE, MAX_META_BYTES, MAX_PAYLOAD_BYTES};
use mountos_native::Error;
use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use std::io::{self, Cursor, Read};
use std::panic::{catch_unwind, AssertUnwindSafe};
const SEED: u64 = 0x6d66_7331;
const ITERATIONS: usize = 20_000;
thread_local! {
static MAX_ALLOC: Cell<usize> = const { Cell::new(0) };
}
struct Tracking;
unsafe impl GlobalAlloc for Tracking {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
let _ = MAX_ALLOC.try_with(|m| m.set(m.get().max(l.size())));
System.alloc(l)
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
System.dealloc(p, l)
}
unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
let _ = MAX_ALLOC.try_with(|m| m.set(m.get().max(n)));
System.realloc(p, l, n)
}
}
#[global_allocator]
static ALLOC: Tracking = Tracking;
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
(self.next() % n as u64) as usize
}
fn bytes(&mut self, n: usize) -> Vec<u8> {
(0..n).map(|_| self.next() as u8).collect()
}
fn some(&mut self, min: usize, span: usize) -> Vec<u8> {
let n = min + self.below(span);
self.bytes(n)
}
}
fn random_frame(r: &mut Rng) -> Frame {
Frame::new(
r.below(16) as u8,
r.below(65536) as u16,
r.next(),
r.some(0, 64),
r.some(0, 256),
)
}
fn wire(f: &Frame) -> Vec<u8> {
let mut v = Vec::new();
frame::write(&mut v, f).unwrap();
v
}
struct Chunky<'a> {
data: &'a [u8],
rng: Rng,
}
impl Read for Chunky<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.rng.below(8) == 0 {
return Err(io::ErrorKind::Interrupted.into());
}
let n = (1 + self.rng.below(buf.len().max(1)))
.min(self.data.len())
.min(buf.len());
buf[..n].copy_from_slice(&self.data[..n]);
self.data = &self.data[n..];
Ok(n)
}
}
fn drain(rd: &mut impl Read) -> (Vec<Frame>, Option<Error>) {
let mut out = Vec::new();
loop {
match frame::read(rd) {
Ok(Some(f)) => out.push(f),
Ok(None) => return (out, None),
Err(e) => return (out, Some(e)),
}
}
}
fn typed(e: &Error) -> bool {
match e {
Error::Protocol(_) => true,
Error::Io(e) => e.kind() == io::ErrorKind::UnexpectedEof,
_ => false,
}
}
fn check(frames: &[Frame], fed: usize) {
let mut bytes = 0;
for f in frames {
assert!(f.meta.len() <= MAX_META_BYTES as usize && f.payload.len() <= MAX_PAYLOAD_BYTES as usize);
bytes += HEADER_SIZE + f.meta.len() + f.payload.len();
}
assert!(bytes <= fed, "decoded more bytes than were fed");
}
fn run(name: &str, it: usize, data: &[u8], chunky: Option<u64>) -> Option<Error> {
let res = catch_unwind(AssertUnwindSafe(|| match chunky {
Some(seed) => drain(&mut Chunky {
data,
rng: Rng(seed | 1),
}),
None => drain(&mut Cursor::new(data)),
}));
let (frames, err) = res.unwrap_or_else(|_| panic!("{name} iteration {it}: reader panicked on {data:02x?}"));
if let Some(e) = &err {
assert!(typed(e), "{name} iteration {it}: untyped error {e:?}");
}
check(&frames, data.len());
err
}
#[test]
fn random_buffers_fail_only_with_typed_errors() {
let mut r = Rng(SEED);
let mut rejected = 0;
for it in 0..ITERATIONS {
let mut data = r.some(0, 300);
if r.next().is_multiple_of(2) {
let mut head = wire(&random_frame(&mut r));
head.truncate(HEADER_SIZE);
let off = 16 + 4 * r.below(2);
let len = if r.below(3) == 0 {
u32::MAX
} else {
r.below(1 << 25) as u32
};
head[off..off + 4].copy_from_slice(&len.to_be_bytes());
head.extend(data);
data = head;
}
let chunky = (it % 2 == 1).then(|| r.next());
if run("random", it, &data, chunky).is_some() {
rejected += 1;
}
}
assert!(rejected > ITERATIONS / 4, "the generator must reach the error paths");
}
#[test]
fn mutated_valid_frames_fail_only_with_typed_errors() {
let mut r = Rng(SEED + 1);
for it in 0..ITERATIONS {
let n = 1 + r.below(3);
let mut data: Vec<u8> = (0..n).flat_map(|_| wire(&random_frame(&mut r))).collect();
match r.below(4) {
0 => {
for _ in 0..1 + r.below(4) {
let i = r.below(data.len());
data[i] ^= 1 << r.below(8);
}
}
1 => data.truncate(r.below(data.len())),
2 => {
let extra = r.some(1, 40);
data.extend(extra);
}
_ => {
let i = r.below(HEADER_SIZE);
data[i] = r.next() as u8;
}
}
let chunky = (it % 2 == 1).then(|| r.next());
run("mutated", it, &data, chunky);
}
}
#[test]
fn write_then_read_returns_the_same_frames() {
let mut r = Rng(SEED + 2);
for it in 0..ITERATIONS / 4 {
let want: Vec<Frame> = (0..1 + r.below(5)).map(|_| random_frame(&mut r)).collect();
let data: Vec<u8> = want.iter().flat_map(wire).collect();
let chunky = (it % 2 == 1).then(|| r.next());
let (got, err) = match chunky {
Some(s) => drain(&mut Chunky {
data: &data,
rng: Rng(s | 1),
}),
None => drain(&mut Cursor::new(&data)),
};
assert!(err.is_none(), "iteration {it}: {err:?}");
assert_eq!(got, want, "iteration {it}");
}
}
#[test]
fn every_prefix_of_a_frame_ends_with_a_typed_result() {
let f = Frame::new(1, 3, 5, b"abc".to_vec(), b"defgh".to_vec());
let full = wire(&f);
for n in 0..full.len() {
let (frames, err) = drain(&mut Cursor::new(&full[..n]));
assert!(frames.is_empty());
match (n, err) {
(0, None) => {}
(_, Some(Error::Io(e))) if e.kind() == io::ErrorKind::UnexpectedEof => {}
(n, other) => panic!("prefix {n}: {other:?}"),
}
}
}
#[test]
fn a_claim_at_the_limit_then_eof_allocates_one_step() {
let mut head = wire(&Frame::default());
head[20..24].copy_from_slice(&MAX_PAYLOAD_BYTES.to_be_bytes());
head.extend(vec![0u8; 1000]);
MAX_ALLOC.with(|m| m.set(0));
let (_, err) = drain(&mut Cursor::new(&head));
let peak = MAX_ALLOC.with(|m| m.get());
assert!(
matches!(err, Some(Error::Io(ref e)) if e.kind() == io::ErrorKind::UnexpectedEof),
"{err:?}"
);
assert!(
peak <= 2 * (64 << 10),
"largest allocation {peak} bytes for a 1000 byte body"
);
}
#[test]
fn oversize_sections_are_refused_before_any_byte_is_written() {
let mut sink = Vec::new();
let f = Frame::new(0, 0, 0, vec![], vec![0; MAX_PAYLOAD_BYTES as usize + 1]);
assert!(matches!(frame::write(&mut sink, &f), Err(Error::TooLarge(_))));
assert!(sink.is_empty());
}