#![expect(
clippy::arithmetic_side_effects,
reason = "fixture byte and operation counts are explicitly bounded"
)]
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use super::*;
use crate::SessionId;
use crate::constants::MAX_FRAME_LEN;
use crate::durability::LauncherTie;
use crate::pal::error::{PalError, PalErrorKind};
use crate::pal::pseudoconsole::WindowSize;
use crate::protocol::{Message, StartupStep, decode_body, encode};
fn expected(bytes: &[u8], end: PalErrorKind) -> Result<Message, PalErrorKind> {
let Some((prefix, payload)) = bytes.split_first_chunk::<4>() else {
return Err(end);
};
let len = u32::from_le_bytes(*prefix);
if !(1..=MAX_FRAME_LEN).contains(&len) {
return Err(PalErrorKind::Other);
}
let Some(payload) = payload.get(..len as usize) else {
return Err(end);
};
let (&kind, body) = payload.split_first().unwrap();
decode_body(kind, body.to_vec()).map_err(|_error| PalErrorKind::Other)
}
fn check(bytes: &[u8], splits: &[u8], terminal: Option<PalErrorKind>) {
let mut at = 0;
let mut calls = 0;
let actual = read_frame(|buffer| {
calls += 1;
assert!(
calls <= bytes.len() + 1,
"every read must make progress or terminate"
);
if at == bytes.len() {
return terminal.map_or(Ok(0), |kind| Err(PalError::new(kind)));
}
let split = usize::from(*splits.get((calls - 1) % splits.len()).unwrap()) + 1;
let count = split.min(buffer.len()).min(bytes.len() - at);
buffer
.get_mut(..count)
.unwrap()
.copy_from_slice(bytes.get(at..at + count).unwrap());
at += count;
Ok(count)
})
.map_err(|error| error.kind());
assert_eq!(
actual,
expected(bytes, terminal.unwrap_or(PalErrorKind::Disconnected))
);
if actual.is_ok() {
let len = u32::from_le_bytes(*bytes.first_chunk::<4>().unwrap()) as usize;
assert_eq!(at, len + 4, "the next frame must remain unread");
}
}
#[test]
fn generated_segmented_frames() {
let mut rng = StdRng::seed_from_u64(0xD0_7E);
let size = WindowSize::new(91, 37).unwrap();
let seeds = [
Message::Attach { size },
Message::Input(b"input".to_vec()),
Message::Resize { size },
Message::Attached {
session_id: SessionId::MIN,
},
Message::Output(b"output".to_vec()),
Message::Displaced,
Message::AppExited { status: -7 },
Message::StartupOk {
session_id: SessionId::MIN,
launcher_tie: LauncherTie::Unknown,
pipe_name: "pipe".to_string(),
},
Message::StartupErr {
step: StartupStep::App,
},
Message::StartupCommit,
];
for message in seeds {
let frame = encode(&message);
let body_start = size_of::<u32>() + size_of::<u8>();
let body_middle = body_start.midpoint(frame.len());
let cuts = (0..=body_start).chain([body_middle, frame.len() - 1, frame.len()]);
for cut in cuts {
let splits: [u8; 8] = rng.random();
for terminal in [
None,
Some(PalErrorKind::Disconnected),
Some(PalErrorKind::Timeout),
] {
check(frame.get(..cut).unwrap(), &splits, terminal);
}
}
for _ in 0..8 {
let mut bytes = frame.clone();
let mutate_at = rng.random_range(4..bytes.len());
*bytes.get_mut(mutate_at).unwrap() = rng.random();
let declared = match rng.random_range(0..4) {
0 => 0,
1 => MAX_FRAME_LEN + 1,
_ => rng.random_range(1..=64),
};
bytes
.get_mut(..4)
.unwrap()
.copy_from_slice(&declared.to_le_bytes());
let splits: [u8; 8] = rng.random();
check(&bytes, &splits, Some(PalErrorKind::Other));
}
let mut adjacent = frame.clone();
adjacent.extend_from_slice(&frame);
check(&adjacent, &[0], None);
}
}
#[test]
fn rejects_reader_overreporting_without_slicing_out_of_bounds() {
let error = read_frame(|buffer| Ok(buffer.len() + 1)).unwrap_err();
assert_eq!(error.kind(), PalErrorKind::Other);
}
#[test]
fn rejects_nonprogress_in_each_frame_component() {
let frame = encode(&Message::Input(b"x".to_vec()));
for cut in [0, 4, 5] {
check(frame.get(..cut).unwrap(), &[0], None);
}
}