panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
use std::io::Write;

use crate::ansi;
use crate::terminal::TerminalState;

/// Fuzz the parser with random or malformed byte sequences.
pub fn fuzz_bytes(data: &[u8], cols: u16, rows: u16) -> Result<String, String> {
    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let mut terminal = TerminalState::new(cols as usize, rows as usize, 100);
        let mut parser = ansi::Parser::new();
        parser.advance(data, &mut terminal);
        format!(
            "ok: grid={}x{} cursor=({},{}) scrollback={}",
            terminal.grid.cols(),
            terminal.grid.rows(),
            terminal.grid.cursor_row(),
            terminal.grid.cursor_col(),
            terminal.scrollback.len()
        )
    }));

    match result {
        Ok(msg) => Ok(msg),
        Err(e) => {
            let msg = if let Some(s) = e.downcast_ref::<&str>() {
                s.to_string()
            } else if let Some(s) = e.downcast_ref::<String>() {
                s.clone()
            } else {
                "unknown panic".to_string()
            };
            Err(msg)
        }
    }
}

/// Generate a random ANSI byte sequence for fuzzing.
pub fn generate_random_fuzz(rng: &mut impl FnMut() -> u8) -> Vec<u8> {
    let len = (rng() as usize % 4096) + 1;
    let mut data = Vec::with_capacity(len);
    for _ in 0..len {
        data.push(rng());
    }
    data
}

/// Generate structured malformed ANSI sequences for targeted fuzzing.
pub fn generate_structured_fuzz() -> Vec<u8> {
    use fastrand::Rng;
    let mut rng = Rng::new();
    let kind = rng.u8(0..12);
    let mut data = Vec::new();
    match kind {
        0 => {
            // Malformed UTF-8: incomplete sequences
            data.extend_from_slice(b"\xE2\x82"); // incomplete 3-byte sequence
            data.push(rng.u8(0..255));
        }
        1 => {
            // Malformed UTF-8: overlong encoding
            data.extend_from_slice(b"\xF0\x82\x82\xAC"); // overlong euro sign
        }
        2 => {
            // Broken CSI: missing final byte
            data.push(0x1B);
            data.push(b'[');
            for _ in 0..rng.usize(0..8) {
                data.push(rng.u8(0x30..0x3F)); // parameter bytes
            }
            // No final byte
        }
        3 => {
            // Broken OSC: incomplete
            data.push(0x1B);
            data.push(b']');
            for _ in 0..rng.usize(0..32) {
                data.push(rng.u8(0x20..0x7E));
            }
            // No BEL or ST
        }
        4 => {
            // Oversized CSI parameters
            data.extend_from_slice(b"\x1B[");
            for _ in 0..rng.usize(1..10) {
                let p = rng.u32(0..999_999_999);
                write!(data, "{}", p).ok();
                if rng.bool() {
                    data.push(b';');
                }
            }
            data.push(b'm');
        }
        5 => {
            // Oversized SGR with extreme values
            data.extend_from_slice(b"\x1B[");
            for _ in 0..rng.usize(1..20) {
                let p = match rng.u8(0..6) {
                    0 => rng.u16(0..256),
                    1 => rng.u16(256..65535),
                    2 => rng.u16(0..10),
                    _ => 38, // extended color
                };
                write!(data, "{}", p).ok();
                data.push(b';');
                if p == 38 || p == 48 {
                    let mode = rng.u8(0..3);
                    match mode {
                        0 => {
                            write!(data, "5;{}", rng.u8(0..255)).ok();
                        }
                        1 => {
                            write!(
                                data,
                                "2;{};{};{}",
                                rng.u8(0..255),
                                rng.u8(0..255),
                                rng.u8(0..255)
                            )
                            .ok();
                        }
                        _ => {
                            write!(data, "{};{}", rng.u8(0..255), rng.u8(0..255)).ok();
                        }
                    }
                    if rng.bool() {
                        data.push(b';');
                    }
                }
            }
            data.push(b'm');
        }
        6 => {
            // Strobed ESC sequences
            for _ in 0..rng.usize(1..20) {
                data.push(0x1B);
                data.push(rng.u8(0x20..0x7F));
            }
        }
        7 => {
            // Unicode edge cases
            let cases = [
                '\0',       // null
                '\u{FFFE}', // non-character
                '\u{FFFF}', // non-character
                '\u{202E}', // RIGHT-TO-LEFT OVERRIDE
                '\u{200B}', // zero-width space
                '\u{FEFF}', // BOM
                '\u{FFFD}', // replacement character
                '\u{00AD}', // soft hyphen
            ];
            for c in cases {
                let mut buf = [0u8; 4];
                let s = c.encode_utf8(&mut buf);
                data.extend_from_slice(s.as_bytes());
            }
        }
        8 => {
            // Invalid wide character states: combining marks on their own
            let combinings = [
                '\u{0300}', '\u{0301}', '\u{0302}', '\u{0308}', '\u{0323}', '\u{0345}', '\u{094D}',
            ];
            for c in combinings {
                let mut buf = [0u8; 4];
                let s = c.encode_utf8(&mut buf);
                data.extend_from_slice(s.as_bytes());
            }
        }
        9 => {
            // DEC private sequences with random params
            data.extend_from_slice(b"\x1B[?");
            let p = rng.u16(1..9999);
            write!(data, "{}", p).ok();
            data.push(if rng.bool() { b'h' } else { b'l' });
        }
        10 => {
            // Random interleaved control characters
            for _ in 0..rng.usize(1..50) {
                if rng.bool() {
                    data.push(rng.u8(0x01..0x1F)); // raw control
                } else {
                    data.push(rng.u8(0x20..0x7F)); // printable ASCII
                }
            }
        }
        11 => {
            // Mixed valid + invalid sequences
            data.extend_from_slice(b"\x1B[31mHello\x1B[0m\n");
            data.extend_from_slice(b"\x1B[");
            data.push(rng.u8(0x00..0xFF));
            data.push(rng.u8(0x00..0xFF));
            data.extend_from_slice(b"\x1B[38;2;255;garbage\x1B[0m");
        }
        _ => {}
    }
    // Add some random garbage at the end
    for _ in 0..rng.usize(0..64) {
        data.push(rng.u8(0..255));
    }
    data
}

/// Run fuzz iterations and report results.
pub fn fuzz_iterations(n: usize, cols: u16, rows: u16) -> FuzzReport {
    let mut report = FuzzReport::default();
    let mut rng = || fastrand::u8(0..=255);
    for i in 0..n {
        let data = if i % 4 == 0 {
            generate_structured_fuzz()
        } else {
            generate_random_fuzz(&mut rng)
        };
        match fuzz_bytes(&data, cols, rows) {
            Ok(_) => report.passes += 1,
            Err(msg) => {
                report.crashes += 1;
                report.last_crash = Some((data, msg));
                if report.crashes >= 5 {
                    break;
                }
            }
        }
    }
    report
}

/// Run targeted fuzzing against specific categories.
pub fn fuzz_ansi_categories(cols: u16, rows: u16) -> Vec<FuzzCategoryReport> {
    let mut reports = Vec::new();

    // Each category is (name, data_generator, iterations)
    let categories: Vec<(Vec<u8>, usize)> = vec![
        (vec![0xE2, 0x82, 0xFF], 100), // malformed_utf8
        (vec![0x1B, b'[', 0x30], 100), // broken_csi (truncated)
        (
            {
                let mut d = b"\x1B[".to_vec();
                for _ in 0..20 {
                    d.extend_from_slice(b"999999999;");
                }
                d.push(b'm');
                d
            },
            100,
        ), // oversized_params
        (vec![0x1B, b']', 0x48, 0x65], 100), // incomplete_osc
        (
            {
                let mut d = Vec::with_capacity(256);
                for i in 0..=255u16 {
                    d.push((i & 0xFF) as u8);
                }
                d
            },
            100,
        ), // all byte values
        (
            {
                let mut d = Vec::new();
                for &c in &[
                    '\0', '\u{FFFE}', '\u{FFFF}', '\u{202E}', '\u{200B}', '\u{FEFF}', '\u{FFFD}',
                ] {
                    let mut buf = [0u8; 4];
                    let s = c.encode_utf8(&mut buf);
                    d.extend_from_slice(s.as_bytes());
                }
                d
            },
            100,
        ), // unicode_edge
    ];

    let category_names = [
        "malformed_utf8",
        "broken_csi",
        "oversized_params",
        "incomplete_osc",
        "all_byte_values",
        "unicode_edge",
    ];

    for (i, (data, count)) in categories.iter().enumerate() {
        let mut passes = 0;
        let mut crashes = 0;
        let mut last = None;
        for _ in 0..*count {
            match fuzz_bytes(data, cols, rows) {
                Ok(_) => passes += 1,
                Err(msg) => {
                    crashes += 1;
                    last = Some((data.clone(), msg));
                }
            }
        }
        reports.push(FuzzCategoryReport {
            category: category_names[i].to_string(),
            passes,
            crashes,
            last_crash: last,
        });
    }
    reports
}

#[derive(Debug, Clone)]
pub struct FuzzCategoryReport {
    pub category: String,
    pub passes: usize,
    pub crashes: usize,
    pub last_crash: Option<(Vec<u8>, String)>,
}

#[derive(Debug, Default)]
pub struct FuzzReport {
    pub passes: usize,
    pub crashes: usize,
    pub last_crash: Option<(Vec<u8>, String)>,
}

pub fn print_fuzz_report(r: &FuzzReport) {
    println!("=== Fuzz Report ===");
    println!("  Passes:  {}", r.passes);
    println!("  Crashes: {}", r.crashes);
    if let Some((data, msg)) = &r.last_crash {
        println!("  Last crash: {}", msg);
        let hex: String = data
            .iter()
            .take(64)
            .map(|b| format!("{:02x}", b))
            .collect::<Vec<_>>()
            .join(" ");
        println!("  Bytes (hex): {}...", hex);
    }
}

pub fn print_category_reports(reports: &[FuzzCategoryReport]) {
    println!("=== Fuzz Category Report ===");
    for r in reports {
        let status = if r.crashes == 0 { "" } else { "" };
        println!(
            "  {} {}: {} passes, {} crashes",
            status, r.category, r.passes, r.crashes
        );
        if let Some((data, msg)) = &r.last_crash {
            println!("    Last crash: {}", msg);
            let hex: String = data
                .iter()
                .take(32)
                .map(|b| format!("{:02x}", b))
                .collect::<Vec<_>>()
                .join(" ");
            println!("    Bytes (hex): {}...", hex);
        }
    }
}