//! Tests for [`super`]. use super::*; /// Recording Perform: every callback appends a stringly summary so tests /// can assert on the whole event log. #[derive(Default)] struct Rec(Vec); impl Perform for Rec { fn print(&mut self, c: char) { self.0.push(format!("print({c:?})")); } fn execute(&mut self, byte: u8) { self.0.push(format!("exec({byte:#04x})")); } fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { let p: Vec> = params.iter().map(<[u16]>::to_vec).collect(); self.0.push(format!( "csi(params={p:?}, intermediates={intermediates:?}, ignore={ignore}, action={action:?})" )); } fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) { self.0.push(format!( "esc(intermediates={intermediates:?}, ignore={ignore}, byte={byte:#04x})" )); } fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) { let p: Vec> = params.iter().map(|s| s.to_vec()).collect(); self.0 .push(format!("osc(params={p:?}, bell={bell_terminated})")); } fn apc_dispatch(&mut self, data: &[u8]) { self.0.push(format!("apc({data:?})")); } fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) { let p: Vec> = params.iter().map(<[u16]>::to_vec).collect(); self.0.push(format!( "hook(params={p:?}, intermediates={intermediates:?}, ignore={ignore}, action={action:?})" )); } fn put(&mut self, byte: u8) { self.0.push(format!("put({byte:#04x})")); } fn unhook(&mut self) { self.0.push("unhook".into()); } } fn run(bytes: &[u8]) -> Vec { let mut p = Parser::new(); let mut r = Rec::default(); p.advance(&mut r, bytes); r.0 } // ---- Ground / print / execute ------------------------------------- #[test] fn plain_ascii_prints() { assert_eq!(run(b"abc"), vec!["print('a')", "print('b')", "print('c')"]); } #[test] fn c0_control_executes() { assert_eq!(run(b"\r\n"), vec!["exec(0x0d)", "exec(0x0a)"]); } #[test] fn utf8_two_byte_prints_one_char() { // U+00E9 (é) = 0xC3 0xA9 assert_eq!(run(&[0xC3, 0xA9]), vec!["print('é')"]); } #[test] fn utf8_three_byte_prints_one_char() { // U+2603 (☃) = 0xE2 0x98 0x83 assert_eq!(run(&[0xE2, 0x98, 0x83]), vec!["print('☃')"]); } #[test] fn utf8_four_byte_prints_one_char() { // U+1F980 (🦀) = 0xF0 0x9F 0xA6 0x80 assert_eq!(run(&[0xF0, 0x9F, 0xA6, 0x80]), vec!["print('🦀')"]); } #[test] fn utf8_survives_chunk_boundary() { let mut p = Parser::new(); let mut r = Rec::default(); // 🦀 split 2 / 2 p.advance(&mut r, &[0xF0, 0x9F]); p.advance(&mut r, &[0xA6, 0x80]); assert_eq!(r.0, vec!["print('🦀')"]); } // ---- CSI ----------------------------------------------------------- #[test] fn csi_single_param() { assert_eq!( run(b"\x1b[10A"), vec!["csi(params=[[10]], intermediates=[], ignore=false, action='A')"] ); } #[test] fn csi_multi_params() { assert_eq!( run(b"\x1b[1;2;3m"), vec!["csi(params=[[1], [2], [3]], intermediates=[], ignore=false, action='m')"] ); } #[test] fn csi_subparams_colon() { // `\e[38:2::1:2:3m` groups into one param with six subparams. assert_eq!( run(b"\x1b[38:2::1:2:3m"), vec!["csi(params=[[38, 2, 0, 1, 2, 3]], intermediates=[], ignore=false, action='m')"] ); } #[test] fn csi_private_marker() { assert_eq!( run(b"\x1b[?25h"), vec!["csi(params=[[25]], intermediates=[63], ignore=false, action='h')"] ); } #[test] fn csi_no_params() { assert_eq!( run(b"\x1b[m"), vec!["csi(params=[], intermediates=[], ignore=false, action='m')"] ); } #[test] fn csi_with_intermediate_space_q() { // DECSCUSR: `CSI Ps SP q` — space (0x20) intermediate then 'q'. assert_eq!( run(b"\x1b[2 q"), vec!["csi(params=[[2]], intermediates=[32], ignore=false, action='q')"] ); } // ---- OSC ----------------------------------------------------------- #[test] fn osc_st_terminated() { assert_eq!( run(b"\x1b]0;title\x1b\\"), vec![ "osc(params=[[48], [116, 105, 116, 108, 101]], bell=false)", // The trailing `\` after ESC dispatches as a no-op esc byte. "esc(intermediates=[], ignore=false, byte=0x5c)" ] ); } #[test] fn osc_bel_terminated() { assert_eq!( run(b"\x1b]2;shop\x07"), vec!["osc(params=[[50], [115, 104, 111, 112]], bell=true)"] ); } // ---- APC (kitty graphics) ------------------------------------------ #[test] fn apc_st_terminated() { let events = run(b"\x1b_Ga=T,f=32;PAYLOAD\x1b\\"); assert_eq!(events.len(), 2); assert!(events[0].starts_with("apc(")); assert!( events[0].contains("71"), "payload should carry 'G' (0x47=71)" ); } #[test] fn apc_bel_terminated() { let events = run(b"\x1b_hello\x07"); assert_eq!(events, vec!["apc([104, 101, 108, 108, 111])"]); } #[test] fn apc_survives_chunk_boundary() { let mut p = Parser::new(); let mut r = Rec::default(); p.advance(&mut r, b"\x1b_Ga="); assert!(r.0.is_empty(), "no dispatch mid-APC"); p.advance(&mut r, b"T;X\x07"); assert_eq!(r.0, vec!["apc([71, 97, 61, 84, 59, 88])"]); } // ---- ESC dispatch -------------------------------------------------- #[test] fn esc_bare_letter() { assert_eq!( run(b"\x1bM"), vec!["esc(intermediates=[], ignore=false, byte=0x4d)"] ); } // ---- DCS ----------------------------------------------------------- #[test] fn dcs_passthrough_st_terminated() { // `\eP1$r0m\e\\` — DECRQSS-response shape. `r` triggers hook (into // passthrough), `0`/`m` are the two put bytes, ESC unhooks, `\` is // the no-op esc byte closing the ST. let events = run(b"\x1bP1$r0m\x1b\\"); let names: Vec<&str> = events .iter() .map(|s| s.split('(').next().unwrap()) .collect(); assert_eq!(names, vec!["hook", "put", "put", "unhook", "esc"]); } // ---- State recovery ------------------------------------------------ #[test] fn cancel_aborts_escape() { // ESC then CAN (0x18) — the CAN executes and returns to Ground. let events = run(b"\x1b\x18X"); assert_eq!(events, vec!["exec(0x18)", "print('X')"]); } #[test] fn csi_ignoring_after_extra_intermediates() { // Only two intermediates fit; the third sets the ignore flag. let events = run(b"\x1b[!\"# X"); // The parser should still dispatch on the final byte, with ignore=true. assert!( events.last().is_some_and(|s| s.contains("ignore=true")), "dispatch should carry ignore=true, got {events:?}" ); } // ---- Bounded state (the 2026-08-29 DoS finding) -------------------- /// A CSI with more parameters than the list holds dispatches with the /// ignore flag set, exactly as a third intermediate already does, and stops /// growing. #[test] fn csi_past_the_parameter_cap_is_ignored_not_buffered() { let mut p = Parser::new(); let mut r = Rec::default(); let mut seq = b"\x1b[".to_vec(); seq.extend(std::iter::repeat_n(b';', 100_000)); seq.push(b'm'); p.advance(&mut r, &seq); let event = r.0.last().expect("a dispatch").clone(); assert!(event.contains("ignore=true"), "got {event}"); assert!( p.buffered_bytes() < 8 * 1024, "parser kept {} bytes for 100k separators", p.buffered_bytes() ); assert!(p.in_ground()); } /// Parameters up to the cap still arrive, so the cap changes nothing for /// anything anyone sends. #[test] fn csi_at_the_parameter_cap_still_dispatches_every_slot() { let params: Vec = (1..=MAX_PARAMS).map(|n| n.to_string()).collect(); let events = run(format!("\x1b[{}m", params.join(";")).as_bytes()); let event = events.last().expect("a dispatch"); assert!(event.contains("ignore=false"), "got {event}"); assert!(event.contains(&format!("[{MAX_PARAMS}]")), "got {event}"); } /// An over-long OSC body is dropped rather than truncated, and the buffer /// it grew goes back down. /// /// Dropped because half a base64 clipboard write is a different request, /// not a smaller one; shrunk because capacity is a high-water mark, and /// without that the cap would bound one sequence rather than the process. #[test] fn an_oversized_osc_body_is_dropped_and_the_buffer_shrinks() { let mut p = Parser::new(); let mut r = Rec::default(); let mut seq = b"\x1b]0;".to_vec(); seq.extend(std::iter::repeat_n(b'A', MAX_STRING_BYTES + 1)); seq.push(0x07); p.advance(&mut r, &seq); assert!( r.0.is_empty(), "a body over the cap should dispatch nothing, got {:?}", r.0 ); assert!( p.buffered_bytes() < 8 * 1024, "buffers stayed at {} bytes after the body ended", p.buffered_bytes() ); assert!(p.in_ground()); } /// A body under the cap is unaffected, including one large enough to have /// grown the buffer well past its resting size. #[test] fn an_ordinary_osc_body_still_dispatches() { let title = "t".repeat(100_000); let events = run(format!("\x1b]0;{title}\x07").as_bytes()); assert_eq!(events.len(), 1, "got {events:?}"); assert!(events[0].starts_with("osc("), "got {events:?}"); } /// The OSC field table is bounded on its own account, because a separator /// costs an index pair and contributes no body byte to charge it against. #[test] fn osc_separators_alone_do_not_grow_the_field_table() { let mut p = Parser::new(); let mut r = Rec::default(); let mut seq = b"\x1b]".to_vec(); seq.extend(std::iter::repeat_n(b';', 100_000)); seq.push(0x07); p.advance(&mut r, &seq); assert!(r.0.is_empty(), "got {:?}", r.0); assert!( p.buffered_bytes() < 64 * 1024, "parser kept {} bytes for 100k separators", p.buffered_bytes() ); } /// The APC body has the same ceiling. This is the one the kitty graphics /// protocol arrives through, and the protocol requires payloads over 4096 /// bytes to be chunked, so nothing legitimate comes near it. #[test] fn an_oversized_apc_body_is_dropped() { let mut p = Parser::new(); let mut r = Rec::default(); let mut seq = b"\x1b_G".to_vec(); seq.extend(std::iter::repeat_n(b'A', MAX_STRING_BYTES + 1)); seq.extend_from_slice(b"\x1b\\"); p.advance(&mut r, &seq); assert!(!r.0.iter().any(|e| e.starts_with("apc(")), "got {:?}", r.0); assert!(p.buffered_bytes() < 8 * 1024); } // ---- The bounds, as numbers --------------------------------------- /// Every one of these is written as an arithmetic expression, and an /// expression nothing asserts is a number nothing pins: mutation found that /// `8 * 1024 * 1024` could become `8 + 1024 + 1024` and no test noticed. /// The parser stays self-consistent under that change, which is exactly why /// its own behavioural tests cannot catch it. #[test] fn the_bounds_are_the_numbers_the_module_documents() { assert_eq!(MAX_PARAMS, 32, "vte's number, so we are a drop-in for it"); assert_eq!(MAX_STRING_BYTES, 8_388_608, "8 MiB"); assert_eq!(MAX_OSC_PARAMS, 1024); assert_eq!(INITIAL_BODY_CAPACITY, 2048); } // ---- Params, directly ---------------------------------------------- /// `len` and `is_empty` are public and nothing called them, so the whole /// accessor pair could return a constant unnoticed. `clear` is the other /// half: a parameter list that does not empty carries one sequence's /// parameters into the next. #[test] fn params_len_and_is_empty_track_the_open_groups() { let mut p = Params::default(); assert!(p.is_empty()); assert_eq!(p.len(), 0); assert!(p.new_param()); assert!(!p.is_empty()); assert_eq!(p.len(), 1); assert!(p.new_param()); assert_eq!(p.len(), 2); p.clear(); assert!(p.is_empty()); assert_eq!(p.len(), 0); } /// A subparameter costs a slot exactly as a parameter does, or `38:2::R:G:B` /// repeated is a cap that does not bind. The expression under test is /// `slots += 1`; `slots *= 1` leaves it at its opening value forever, which /// nothing observes without pushing the list to the cap. #[test] fn subparameters_consume_slots_so_the_cap_still_binds() { let mut p = Params::default(); assert!(p.new_param(), "the first slot"); for i in 1..MAX_PARAMS { assert!(p.new_subparam(), "slot {} of {MAX_PARAMS}", i + 1); } assert!( !p.new_subparam(), "slot {} is past the cap and must be refused", MAX_PARAMS + 1 ); } /// `footprint` feeds [`Parser::buffered_bytes`], which is what the soak /// oracle asserts an amplification ceiling against. A footprint that /// under-reports is an oracle that cannot fire. /// /// The expected value is written out here rather than read from the /// function, so the arithmetic is stated twice and a change to either side /// disagrees. #[test] fn footprint_counts_the_outer_vec_and_every_group() { let p = Params::default(); assert_eq!(p.footprint(), 0, "an unused list holds nothing"); let mut p = Params::default(); assert!(p.new_param()); assert!(p.new_subparam()); assert!(p.new_param()); let outer = p.inner.capacity(); let groups: usize = p.inner.iter().map(Vec::capacity).sum(); assert!(outer > 0 && groups > 0, "the case has to be non-degenerate"); assert_eq!( p.footprint(), outer * std::mem::size_of::>() + groups * std::mem::size_of::() ); } // ---- Parser accounting --------------------------------------------- /// `in_ground` is how a caller knows a chunk boundary is safe to cut on, /// and it is what the fuzz oracle checks after a terminated sequence. A /// constant `true` makes both of those say yes mid-sequence. #[test] fn in_ground_is_false_while_a_sequence_is_open() { let mut p = Parser::new(); let mut r = Rec::default(); assert!(p.in_ground(), "a fresh parser holds nothing"); p.advance(&mut r, b"\x1b[1"); assert!(!p.in_ground(), "mid-CSI"); p.advance(&mut r, b"m"); assert!(p.in_ground(), "the sequence terminated"); } /// The same twice-stated arithmetic as `footprint`, for the total the soak /// oracle actually reads. #[test] fn buffered_bytes_sums_every_buffer_the_parser_holds() { let mut p = Parser::new(); let mut r = Rec::default(); // An open DCS with parameters. The two body buffers are allocated at // their resting size from `Parser::new`, so the parameter list is the // only term that can be zero -- and an OSC leaves it zero, which lets // the `+ params.footprint()` term become `-` unnoticed. p.advance(&mut r, b"\x1bP1;2;3"); assert!(p.params.footprint() > 0, "the parameter term must be live"); let expected = p.osc_buf.capacity() + p.apc_buf.capacity() + p.osc_params.capacity() * std::mem::size_of::<(usize, usize)>() + p.params.footprint(); assert!(expected > 1, "the case has to distinguish 0 and 1"); assert_eq!(p.buffered_bytes(), expected); } /// `clear` empties the intermediates, the ignore flag and the parameters /// when a fresh sequence starts. Without it one sequence's parameters are /// dispatched as the next one's. #[test] fn a_fresh_sequence_does_not_inherit_the_last_ones_parameters() { assert_eq!( run(b"\x1b[1;2m\x1b[m"), vec![ "csi(params=[[1], [2]], intermediates=[], ignore=false, action='m')", "csi(params=[], intermediates=[], ignore=false, action='m')", ] ); } // ---- Ground -------------------------------------------------------- /// CAN and SUB abort a sequence and are executed in Ground like any other /// C0. They are their own match arm, so deleting it drops them silently. #[test] fn can_and_sub_execute_in_ground() { assert_eq!(run(b"\x18"), vec!["exec(0x18)"]); assert_eq!(run(b"\x1a"), vec!["exec(0x1a)"]); } // ---- Escape -------------------------------------------------------- #[test] fn a_c0_inside_escape_executes_without_ending_the_sequence() { assert_eq!( run(b"\x1b\rB"), vec![ "exec(0x0d)", "esc(intermediates=[], ignore=false, byte=0x42)" ] ); } #[test] fn an_escape_intermediate_reaches_the_dispatch() { assert_eq!( run(b"\x1b(B"), vec!["esc(intermediates=[40], ignore=false, byte=0x42)"] ); } /// A second intermediate accumulates rather than replacing the first. #[test] fn escape_intermediates_accumulate() { assert_eq!( run(b"\x1b($B"), vec!["esc(intermediates=[40, 36], ignore=false, byte=0x42)"] ); } #[test] fn a_c0_inside_an_escape_intermediate_executes() { assert_eq!( run(b"\x1b(\rB"), vec![ "exec(0x0d)", "esc(intermediates=[40], ignore=false, byte=0x42)" ] ); } /// SOS (0x58) and PM (0x5E) open a string the parser treats exactly as APC /// does: consumed, and delivered through the same callback. They share an /// arm with nothing, so deleting it leaves the parser sitting in Escape /// eating the body as escape finals. #[test] fn sos_and_pm_open_a_string_and_dispatch_it() { assert_eq!( run(b"\x1bXhi\x1b\\"), vec![ "apc([104, 105])", "esc(intermediates=[], ignore=false, byte=0x5c)" ] ); assert_eq!( run(b"\x1b^hi\x1b\\"), vec![ "apc([104, 105])", "esc(intermediates=[], ignore=false, byte=0x5c)" ] ); } // ---- CSI ----------------------------------------------------------- #[test] fn a_c0_inside_csi_entry_executes() { assert_eq!( run(b"\x1b[\rm"), vec![ "exec(0x0d)", "csi(params=[], intermediates=[], ignore=false, action='m')" ] ); } /// A colon opening a CSI opens a subparameter group, so the sequence /// carries one empty slot rather than none. Deleting the arm makes the /// colon vanish and the dispatch carry no parameters at all. #[test] fn a_leading_colon_opens_a_subparameter_slot() { assert_eq!( run(b"\x1b[:m"), vec!["csi(params=[[0]], intermediates=[], ignore=false, action='m')"] ); } /// The overflow flag is set when a slot is REFUSED, not when one is taken. /// Dropping the `!` inverts that, and every ordinary sequence starts /// reporting itself as ignored. #[test] fn opening_a_slot_that_succeeds_does_not_mark_the_sequence_ignored() { assert_eq!( run(b"\x1b[:m"), vec!["csi(params=[[0]], intermediates=[], ignore=false, action='m')"] ); assert_eq!( run(b"\x1b[;m"), vec!["csi(params=[[]], intermediates=[], ignore=false, action='m')"] ); } #[test] fn a_c0_inside_csi_param_executes() { assert_eq!( run(b"\x1b[1\rm"), vec![ "exec(0x0d)", "csi(params=[[1]], intermediates=[], ignore=false, action='m')" ] ); } /// A private marker arriving after a parameter is malformed: the DEC /// parser sends the sequence to CsiIgnore, so nothing dispatches. #[test] fn a_private_marker_after_a_parameter_abandons_the_sequence() { assert_eq!(run(b"\x1b[1::new()); } #[test] fn a_c0_inside_a_csi_intermediate_executes() { assert_eq!( run(b"\x1b[ \rm"), vec![ "exec(0x0d)", "csi(params=[], intermediates=[32], ignore=false, action='m')" ] ); } /// A parameter byte after an intermediate is out of order, so the sequence /// is abandoned rather than dispatched with the bytes rearranged. #[test] fn a_parameter_after_an_intermediate_abandons_the_sequence() { assert_eq!(run(b"\x1b[ 1m"), Vec::::new()); } /// CsiIgnore still executes C0s, and still ends on a final byte -- ending /// is the part that matters, because a parser stuck in CsiIgnore swallows /// the rest of the stream. #[test] fn an_abandoned_csi_still_executes_c0s_and_still_ends() { assert_eq!(run(b"\x1b[1<\rma"), vec!["exec(0x0d)", "print('a')"]); } // ---- DCS ----------------------------------------------------------- #[test] fn a_bare_dcs_hooks_and_unhooks() { assert_eq!( run(b"\x1bPq\x1b\\"), vec![ "hook(params=[], intermediates=[], ignore=false, action='q')", "unhook", "esc(intermediates=[], ignore=false, byte=0x5c)", ] ); } #[test] fn dcs_passthrough_delivers_the_body_and_a_bell_terminates_it() { assert_eq!( run(b"\x1bPqAB\x07"), vec![ "hook(params=[], intermediates=[], ignore=false, action='q')", "put(0x41)", "put(0x42)", "unhook", ] ); } /// The digit arithmetic in both DCS parameter states, which is the same /// `b - b'0'` the CSI states use and was covered in neither. #[test] fn dcs_parameters_parse_as_numbers() { assert_eq!( run(b"\x1bP1q\x1b\\")[0], "hook(params=[[1]], intermediates=[], ignore=false, action='q')" ); assert_eq!( run(b"\x1bP12q\x1b\\")[0], "hook(params=[[12]], intermediates=[], ignore=false, action='q')" ); assert_eq!( run(b"\x1bP1;2q\x1b\\")[0], "hook(params=[[1], [2]], intermediates=[], ignore=false, action='q')" ); } #[test] fn a_dcs_intermediate_reaches_the_hook() { assert_eq!( run(b"\x1bP$q\x1b\\")[0], "hook(params=[], intermediates=[36], ignore=false, action='q')" ); assert_eq!( run(b"\x1bP1$q\x1b\\")[0], "hook(params=[[1]], intermediates=[36], ignore=false, action='q')" ); assert_eq!( run(b"\x1bP$$q\x1b\\")[0], "hook(params=[], intermediates=[36, 36], ignore=false, action='q')" ); } #[test] fn a_dcs_private_marker_reaches_the_hook() { assert_eq!( run(b"\x1bP?q\x1b\\")[0], "hook(params=[], intermediates=[63], ignore=false, action='q')" ); } /// A separator opening a DCS opens an empty parameter, and does not mark /// the sequence ignored -- the same inverted-`!` shape as CSI. #[test] fn a_leading_dcs_separator_opens_an_empty_parameter() { assert_eq!( run(b"\x1bP;q\x1b\\")[0], "hook(params=[[]], intermediates=[], ignore=false, action='q')" ); } /// Every route into DcsIgnore. A colon is illegal in a DCS parameter list /// and a private marker is illegal after one, so both abandon the sequence: /// no hook, and therefore no passthrough of whatever followed. #[test] fn an_illegal_dcs_prelude_abandons_the_sequence() { for seq in [ &b"\x1bP:q\x1b\\"[..], // colon in DcsEntry &b"\x1bP1:q\x1b\\"[..], // colon in DcsParam &b"\x1bP1::new()); } /// DcsIgnore ends on ESC and nothing else, and the parser comes back out of /// it. A DcsIgnore that never ends swallows the rest of the stream; one /// that ends on every byte ends in the middle of the discarded prelude. #[test] fn an_abandoned_dcs_ends_on_the_terminator_and_prints_again() { assert_eq!( run(b"\x1bP:q\x1b\\a"), vec![ "esc(intermediates=[], ignore=false, byte=0x5c)", "print('a')" ] ); }