dure 0.2.18

Detachable Windows console sessions that outlive the terminal
//! Bounded generated byte streams, split points, and terminal read outcomes.

#![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};

/// Independent complete-frame oracle, deliberately without an accumulation loop.
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)
}

/// Runs the real accumulator with bounded reads and an explicit terminal outcome.
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() {
    // A fixed seed makes failures reproducible. The small mutation count and
    // payload bound keep this same target useful under Miri.
    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);
        // Cover every header/kind boundary and representative body truncations,
        // keeping interpretation bounded independently of the seed's body length.
        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);
    }
}