Skip to main content

max / shop

Move shop-vt's test module to a sibling file lib.rs goes 1595 lines to 806, its 58 tests moving to src/tests.rs. The production half stays whole: it is one Paul Williams transition table whose handlers share eleven Parser fields, and splitting it is deliberately not planned. The mutants exclusion list is unaffected. It matches type-qualified patterns (Perform::, Parser::) rather than file paths, as its own header states.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
Author: Max Johnson <me@maxj.phd> · 2026-09-03 23:24 UTC
Signed with PGP, not checked
Commit: f37fba2b9f61e550063e13307f57606e692dcb57
Parent: 60c26d0
2 files changed, +500 insertions, -497 deletions
@@ -803,793 +803,4 @@
803 803 }
804 804
805 805 #[cfg(test)]
806 - mod tests {
807 - use super::*;
808 -
809 - /// Recording Perform: every callback appends a stringly summary so tests
810 - /// can assert on the whole event log.
811 - #[derive(Default)]
812 - struct Rec(Vec<String>);
813 -
814 - impl Perform for Rec {
815 - fn print(&mut self, c: char) {
816 - self.0.push(format!("print({c:?})"));
817 - }
818 - fn execute(&mut self, byte: u8) {
819 - self.0.push(format!("exec({byte:#04x})"));
820 - }
821 - fn csi_dispatch(
822 - &mut self,
823 - params: &Params,
824 - intermediates: &[u8],
825 - ignore: bool,
826 - action: char,
827 - ) {
828 - let p: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
829 - self.0.push(format!(
830 - "csi(params={p:?}, intermediates={intermediates:?}, ignore={ignore}, action={action:?})"
831 - ));
832 - }
833 - fn esc_dispatch(&mut self, intermediates: &[u8], ignore: bool, byte: u8) {
834 - self.0.push(format!(
835 - "esc(intermediates={intermediates:?}, ignore={ignore}, byte={byte:#04x})"
836 - ));
837 - }
838 - fn osc_dispatch(&mut self, params: &[&[u8]], bell_terminated: bool) {
839 - let p: Vec<Vec<u8>> = params.iter().map(|s| s.to_vec()).collect();
840 - self.0
841 - .push(format!("osc(params={p:?}, bell={bell_terminated})"));
842 - }
843 - fn apc_dispatch(&mut self, data: &[u8]) {
844 - self.0.push(format!("apc({data:?})"));
845 - }
846 - fn hook(&mut self, params: &Params, intermediates: &[u8], ignore: bool, action: char) {
847 - let p: Vec<Vec<u16>> = params.iter().map(<[u16]>::to_vec).collect();
848 - self.0.push(format!(
849 - "hook(params={p:?}, intermediates={intermediates:?}, ignore={ignore}, action={action:?})"
850 - ));
851 - }
852 - fn put(&mut self, byte: u8) {
853 - self.0.push(format!("put({byte:#04x})"));
854 - }
855 - fn unhook(&mut self) {
856 - self.0.push("unhook".into());
857 - }
858 - }
859 -
860 - fn run(bytes: &[u8]) -> Vec<String> {
861 - let mut p = Parser::new();
862 - let mut r = Rec::default();
863 - p.advance(&mut r, bytes);
864 - r.0
865 - }
866 -
867 - // ---- Ground / print / execute -------------------------------------
868 -
869 - #[test]
870 - fn plain_ascii_prints() {
871 - assert_eq!(run(b"abc"), vec!["print('a')", "print('b')", "print('c')"]);
872 - }
873 -
874 - #[test]
875 - fn c0_control_executes() {
876 - assert_eq!(run(b"\r\n"), vec!["exec(0x0d)", "exec(0x0a)"]);
877 - }
878 -
879 - #[test]
880 - fn utf8_two_byte_prints_one_char() {
881 - // U+00E9 (é) = 0xC3 0xA9
882 - assert_eq!(run(&[0xC3, 0xA9]), vec!["print('é')"]);
883 - }
884 -
885 - #[test]
886 - fn utf8_three_byte_prints_one_char() {
887 - // U+2603 (☃) = 0xE2 0x98 0x83
888 - assert_eq!(run(&[0xE2, 0x98, 0x83]), vec!["print('☃')"]);
889 - }
890 -
891 - #[test]
892 - fn utf8_four_byte_prints_one_char() {
893 - // U+1F980 (🦀) = 0xF0 0x9F 0xA6 0x80
894 - assert_eq!(run(&[0xF0, 0x9F, 0xA6, 0x80]), vec!["print('🦀')"]);
895 - }
896 -
897 - #[test]
898 - fn utf8_survives_chunk_boundary() {
899 - let mut p = Parser::new();
900 - let mut r = Rec::default();
901 - // 🦀 split 2 / 2
902 - p.advance(&mut r, &[0xF0, 0x9F]);
903 - p.advance(&mut r, &[0xA6, 0x80]);
904 - assert_eq!(r.0, vec!["print('🦀')"]);
905 - }
906 -
907 - // ---- CSI -----------------------------------------------------------
908 -
909 - #[test]
910 - fn csi_single_param() {
911 - assert_eq!(
912 - run(b"\x1b[10A"),
913 - vec!["csi(params=[[10]], intermediates=[], ignore=false, action='A')"]
914 - );
915 - }
916 -
917 - #[test]
918 - fn csi_multi_params() {
919 - assert_eq!(
920 - run(b"\x1b[1;2;3m"),
921 - vec!["csi(params=[[1], [2], [3]], intermediates=[], ignore=false, action='m')"]
922 - );
923 - }
924 -
925 - #[test]
926 - fn csi_subparams_colon() {
927 - // `\e[38:2::1:2:3m` groups into one param with six subparams.
928 - assert_eq!(
929 - run(b"\x1b[38:2::1:2:3m"),
930 - vec!["csi(params=[[38, 2, 0, 1, 2, 3]], intermediates=[], ignore=false, action='m')"]
931 - );
932 - }
933 -
934 - #[test]
935 - fn csi_private_marker() {
936 - assert_eq!(
937 - run(b"\x1b[?25h"),
938 - vec!["csi(params=[[25]], intermediates=[63], ignore=false, action='h')"]
939 - );
940 - }
941 -
942 - #[test]
943 - fn csi_no_params() {
944 - assert_eq!(
945 - run(b"\x1b[m"),
946 - vec!["csi(params=[], intermediates=[], ignore=false, action='m')"]
947 - );
948 - }
949 -
950 - #[test]
951 - fn csi_with_intermediate_space_q() {
952 - // DECSCUSR: `CSI Ps SP q` — space (0x20) intermediate then 'q'.
953 - assert_eq!(
954 - run(b"\x1b[2 q"),
955 - vec!["csi(params=[[2]], intermediates=[32], ignore=false, action='q')"]
956 - );
957 - }
958 -
959 - // ---- OSC -----------------------------------------------------------
960 -
961 - #[test]
962 - fn osc_st_terminated() {
963 - assert_eq!(
964 - run(b"\x1b]0;title\x1b\\"),
965 - vec![
966 - "osc(params=[[48], [116, 105, 116, 108, 101]], bell=false)",
967 - // The trailing `\` after ESC dispatches as a no-op esc byte.
968 - "esc(intermediates=[], ignore=false, byte=0x5c)"
969 - ]
970 - );
971 - }
972 -
973 - #[test]
974 - fn osc_bel_terminated() {
975 - assert_eq!(
976 - run(b"\x1b]2;shop\x07"),
977 - vec!["osc(params=[[50], [115, 104, 111, 112]], bell=true)"]
978 - );
979 - }
980 -
981 - // ---- APC (kitty graphics) ------------------------------------------
982 -
983 - #[test]
984 - fn apc_st_terminated() {
985 - let events = run(b"\x1b_Ga=T,f=32;PAYLOAD\x1b\\");
986 - assert_eq!(events.len(), 2);
987 - assert!(events[0].starts_with("apc("));
988 - assert!(
989 - events[0].contains("71"),
990 - "payload should carry 'G' (0x47=71)"
991 - );
992 - }
993 -
994 - #[test]
995 - fn apc_bel_terminated() {
996 - let events = run(b"\x1b_hello\x07");
997 - assert_eq!(events, vec!["apc([104, 101, 108, 108, 111])"]);
998 - }
999 -
1000 - #[test]
1001 - fn apc_survives_chunk_boundary() {
1002 - let mut p = Parser::new();
1003 - let mut r = Rec::default();
1004 - p.advance(&mut r, b"\x1b_Ga=");
1005 - assert!(r.0.is_empty(), "no dispatch mid-APC");
1006 - p.advance(&mut r, b"T;X\x07");
1007 - assert_eq!(r.0, vec!["apc([71, 97, 61, 84, 59, 88])"]);
1008 - }
1009 -
1010 - // ---- ESC dispatch --------------------------------------------------
1011 -
1012 - #[test]
1013 - fn esc_bare_letter() {
1014 - assert_eq!(
1015 - run(b"\x1bM"),
1016 - vec!["esc(intermediates=[], ignore=false, byte=0x4d)"]
1017 - );
1018 - }
1019 -
1020 - // ---- DCS -----------------------------------------------------------
1021 -
1022 - #[test]
1023 - fn dcs_passthrough_st_terminated() {
1024 - // `\eP1$r0m\e\\` — DECRQSS-response shape. `r` triggers hook (into
1025 - // passthrough), `0`/`m` are the two put bytes, ESC unhooks, `\` is
1026 - // the no-op esc byte closing the ST.
1027 - let events = run(b"\x1bP1$r0m\x1b\\");
1028 - let names: Vec<&str> = events
1029 - .iter()
1030 - .map(|s| s.split('(').next().unwrap())
1031 - .collect();
1032 - assert_eq!(names, vec!["hook", "put", "put", "unhook", "esc"]);
1033 - }
1034 -
1035 - // ---- State recovery ------------------------------------------------
1036 -
1037 - #[test]
1038 - fn cancel_aborts_escape() {
1039 - // ESC then CAN (0x18) — the CAN executes and returns to Ground.
1040 - let events = run(b"\x1b\x18X");
1041 - assert_eq!(events, vec!["exec(0x18)", "print('X')"]);
1042 - }
1043 -
1044 - #[test]
1045 - fn csi_ignoring_after_extra_intermediates() {
1046 - // Only two intermediates fit; the third sets the ignore flag.
1047 - let events = run(b"\x1b[!\"# X");
1048 - // The parser should still dispatch on the final byte, with ignore=true.
1049 - assert!(
1050 - events.last().is_some_and(|s| s.contains("ignore=true")),
1051 - "dispatch should carry ignore=true, got {events:?}"
1052 - );
1053 - }
1054 -
1055 - // ---- Bounded state (the 2026-08-29 DoS finding) --------------------
1056 -
1057 - /// A CSI with more parameters than the list holds dispatches with the
1058 - /// ignore flag set, exactly as a third intermediate already does, and stops
1059 - /// growing.
1060 - #[test]
1061 - fn csi_past_the_parameter_cap_is_ignored_not_buffered() {
1062 - let mut p = Parser::new();
1063 - let mut r = Rec::default();
1064 - let mut seq = b"\x1b[".to_vec();
1065 - seq.extend(std::iter::repeat_n(b';', 100_000));
1066 - seq.push(b'm');
1067 - p.advance(&mut r, &seq);
1068 -
1069 - let event = r.0.last().expect("a dispatch").clone();
1070 - assert!(event.contains("ignore=true"), "got {event}");
1071 - assert!(
1072 - p.buffered_bytes() < 8 * 1024,
1073 - "parser kept {} bytes for 100k separators",
1074 - p.buffered_bytes()
1075 - );
1076 - assert!(p.in_ground());
1077 - }
1078 -
1079 - /// Parameters up to the cap still arrive, so the cap changes nothing for
1080 - /// anything anyone sends.
1081 - #[test]
1082 - fn csi_at_the_parameter_cap_still_dispatches_every_slot() {
1083 - let params: Vec<String> = (1..=MAX_PARAMS).map(|n| n.to_string()).collect();
1084 - let events = run(format!("\x1b[{}m", params.join(";")).as_bytes());
1085 -
1086 - let event = events.last().expect("a dispatch");
1087 - assert!(event.contains("ignore=false"), "got {event}");
1088 - assert!(event.contains(&format!("[{MAX_PARAMS}]")), "got {event}");
1089 - }
1090 -
1091 - /// An over-long OSC body is dropped rather than truncated, and the buffer
1092 - /// it grew goes back down.
1093 - ///
1094 - /// Dropped because half a base64 clipboard write is a different request,
1095 - /// not a smaller one; shrunk because capacity is a high-water mark, and
1096 - /// without that the cap would bound one sequence rather than the process.
1097 - #[test]
1098 - fn an_oversized_osc_body_is_dropped_and_the_buffer_shrinks() {
1099 - let mut p = Parser::new();
1100 - let mut r = Rec::default();
1101 - let mut seq = b"\x1b]0;".to_vec();
1102 - seq.extend(std::iter::repeat_n(b'A', MAX_STRING_BYTES + 1));
1103 - seq.push(0x07);
1104 - p.advance(&mut r, &seq);
1105 -
1106 - assert!(
1107 - r.0.is_empty(),
1108 - "a body over the cap should dispatch nothing, got {:?}",
1109 - r.0
1110 - );
1111 - assert!(
1112 - p.buffered_bytes() < 8 * 1024,
1113 - "buffers stayed at {} bytes after the body ended",
1114 - p.buffered_bytes()
1115 - );
1116 - assert!(p.in_ground());
1117 - }
1118 -
1119 - /// A body under the cap is unaffected, including one large enough to have
1120 - /// grown the buffer well past its resting size.
1121 - #[test]
1122 - fn an_ordinary_osc_body_still_dispatches() {
1123 - let title = "t".repeat(100_000);
1124 - let events = run(format!("\x1b]0;{title}\x07").as_bytes());
1125 - assert_eq!(events.len(), 1, "got {events:?}");
1126 - assert!(events[0].starts_with("osc("), "got {events:?}");
1127 - }
1128 -
1129 - /// The OSC field table is bounded on its own account, because a separator
1130 - /// costs an index pair and contributes no body byte to charge it against.
1131 - #[test]
1132 - fn osc_separators_alone_do_not_grow_the_field_table() {
1133 - let mut p = Parser::new();
1134 - let mut r = Rec::default();
1135 - let mut seq = b"\x1b]".to_vec();
1136 - seq.extend(std::iter::repeat_n(b';', 100_000));
1137 - seq.push(0x07);
1138 - p.advance(&mut r, &seq);
1139 -
1140 - assert!(r.0.is_empty(), "got {:?}", r.0);
1141 - assert!(
1142 - p.buffered_bytes() < 64 * 1024,
1143 - "parser kept {} bytes for 100k separators",
1144 - p.buffered_bytes()
1145 - );
1146 - }
1147 -
1148 - /// The APC body has the same ceiling. This is the one the kitty graphics
1149 - /// protocol arrives through, and the protocol requires payloads over 4096
1150 - /// bytes to be chunked, so nothing legitimate comes near it.
1151 - #[test]
1152 - fn an_oversized_apc_body_is_dropped() {
1153 - let mut p = Parser::new();
1154 - let mut r = Rec::default();
1155 - let mut seq = b"\x1b_G".to_vec();
1156 - seq.extend(std::iter::repeat_n(b'A', MAX_STRING_BYTES + 1));
1157 - seq.extend_from_slice(b"\x1b\\");
1158 - p.advance(&mut r, &seq);
1159 -
1160 - assert!(!r.0.iter().any(|e| e.starts_with("apc(")), "got {:?}", r.0);
1161 - assert!(p.buffered_bytes() < 8 * 1024);
1162 - }
1163 -
1164 - // ---- The bounds, as numbers ---------------------------------------
1165 -
1166 - /// Every one of these is written as an arithmetic expression, and an
1167 - /// expression nothing asserts is a number nothing pins: mutation found that
1168 - /// `8 * 1024 * 1024` could become `8 + 1024 + 1024` and no test noticed.
1169 - /// The parser stays self-consistent under that change, which is exactly why
1170 - /// its own behavioural tests cannot catch it.
1171 - #[test]
1172 - fn the_bounds_are_the_numbers_the_module_documents() {
1173 - assert_eq!(MAX_PARAMS, 32, "vte's number, so we are a drop-in for it");
1174 - assert_eq!(MAX_STRING_BYTES, 8_388_608, "8 MiB");
1175 - assert_eq!(MAX_OSC_PARAMS, 1024);
1176 - assert_eq!(INITIAL_BODY_CAPACITY, 2048);
1177 - }
1178 -
1179 - // ---- Params, directly ----------------------------------------------
1180 -
1181 - /// `len` and `is_empty` are public and nothing called them, so the whole
1182 - /// accessor pair could return a constant unnoticed. `clear` is the other
1183 - /// half: a parameter list that does not empty carries one sequence's
1184 - /// parameters into the next.
1185 - #[test]
1186 - fn params_len_and_is_empty_track_the_open_groups() {
1187 - let mut p = Params::default();
1188 - assert!(p.is_empty());
1189 - assert_eq!(p.len(), 0);
1190 -
1191 - assert!(p.new_param());
1192 - assert!(!p.is_empty());
1193 - assert_eq!(p.len(), 1);
1194 -
1195 - assert!(p.new_param());
1196 - assert_eq!(p.len(), 2);
1197 -
1198 - p.clear();
1199 - assert!(p.is_empty());
1200 - assert_eq!(p.len(), 0);
1201 - }
1202 -
1203 - /// A subparameter costs a slot exactly as a parameter does, or `38:2::R:G:B`
1204 - /// repeated is a cap that does not bind. The expression under test is
1205 - /// `slots += 1`; `slots *= 1` leaves it at its opening value forever, which
1206 - /// nothing observes without pushing the list to the cap.
1207 - #[test]
1208 - fn subparameters_consume_slots_so_the_cap_still_binds() {
1209 - let mut p = Params::default();
1210 - assert!(p.new_param(), "the first slot");
1211 - for i in 1..MAX_PARAMS {
1212 - assert!(p.new_subparam(), "slot {} of {MAX_PARAMS}", i + 1);
1213 - }
1214 - assert!(
1215 - !p.new_subparam(),
1216 - "slot {} is past the cap and must be refused",
1217 - MAX_PARAMS + 1
1218 - );
1219 - }
1220 -
1221 - /// `footprint` feeds [`Parser::buffered_bytes`], which is what the soak
1222 - /// oracle asserts an amplification ceiling against. A footprint that
1223 - /// under-reports is an oracle that cannot fire.
1224 - ///
1225 - /// The expected value is written out here rather than read from the
1226 - /// function, so the arithmetic is stated twice and a change to either side
1227 - /// disagrees.
1228 - #[test]
1229 - fn footprint_counts_the_outer_vec_and_every_group() {
1230 - let p = Params::default();
1231 - assert_eq!(p.footprint(), 0, "an unused list holds nothing");
1232 -
1233 - let mut p = Params::default();
1234 - assert!(p.new_param());
1235 - assert!(p.new_subparam());
1236 - assert!(p.new_param());
1237 -
1238 - let outer = p.inner.capacity();
1239 - let groups: usize = p.inner.iter().map(Vec::capacity).sum();
1240 - assert!(outer > 0 && groups > 0, "the case has to be non-degenerate");
1241 - assert_eq!(
1242 - p.footprint(),
1243 - outer * std::mem::size_of::<Vec<u16>>() + groups * std::mem::size_of::<u16>()
1244 - );
1245 - }
1246 -
1247 - // ---- Parser accounting ---------------------------------------------
1248 -
1249 - /// `in_ground` is how a caller knows a chunk boundary is safe to cut on,
1250 - /// and it is what the fuzz oracle checks after a terminated sequence. A
1251 - /// constant `true` makes both of those say yes mid-sequence.
1252 - #[test]
1253 - fn in_ground_is_false_while_a_sequence_is_open() {
1254 - let mut p = Parser::new();
1255 - let mut r = Rec::default();
1256 - assert!(p.in_ground(), "a fresh parser holds nothing");
1257 -
1258 - p.advance(&mut r, b"\x1b[1");
1259 - assert!(!p.in_ground(), "mid-CSI");
1260 -
1261 - p.advance(&mut r, b"m");
1262 - assert!(p.in_ground(), "the sequence terminated");
1263 - }
1264 -
1265 - /// The same twice-stated arithmetic as `footprint`, for the total the soak
1266 - /// oracle actually reads.
1267 - #[test]
1268 - fn buffered_bytes_sums_every_buffer_the_parser_holds() {
1269 - let mut p = Parser::new();
1270 - let mut r = Rec::default();
1271 - // An open DCS with parameters. The two body buffers are allocated at
1272 - // their resting size from `Parser::new`, so the parameter list is the
1273 - // only term that can be zero -- and an OSC leaves it zero, which lets
1274 - // the `+ params.footprint()` term become `-` unnoticed.
1275 - p.advance(&mut r, b"\x1bP1;2;3");
1276 - assert!(p.params.footprint() > 0, "the parameter term must be live");
1277 -
1278 - let expected = p.osc_buf.capacity()
1279 - + p.apc_buf.capacity()
1280 - + p.osc_params.capacity() * std::mem::size_of::<(usize, usize)>()
1281 - + p.params.footprint();
1282 - assert!(expected > 1, "the case has to distinguish 0 and 1");
1283 - assert_eq!(p.buffered_bytes(), expected);
1284 - }
1285 -
1286 - /// `clear` empties the intermediates, the ignore flag and the parameters
1287 - /// when a fresh sequence starts. Without it one sequence's parameters are
1288 - /// dispatched as the next one's.
1289 - #[test]
1290 - fn a_fresh_sequence_does_not_inherit_the_last_ones_parameters() {
1291 - assert_eq!(
1292 - run(b"\x1b[1;2m\x1b[m"),
1293 - vec![
1294 - "csi(params=[[1], [2]], intermediates=[], ignore=false, action='m')",
1295 - "csi(params=[], intermediates=[], ignore=false, action='m')",
1296 - ]
1297 - );
1298 - }
1299 -
1300 - // ---- Ground --------------------------------------------------------
1301 -
1302 - /// CAN and SUB abort a sequence and are executed in Ground like any other
Lines truncated
@@ -1,0 +1,784 @@
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 --------------------------------------------------------
Lines truncated