Skip to main content

max / shop

24.2 KB · 785 lines History Blame Raw
1 //! Tests for [`super`].
2
3 use super::*;
4
5 /// Recording Perform: every callback appends a stringly summary so tests
6 /// can assert on the whole event log.
7 #[derive(Default)]
8 struct Rec(Vec<String>);
9
10 impl Perform for Rec {
11 fn print(&mut self, c: char) {
12 self.0.push(format!("print({c:?})"));
13 }
14 fn execute(&mut self, byte: u8) {
15 self.0.push(format!("exec({byte:#04x})"));
16 }
17 fn csi_dispatch(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
18 let p: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
19 self.0.push(format!(
20 "csi(params={p:?}, intermediates={intermediates:?}, ignore={ignore}, action={action:?})"
21 ));
22 }
23 fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
24 self.0.push(format!(
25 "esc(intermediates={intermediates:?}, ignore={ignore}, byte={byte:#04x})"
26 ));
27 }
28 fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
29 let p: Vec<Vec<u8>> = params.iter().map(|s| s.to_vec()).collect();
30 self.0
31 .push(format!("osc(params={p:?}, bell={bell_terminated})"));
32 }
33 fn apc_dispatch(&mut self, data: &[u8]) {
34 self.0.push(format!("apc({data:?})"));
35 }
36 fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
37 let p: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
38 self.0.push(format!(
39 "hook(params={p:?}, intermediates={intermediates:?}, ignore={ignore}, action={action:?})"
40 ));
41 }
42 fn put(&mut self, byte: u8) {
43 self.0.push(format!("put({byte:#04x})"));
44 }
45 fn unhook(&mut self) {
46 self.0.push("unhook".into());
47 }
48 }
49
50 fn run(bytes: &[u8]) -> Vec<String> {
51 let mut p = Parser::new();
52 let mut r = Rec::default();
53 p.advance(&mut r, bytes);
54 r.0
55 }
56
57 // ---- Ground / print / execute -------------------------------------
58
59 #[test]
60 fn plain_ascii_prints() {
61 assert_eq!(run(b"abc"), vec!["print('a')", "print('b')", "print('c')"]);
62 }
63
64 #[test]
65 fn c0_control_executes() {
66 assert_eq!(run(b"\r\n"), vec!["exec(0x0d)", "exec(0x0a)"]);
67 }
68
69 #[test]
70 fn utf8_two_byte_prints_one_char() {
71 // U+00E9 (é) = 0xC3 0xA9
72 assert_eq!(run(&[0xC3, 0xA9]), vec!["print('é')"]);
73 }
74
75 #[test]
76 fn utf8_three_byte_prints_one_char() {
77 // U+2603 (☃) = 0xE2 0x98 0x83
78 assert_eq!(run(&[0xE2, 0x98, 0x83]), vec!["print('☃')"]);
79 }
80
81 #[test]
82 fn utf8_four_byte_prints_one_char() {
83 // U+1F980 (🦀) = 0xF0 0x9F 0xA6 0x80
84 assert_eq!(run(&[0xF0, 0x9F, 0xA6, 0x80]), vec!["print('🦀')"]);
85 }
86
87 #[test]
88 fn utf8_survives_chunk_boundary() {
89 let mut p = Parser::new();
90 let mut r = Rec::default();
91 // 🦀 split 2 / 2
92 p.advance(&mut r, &[0xF0, 0x9F]);
93 p.advance(&mut r, &[0xA6, 0x80]);
94 assert_eq!(r.0, vec!["print('🦀')"]);
95 }
96
97 // ---- CSI -----------------------------------------------------------
98
99 #[test]
100 fn csi_single_param() {
101 assert_eq!(
102 run(b"\x1b[10A"),
103 vec!["csi(params=[[10]], intermediates=[], ignore=false, action='A')"]
104 );
105 }
106
107 #[test]
108 fn csi_multi_params() {
109 assert_eq!(
110 run(b"\x1b[1;2;3m"),
111 vec!["csi(params=[[1], [2], [3]], intermediates=[], ignore=false, action='m')"]
112 );
113 }
114
115 #[test]
116 fn csi_subparams_colon() {
117 // `\e[38:2::1:2:3m` groups into one param with six subparams.
118 assert_eq!(
119 run(b"\x1b[38:2::1:2:3m"),
120 vec!["csi(params=[[38, 2, 0, 1, 2, 3]], intermediates=[], ignore=false, action='m')"]
121 );
122 }
123
124 #[test]
125 fn csi_private_marker() {
126 assert_eq!(
127 run(b"\x1b[?25h"),
128 vec!["csi(params=[[25]], intermediates=[63], ignore=false, action='h')"]
129 );
130 }
131
132 #[test]
133 fn csi_no_params() {
134 assert_eq!(
135 run(b"\x1b[m"),
136 vec!["csi(params=[], intermediates=[], ignore=false, action='m')"]
137 );
138 }
139
140 #[test]
141 fn csi_with_intermediate_space_q() {
142 // DECSCUSR: `CSI Ps SP q` — space (0x20) intermediate then 'q'.
143 assert_eq!(
144 run(b"\x1b[2 q"),
145 vec!["csi(params=[[2]], intermediates=[32], ignore=false, action='q')"]
146 );
147 }
148
149 // ---- OSC -----------------------------------------------------------
150
151 #[test]
152 fn osc_st_terminated() {
153 assert_eq!(
154 run(b"\x1b]0;title\x1b\\"),
155 vec![
156 "osc(params=[[48], [116, 105, 116, 108, 101]], bell=false)",
157 // The trailing `\` after ESC dispatches as a no-op esc byte.
158 "esc(intermediates=[], ignore=false, byte=0x5c)"
159 ]
160 );
161 }
162
163 #[test]
164 fn osc_bel_terminated() {
165 assert_eq!(
166 run(b"\x1b]2;shop\x07"),
167 vec!["osc(params=[[50], [115, 104, 111, 112]], bell=true)"]
168 );
169 }
170
171 // ---- APC (kitty graphics) ------------------------------------------
172
173 #[test]
174 fn apc_st_terminated() {
175 let events = run(b"\x1b_Ga=T,f=32;PAYLOAD\x1b\\");
176 assert_eq!(events.len(), 2);
177 assert!(events[0].starts_with("apc("));
178 assert!(
179 events[0].contains("71"),
180 "payload should carry 'G' (0x47=71)"
181 );
182 }
183
184 #[test]
185 fn apc_bel_terminated() {
186 let events = run(b"\x1b_hello\x07");
187 assert_eq!(events, vec!["apc([104, 101, 108, 108, 111])"]);
188 }
189
190 #[test]
191 fn apc_survives_chunk_boundary() {
192 let mut p = Parser::new();
193 let mut r = Rec::default();
194 p.advance(&mut r, b"\x1b_Ga=");
195 assert!(r.0.is_empty(), "no dispatch mid-APC");
196 p.advance(&mut r, b"T;X\x07");
197 assert_eq!(r.0, vec!["apc([71, 97, 61, 84, 59, 88])"]);
198 }
199
200 // ---- ESC dispatch --------------------------------------------------
201
202 #[test]
203 fn esc_bare_letter() {
204 assert_eq!(
205 run(b"\x1bM"),
206 vec!["esc(intermediates=[], ignore=false, byte=0x4d)"]
207 );
208 }
209
210 // ---- DCS -----------------------------------------------------------
211
212 #[test]
213 fn dcs_passthrough_st_terminated() {
214 // `\eP1$r0m\e\\` — DECRQSS-response shape. `r` triggers hook (into
215 // passthrough), `0`/`m` are the two put bytes, ESC unhooks, `\` is
216 // the no-op esc byte closing the ST.
217 let events = run(b"\x1bP1$r0m\x1b\\");
218 let names: Vec<&str> = events
219 .iter()
220 .map(|s| s.split('(').next().unwrap())
221 .collect();
222 assert_eq!(names, vec!["hook", "put", "put", "unhook", "esc"]);
223 }
224
225 // ---- State recovery ------------------------------------------------
226
227 #[test]
228 fn cancel_aborts_escape() {
229 // ESC then CAN (0x18) — the CAN executes and returns to Ground.
230 let events = run(b"\x1b\x18X");
231 assert_eq!(events, vec!["exec(0x18)", "print('X')"]);
232 }
233
234 #[test]
235 fn csi_ignoring_after_extra_intermediates() {
236 // Only two intermediates fit; the third sets the ignore flag.
237 let events = run(b"\x1b[!\"# X");
238 // The parser should still dispatch on the final byte, with ignore=true.
239 assert!(
240 events.last().is_some_and(|s| s.contains("ignore=true")),
241 "dispatch should carry ignore=true, got {events:?}"
242 );
243 }
244
245 // ---- Bounded state (the 2026-08-29 DoS finding) --------------------
246
247 /// A CSI with more parameters than the list holds dispatches with the
248 /// ignore flag set, exactly as a third intermediate already does, and stops
249 /// growing.
250 #[test]
251 fn csi_past_the_parameter_cap_is_ignored_not_buffered() {
252 let mut p = Parser::new();
253 let mut r = Rec::default();
254 let mut seq = b"\x1b[".to_vec();
255 seq.extend(std::iter::repeat_n(b';', 100_000));
256 seq.push(b'm');
257 p.advance(&mut r, &seq);
258
259 let event = r.0.last().expect("a dispatch").clone();
260 assert!(event.contains("ignore=true"), "got {event}");
261 assert!(
262 p.buffered_bytes() < 8 * 1024,
263 "parser kept {} bytes for 100k separators",
264 p.buffered_bytes()
265 );
266 assert!(p.in_ground());
267 }
268
269 /// Parameters up to the cap still arrive, so the cap changes nothing for
270 /// anything anyone sends.
271 #[test]
272 fn csi_at_the_parameter_cap_still_dispatches_every_slot() {
273 let params: Vec<String> = (1..=MAX_PARAMS).map(|n| n.to_string()).collect();
274 let events = run(format!("\x1b[{}m", params.join(";")).as_bytes());
275
276 let event = events.last().expect("a dispatch");
277 assert!(event.contains("ignore=false"), "got {event}");
278 assert!(event.contains(&format!("[{MAX_PARAMS}]")), "got {event}");
279 }
280
281 /// An over-long OSC body is dropped rather than truncated, and the buffer
282 /// it grew goes back down.
283 ///
284 /// Dropped because half a base64 clipboard write is a different request,
285 /// not a smaller one; shrunk because capacity is a high-water mark, and
286 /// without that the cap would bound one sequence rather than the process.
287 #[test]
288 fn an_oversized_osc_body_is_dropped_and_the_buffer_shrinks() {
289 let mut p = Parser::new();
290 let mut r = Rec::default();
291 let mut seq = b"\x1b]0;".to_vec();
292 seq.extend(std::iter::repeat_n(b'A', MAX_STRING_BYTES + 1));
293 seq.push(0x07);
294 p.advance(&mut r, &seq);
295
296 assert!(
297 r.0.is_empty(),
298 "a body over the cap should dispatch nothing, got {:?}",
299 r.0
300 );
301 assert!(
302 p.buffered_bytes() < 8 * 1024,
303 "buffers stayed at {} bytes after the body ended",
304 p.buffered_bytes()
305 );
306 assert!(p.in_ground());
307 }
308
309 /// A body under the cap is unaffected, including one large enough to have
310 /// grown the buffer well past its resting size.
311 #[test]
312 fn an_ordinary_osc_body_still_dispatches() {
313 let title = "t".repeat(100_000);
314 let events = run(format!("\x1b]0;{title}\x07").as_bytes());
315 assert_eq!(events.len(), 1, "got {events:?}");
316 assert!(events[0].starts_with("osc("), "got {events:?}");
317 }
318
319 /// The OSC field table is bounded on its own account, because a separator
320 /// costs an index pair and contributes no body byte to charge it against.
321 #[test]
322 fn osc_separators_alone_do_not_grow_the_field_table() {
323 let mut p = Parser::new();
324 let mut r = Rec::default();
325 let mut seq = b"\x1b]".to_vec();
326 seq.extend(std::iter::repeat_n(b';', 100_000));
327 seq.push(0x07);
328 p.advance(&mut r, &seq);
329
330 assert!(r.0.is_empty(), "got {:?}", r.0);
331 assert!(
332 p.buffered_bytes() < 64 * 1024,
333 "parser kept {} bytes for 100k separators",
334 p.buffered_bytes()
335 );
336 }
337
338 /// The APC body has the same ceiling. This is the one the kitty graphics
339 /// protocol arrives through, and the protocol requires payloads over 4096
340 /// bytes to be chunked, so nothing legitimate comes near it.
341 #[test]
342 fn an_oversized_apc_body_is_dropped() {
343 let mut p = Parser::new();
344 let mut r = Rec::default();
345 let mut seq = b"\x1b_G".to_vec();
346 seq.extend(std::iter::repeat_n(b'A', MAX_STRING_BYTES + 1));
347 seq.extend_from_slice(b"\x1b\\");
348 p.advance(&mut r, &seq);
349
350 assert!(!r.0.iter().any(|e| e.starts_with("apc(")), "got {:?}", r.0);
351 assert!(p.buffered_bytes() < 8 * 1024);
352 }
353
354 // ---- The bounds, as numbers ---------------------------------------
355
356 /// Every one of these is written as an arithmetic expression, and an
357 /// expression nothing asserts is a number nothing pins: mutation found that
358 /// `8 * 1024 * 1024` could become `8 + 1024 + 1024` and no test noticed.
359 /// The parser stays self-consistent under that change, which is exactly why
360 /// its own behavioural tests cannot catch it.
361 #[test]
362 fn the_bounds_are_the_numbers_the_module_documents() {
363 assert_eq!(MAX_PARAMS, 32, "vte's number, so we are a drop-in for it");
364 assert_eq!(MAX_STRING_BYTES, 8_388_608, "8 MiB");
365 assert_eq!(MAX_OSC_PARAMS, 1024);
366 assert_eq!(INITIAL_BODY_CAPACITY, 2048);
367 }
368
369 // ---- Params, directly ----------------------------------------------
370
371 /// `len` and `is_empty` are public and nothing called them, so the whole
372 /// accessor pair could return a constant unnoticed. `clear` is the other
373 /// half: a parameter list that does not empty carries one sequence's
374 /// parameters into the next.
375 #[test]
376 fn params_len_and_is_empty_track_the_open_groups() {
377 let mut p = Params::default();
378 assert!(p.is_empty());
379 assert_eq!(p.len(), 0);
380
381 assert!(p.new_param());
382 assert!(!p.is_empty());
383 assert_eq!(p.len(), 1);
384
385 assert!(p.new_param());
386 assert_eq!(p.len(), 2);
387
388 p.clear();
389 assert!(p.is_empty());
390 assert_eq!(p.len(), 0);
391 }
392
393 /// A subparameter costs a slot exactly as a parameter does, or `38:2::R:G:B`
394 /// repeated is a cap that does not bind. The expression under test is
395 /// `slots += 1`; `slots *= 1` leaves it at its opening value forever, which
396 /// nothing observes without pushing the list to the cap.
397 #[test]
398 fn subparameters_consume_slots_so_the_cap_still_binds() {
399 let mut p = Params::default();
400 assert!(p.new_param(), "the first slot");
401 for i in 1..MAX_PARAMS {
402 assert!(p.new_subparam(), "slot {} of {MAX_PARAMS}", i + 1);
403 }
404 assert!(
405 !p.new_subparam(),
406 "slot {} is past the cap and must be refused",
407 MAX_PARAMS + 1
408 );
409 }
410
411 /// `footprint` feeds [`Parser::buffered_bytes`], which is what the soak
412 /// oracle asserts an amplification ceiling against. A footprint that
413 /// under-reports is an oracle that cannot fire.
414 ///
415 /// The expected value is written out here rather than read from the
416 /// function, so the arithmetic is stated twice and a change to either side
417 /// disagrees.
418 #[test]
419 fn footprint_counts_the_outer_vec_and_every_group() {
420 let p = Params::default();
421 assert_eq!(p.footprint(), 0, "an unused list holds nothing");
422
423 let mut p = Params::default();
424 assert!(p.new_param());
425 assert!(p.new_subparam());
426 assert!(p.new_param());
427
428 let outer = p.inner.capacity();
429 let groups: usize = p.inner.iter().map(Vec::capacity).sum();
430 assert!(outer > 0 && groups > 0, "the case has to be non-degenerate");
431 assert_eq!(
432 p.footprint(),
433 outer * std::mem::size_of::<Vec<u16>>() + groups * std::mem::size_of::<u16>()
434 );
435 }
436
437 // ---- Parser accounting ---------------------------------------------
438
439 /// `in_ground` is how a caller knows a chunk boundary is safe to cut on,
440 /// and it is what the fuzz oracle checks after a terminated sequence. A
441 /// constant `true` makes both of those say yes mid-sequence.
442 #[test]
443 fn in_ground_is_false_while_a_sequence_is_open() {
444 let mut p = Parser::new();
445 let mut r = Rec::default();
446 assert!(p.in_ground(), "a fresh parser holds nothing");
447
448 p.advance(&mut r, b"\x1b[1");
449 assert!(!p.in_ground(), "mid-CSI");
450
451 p.advance(&mut r, b"m");
452 assert!(p.in_ground(), "the sequence terminated");
453 }
454
455 /// The same twice-stated arithmetic as `footprint`, for the total the soak
456 /// oracle actually reads.
457 #[test]
458 fn buffered_bytes_sums_every_buffer_the_parser_holds() {
459 let mut p = Parser::new();
460 let mut r = Rec::default();
461 // An open DCS with parameters. The two body buffers are allocated at
462 // their resting size from `Parser::new`, so the parameter list is the
463 // only term that can be zero -- and an OSC leaves it zero, which lets
464 // the `+ params.footprint()` term become `-` unnoticed.
465 p.advance(&mut r, b"\x1bP1;2;3");
466 assert!(p.params.footprint() > 0, "the parameter term must be live");
467
468 let expected = p.osc_buf.capacity()
469 + p.apc_buf.capacity()
470 + p.osc_params.capacity() * std::mem::size_of::<(usize, usize)>()
471 + p.params.footprint();
472 assert!(expected > 1, "the case has to distinguish 0 and 1");
473 assert_eq!(p.buffered_bytes(), expected);
474 }
475
476 /// `clear` empties the intermediates, the ignore flag and the parameters
477 /// when a fresh sequence starts. Without it one sequence's parameters are
478 /// dispatched as the next one's.
479 #[test]
480 fn a_fresh_sequence_does_not_inherit_the_last_ones_parameters() {
481 assert_eq!(
482 run(b"\x1b[1;2m\x1b[m"),
483 vec![
484 "csi(params=[[1], [2]], intermediates=[], ignore=false, action='m')",
485 "csi(params=[], intermediates=[], ignore=false, action='m')",
486 ]
487 );
488 }
489
490 // ---- Ground --------------------------------------------------------
491
492 /// CAN and SUB abort a sequence and are executed in Ground like any other
493 /// C0. They are their own match arm, so deleting it drops them silently.
494 #[test]
495 fn can_and_sub_execute_in_ground() {
496 assert_eq!(run(b"\x18"), vec!["exec(0x18)"]);
497 assert_eq!(run(b"\x1a"), vec!["exec(0x1a)"]);
498 }
499
500 // ---- Escape --------------------------------------------------------
501
502 #[test]
503 fn a_c0_inside_escape_executes_without_ending_the_sequence() {
504 assert_eq!(
505 run(b"\x1b\rB"),
506 vec![
507 "exec(0x0d)",
508 "esc(intermediates=[], ignore=false, byte=0x42)"
509 ]
510 );
511 }
512
513 #[test]
514 fn an_escape_intermediate_reaches_the_dispatch() {
515 assert_eq!(
516 run(b"\x1b(B"),
517 vec!["esc(intermediates=[40], ignore=false, byte=0x42)"]
518 );
519 }
520
521 /// A second intermediate accumulates rather than replacing the first.
522 #[test]
523 fn escape_intermediates_accumulate() {
524 assert_eq!(
525 run(b"\x1b($B"),
526 vec!["esc(intermediates=[40, 36], ignore=false, byte=0x42)"]
527 );
528 }
529
530 #[test]
531 fn a_c0_inside_an_escape_intermediate_executes() {
532 assert_eq!(
533 run(b"\x1b(\rB"),
534 vec![
535 "exec(0x0d)",
536 "esc(intermediates=[40], ignore=false, byte=0x42)"
537 ]
538 );
539 }
540
541 /// SOS (0x58) and PM (0x5E) open a string the parser treats exactly as APC
542 /// does: consumed, and delivered through the same callback. They share an
543 /// arm with nothing, so deleting it leaves the parser sitting in Escape
544 /// eating the body as escape finals.
545 #[test]
546 fn sos_and_pm_open_a_string_and_dispatch_it() {
547 assert_eq!(
548 run(b"\x1bXhi\x1b\\"),
549 vec![
550 "apc([104, 105])",
551 "esc(intermediates=[], ignore=false, byte=0x5c)"
552 ]
553 );
554 assert_eq!(
555 run(b"\x1b^hi\x1b\\"),
556 vec![
557 "apc([104, 105])",
558 "esc(intermediates=[], ignore=false, byte=0x5c)"
559 ]
560 );
561 }
562
563 // ---- CSI -----------------------------------------------------------
564
565 #[test]
566 fn a_c0_inside_csi_entry_executes() {
567 assert_eq!(
568 run(b"\x1b[\rm"),
569 vec![
570 "exec(0x0d)",
571 "csi(params=[], intermediates=[], ignore=false, action='m')"
572 ]
573 );
574 }
575
576 /// A colon opening a CSI opens a subparameter group, so the sequence
577 /// carries one empty slot rather than none. Deleting the arm makes the
578 /// colon vanish and the dispatch carry no parameters at all.
579 #[test]
580 fn a_leading_colon_opens_a_subparameter_slot() {
581 assert_eq!(
582 run(b"\x1b[:m"),
583 vec!["csi(params=[[0]], intermediates=[], ignore=false, action='m')"]
584 );
585 }
586
587 /// The overflow flag is set when a slot is REFUSED, not when one is taken.
588 /// Dropping the `!` inverts that, and every ordinary sequence starts
589 /// reporting itself as ignored.
590 #[test]
591 fn opening_a_slot_that_succeeds_does_not_mark_the_sequence_ignored() {
592 assert_eq!(
593 run(b"\x1b[:m"),
594 vec!["csi(params=[[0]], intermediates=[], ignore=false, action='m')"]
595 );
596 assert_eq!(
597 run(b"\x1b[;m"),
598 vec!["csi(params=[[]], intermediates=[], ignore=false, action='m')"]
599 );
600 }
601
602 #[test]
603 fn a_c0_inside_csi_param_executes() {
604 assert_eq!(
605 run(b"\x1b[1\rm"),
606 vec![
607 "exec(0x0d)",
608 "csi(params=[[1]], intermediates=[], ignore=false, action='m')"
609 ]
610 );
611 }
612
613 /// A private marker arriving after a parameter is malformed: the DEC
614 /// parser sends the sequence to CsiIgnore, so nothing dispatches.
615 #[test]
616 fn a_private_marker_after_a_parameter_abandons_the_sequence() {
617 assert_eq!(run(b"\x1b[1<m"), Vec::<String>::new());
618 }
619
620 #[test]
621 fn a_c0_inside_a_csi_intermediate_executes() {
622 assert_eq!(
623 run(b"\x1b[ \rm"),
624 vec![
625 "exec(0x0d)",
626 "csi(params=[], intermediates=[32], ignore=false, action='m')"
627 ]
628 );
629 }
630
631 /// A parameter byte after an intermediate is out of order, so the sequence
632 /// is abandoned rather than dispatched with the bytes rearranged.
633 #[test]
634 fn a_parameter_after_an_intermediate_abandons_the_sequence() {
635 assert_eq!(run(b"\x1b[ 1m"), Vec::<String>::new());
636 }
637
638 /// CsiIgnore still executes C0s, and still ends on a final byte -- ending
639 /// is the part that matters, because a parser stuck in CsiIgnore swallows
640 /// the rest of the stream.
641 #[test]
642 fn an_abandoned_csi_still_executes_c0s_and_still_ends() {
643 assert_eq!(run(b"\x1b[1<\rma"), vec!["exec(0x0d)", "print('a')"]);
644 }
645
646 // ---- DCS -----------------------------------------------------------
647
648 #[test]
649 fn a_bare_dcs_hooks_and_unhooks() {
650 assert_eq!(
651 run(b"\x1bPq\x1b\\"),
652 vec![
653 "hook(params=[], intermediates=[], ignore=false, action='q')",
654 "unhook",
655 "esc(intermediates=[], ignore=false, byte=0x5c)",
656 ]
657 );
658 }
659
660 #[test]
661 fn dcs_passthrough_delivers_the_body_and_a_bell_terminates_it() {
662 assert_eq!(
663 run(b"\x1bPqAB\x07"),
664 vec![
665 "hook(params=[], intermediates=[], ignore=false, action='q')",
666 "put(0x41)",
667 "put(0x42)",
668 "unhook",
669 ]
670 );
671 }
672
673 /// The digit arithmetic in both DCS parameter states, which is the same
674 /// `b - b'0'` the CSI states use and was covered in neither.
675 #[test]
676 fn dcs_parameters_parse_as_numbers() {
677 assert_eq!(
678 run(b"\x1bP1q\x1b\\")[0],
679 "hook(params=[[1]], intermediates=[], ignore=false, action='q')"
680 );
681 assert_eq!(
682 run(b"\x1bP12q\x1b\\")[0],
683 "hook(params=[[12]], intermediates=[], ignore=false, action='q')"
684 );
685 assert_eq!(
686 run(b"\x1bP1;2q\x1b\\")[0],
687 "hook(params=[[1], [2]], intermediates=[], ignore=false, action='q')"
688 );
689 }
690
691 #[test]
692 fn a_dcs_intermediate_reaches_the_hook() {
693 assert_eq!(
694 run(b"\x1bP$q\x1b\\")[0],
695 "hook(params=[], intermediates=[36], ignore=false, action='q')"
696 );
697 assert_eq!(
698 run(b"\x1bP1$q\x1b\\")[0],
699 "hook(params=[[1]], intermediates=[36], ignore=false, action='q')"
700 );
701 assert_eq!(
702 run(b"\x1bP$$q\x1b\\")[0],
703 "hook(params=[], intermediates=[36, 36], ignore=false, action='q')"
704 );
705 }
706
707 #[test]
708 fn a_dcs_private_marker_reaches_the_hook() {
709 assert_eq!(
710 run(b"\x1bP?q\x1b\\")[0],
711 "hook(params=[], intermediates=[63], ignore=false, action='q')"
712 );
713 }
714
715 /// A separator opening a DCS opens an empty parameter, and does not mark
716 /// the sequence ignored -- the same inverted-`!` shape as CSI.
717 #[test]
718 fn a_leading_dcs_separator_opens_an_empty_parameter() {
719 assert_eq!(
720 run(b"\x1bP;q\x1b\\")[0],
721 "hook(params=[[]], intermediates=[], ignore=false, action='q')"
722 );
723 }
724
725 /// Every route into DcsIgnore. A colon is illegal in a DCS parameter list
726 /// and a private marker is illegal after one, so both abandon the sequence:
727 /// no hook, and therefore no passthrough of whatever followed.
728 #[test]
729 fn an_illegal_dcs_prelude_abandons_the_sequence() {
730 for seq in [
731 &b"\x1bP:q\x1b\\"[..], // colon in DcsEntry
732 &b"\x1bP1:q\x1b\\"[..], // colon in DcsParam
733 &b"\x1bP1<q\x1b\\"[..], // private marker after a parameter
734 &b"\x1bP$1q\x1b\\"[..], // parameter after an intermediate
735 ] {
736 let log = run(seq);
737 assert!(
738 !log.iter().any(|e| e.starts_with("hook(")),
739 "{seq:?} must not hook, got {log:?}"
740 );
741 }
742 }
743
744 /// The digit arithmetic in `dcs_entry` specifically. `b - b'0'` and
745 /// `b / b'0'` agree on '0' and '1' and part company at '2', so a test whose
746 /// only DCS parameter starts with 1 cannot see the difference -- and the
747 /// first digit of a DCS is the one `dcs_entry` handles, every later one
748 /// belongs to `dcs_param`.
749 #[test]
750 fn the_first_dcs_digit_is_a_value_and_not_a_quotient() {
751 assert_eq!(
752 run(b"\x1bP2q\x1b\\")[0],
753 "hook(params=[[2]], intermediates=[], ignore=false, action='q')"
754 );
755 assert_eq!(
756 run(b"\x1bP9q\x1b\\")[0],
757 "hook(params=[[9]], intermediates=[], ignore=false, action='q')"
758 );
759 }
760
761 /// DcsIgnore ends on ESC and on nothing else. Testing that with `ESC \` alone
762 /// cannot show it: a handler that ends on EVERY byte reaches Escape one byte
763 /// early, and the ESC that follows is then absorbed by Escape's own ESC arm,
764 /// so the log comes out identical. A final byte with no terminator is what
765 /// separates them -- 'B' dispatches from Escape and is silence from
766 /// DcsIgnore.
767 #[test]
768 fn an_abandoned_dcs_swallows_everything_that_is_not_the_terminator() {
769 assert_eq!(run(b"\x1bP:qB"), Vec::<String>::new());
770 }
771
772 /// DcsIgnore ends on ESC and nothing else, and the parser comes back out of
773 /// it. A DcsIgnore that never ends swallows the rest of the stream; one
774 /// that ends on every byte ends in the middle of the discarded prelude.
775 #[test]
776 fn an_abandoned_dcs_ends_on_the_terminator_and_prints_again() {
777 assert_eq!(
778 run(b"\x1bP:q\x1b\\a"),
779 vec![
780 "esc(intermediates=[], ignore=false, byte=0x5c)",
781 "print('a')"
782 ]
783 );
784 }
785