1const PICKLE_OPCODE_WORK_BUDGET_BYTES: usize = 4096;
11
12const REDUCE: u8 = 0x52; const BUILD: u8 = 0x62; const FRAME_OPCODE: u8 = 0x95;
15
16#[derive(Debug)]
17enum WalkError {
18 ShortRead,
19 Blocked,
20}
21
22struct WalkState<'a> {
23 window: &'a [u8],
24 pos: usize,
25 stack: Vec<u8>,
26 marks: Vec<usize>,
27 memo: std::collections::HashMap<u32, u8>,
28}
29
30fn pickle_read(state: &mut WalkState, size: usize) -> Result<Vec<u8>, WalkError> {
31 if state.pos + size > state.window.len() {
32 return Err(WalkError::ShortRead);
33 }
34 let bytes = state.window[state.pos..state.pos + size].to_vec();
35 state.pos += size;
36 Ok(bytes)
37}
38
39fn pickle_readline(state: &mut WalkState) -> Result<Vec<u8>, WalkError> {
40 let mut line = Vec::new();
41 loop {
42 if state.pos >= state.window.len() {
43 return Err(WalkError::ShortRead);
44 }
45 let byte = state.window[state.pos];
46 state.pos += 1;
47 line.push(byte);
48 if byte == 0x0a {
49 break;
50 }
51 }
52 Ok(line)
53}
54
55fn le_int(bytes: &[u8]) -> usize {
56 let mut value = 0usize;
57 for b in bytes.iter().rev() {
58 value = value * 0x100 + usize::from(*b);
59 }
60 value
61}
62
63fn push_mark(state: &mut WalkState) {
64 state.marks.push(state.stack.len());
65}
66
67fn pop_mark(state: &mut WalkState) -> Result<(), WalkError> {
68 let Some(mark) = state.marks.pop() else {
69 return Err(WalkError::Blocked);
70 };
71 while state.stack.len() > mark {
73 state.stack.pop();
74 }
75 Ok(())
76}
77
78fn is_digits(text: &[u8]) -> bool {
79 !text.is_empty() && text.iter().all(u8::is_ascii_digit)
80}
81
82#[allow(
83 clippy::too_many_lines,
84 clippy::match_same_arms,
85 reason = "one arm per pickle opcode in reference-table order: arms with identical bodies are distinct opcodes that share behavior, and splitting or merging them would obscure the 1:1 opcode mapping"
86)]
87fn dispatch_opcode(state: &mut WalkState, key: u8) -> Result<(), WalkError> {
88 match key {
89 0x28 => {
90 push_mark(state);
92 Ok(())
93 }
94 0x30 => {
95 state
97 .stack
98 .pop()
99 .map_or(Err(WalkError::Blocked), |_| Ok(()))
100 }
101 0x31 => pop_mark(state), 0x32 => {
103 let Some(last) = state.stack.last().copied() else {
105 return Err(WalkError::Blocked);
106 };
107 state.stack.push(last);
108 Ok(())
109 }
110 0x5d | 0x7d | 0x29 => {
111 state.stack.push(1);
113 Ok(())
114 }
115 0x6c | 0x74 | 0x64 => {
116 pop_mark(state)?;
118 state.stack.push(1);
119 Ok(())
120 }
121 0x61 => {
122 state
124 .stack
125 .pop()
126 .map_or(Err(WalkError::Blocked), |_| Ok(()))
127 }
128 0x65 => pop_mark(state), 0x73 => {
130 state
132 .stack
133 .pop()
134 .map_or(Err(WalkError::Blocked), |_| Ok(()))?;
135 state
136 .stack
137 .pop()
138 .map_or(Err(WalkError::Blocked), |_| Ok(()))
139 }
140 0x75 => pop_mark(state), 0x4e | 0x89 | 0x88 => {
142 state.stack.push(1);
144 Ok(())
145 }
146 0x49 => {
147 let data = pickle_readline(state)?;
149 let text = &data[..data.len().saturating_sub(1)];
150 if text == b"01" || text == b"00" {
151 return Ok(());
152 }
153 if text.first() == Some(&b'-') && is_digits(&text[1..]) {
154 return Ok(());
155 }
156 if is_digits(text) {
157 return Ok(());
158 }
159 Err(WalkError::Blocked)
160 }
161 0x4c => {
162 let data = pickle_readline(state)?;
164 let mut text = &data[..data.len().saturating_sub(1)];
165 if text.last() == Some(&b'L') {
166 text = &text[..text.len() - 1];
167 }
168 if text.first() == Some(&b'-') && is_digits(&text[1..]) {
169 return Ok(());
170 }
171 if is_digits(text) {
172 return Ok(());
173 }
174 Err(WalkError::Blocked)
175 }
176 0x46 => {
177 let data = pickle_readline(state)?;
179 let text = std::str::from_utf8(&data[..data.len().saturating_sub(1)])
180 .map_err(|_| WalkError::Blocked)?;
181 if text.parse::<f64>().is_ok() {
182 Ok(())
183 } else {
184 Err(WalkError::Blocked)
185 }
186 }
187 0x4a => {
188 pickle_read(state, 4)?;
189 Ok(())
190 }
191 0x4b => {
192 pickle_read(state, 1)?;
193 Ok(())
194 }
195 0x4d => {
196 pickle_read(state, 2)?;
197 Ok(())
198 }
199 0x47 => {
200 pickle_read(state, 8)?;
201 Ok(())
202 }
203 0x53 => {
204 let data = pickle_readline(state)?;
206 let body = &data[..data.len().saturating_sub(1)];
207 if body.len() < 2
208 || body[0] != body[body.len() - 1]
209 || (body[0] != 0x22 && body[0] != 0x27)
210 {
211 return Err(WalkError::Blocked);
212 }
213 Ok(())
214 }
215 0x56 => {
216 pickle_readline(state)?;
217 Ok(())
218 }
219 0x58 => {
220 let length = le_int(&pickle_read(state, 4)?);
221 pickle_read(state, length)?;
222 Ok(())
223 }
224 0x8c => {
225 let length = pickle_read(state, 1)?[0] as usize;
226 pickle_read(state, length)?;
227 Ok(())
228 }
229 0x54 => {
230 let length = le_int(&pickle_read(state, 4)?);
231 pickle_read(state, length)?;
232 Ok(())
233 }
234 0x55 => {
235 let length = pickle_read(state, 1)?[0] as usize;
236 pickle_read(state, length)?;
237 Ok(())
238 }
239 0x42 => {
240 let length = le_int(&pickle_read(state, 4)?);
241 pickle_read(state, length)?;
242 Ok(())
243 }
244 0x8e => {
245 let length = le_int(&pickle_read(state, 8)?);
246 pickle_read(state, length)?;
247 Ok(())
248 }
249 0x43 => {
250 let length = pickle_read(state, 1)?[0] as usize;
251 pickle_read(state, length)?;
252 Ok(())
253 }
254 0x96 => {
255 let length = le_int(&pickle_read(state, 8)?);
256 pickle_read(state, length)?;
257 Ok(())
258 }
259 0x8a => {
260 let length = pickle_read(state, 1)?[0] as usize;
261 pickle_read(state, length)?;
262 Ok(())
263 }
264 0x8b => {
265 let length = le_int(&pickle_read(state, 4)?);
266 pickle_read(state, length)?;
267 Ok(())
268 }
269 0x80 => {
270 pickle_read(state, 1)?;
271 Ok(())
272 }
273 0x94 => {
274 if state.stack.is_empty() {
276 return Err(WalkError::Blocked);
277 }
278 let synthetic = u32::try_from(state.memo.len()).unwrap_or(u32::MAX);
279 state.memo.insert(synthetic, 1);
280 Ok(())
281 }
282 0x71 => {
283 let index = pickle_read(state, 1)?[0];
285 if state.stack.is_empty() {
286 return Err(WalkError::Blocked);
287 }
288 memo_set(state, u32::from(index));
289 Ok(())
290 }
291 0x72 => {
292 let index = le_int(&pickle_read(state, 4)?);
294 if state.stack.is_empty() {
295 return Err(WalkError::Blocked);
296 }
297 memo_set(state, u32::try_from(index).unwrap_or(u32::MAX));
298 Ok(())
299 }
300 0x68 => {
301 let index = pickle_read(state, 1)?[0];
303 if !memo_has(state, u32::from(index)) {
304 return Err(WalkError::Blocked);
305 }
306 state.stack.push(1);
307 Ok(())
308 }
309 0x6a => {
310 let index = le_int(&pickle_read(state, 4)?);
312 if !memo_has(state, u32::try_from(index).unwrap_or(u32::MAX)) {
313 return Err(WalkError::Blocked);
314 }
315 state.stack.push(1);
316 Ok(())
317 }
318 0x67 => {
319 let data = pickle_readline(state)?;
321 let text = std::str::from_utf8(&data[..data.len().saturating_sub(1)])
322 .map_err(|_| WalkError::Blocked)?;
323 let index: usize = text.parse().map_err(|_| WalkError::Blocked)?;
324 if !memo_has(state, u32::try_from(index).unwrap_or(u32::MAX)) {
325 return Err(WalkError::Blocked);
326 }
327 state.stack.push(1);
328 Ok(())
329 }
330 0x63 | 0x69 | 0x6f | 0x81 | 0x82 | 0x93 | 0x50 | 0x51 | 0x84 | 0x85 | 0x86 => {
333 Err(WalkError::Blocked)
334 }
335 _ => Err(WalkError::Blocked),
336 }
337}
338
339fn memo_set(state: &mut WalkState, index: u32) {
340 state.memo.insert(index, 1);
341}
342
343fn memo_has(state: &WalkState, index: u32) -> bool {
344 state.memo.get(&index).is_some_and(|v| *v == 1)
345}
346
347fn walk_opcodes(
353 state: &mut WalkState,
354 stop_at_reduce_or_build: bool,
355 is_complete: bool,
356) -> Option<bool> {
357 while state.pos < state.window.len() {
358 #[cfg(not(coverage))] let key = match pickle_read(state, 1) {
361 Ok(bytes) => bytes[0],
362 Err(WalkError::ShortRead) => return (!is_complete).then_some(true),
363 Err(WalkError::Blocked) => return Some(false),
364 };
365 #[cfg(coverage)]
366 let key = pickle_read(state, 1).expect("one byte is available under the loop guard")[0];
367 if stop_at_reduce_or_build && (key == REDUCE || key == BUILD) {
368 return Some(true);
369 }
370 if key == FRAME_OPCODE {
371 match pickle_read(state, 8) {
372 Ok(_) => continue,
373 Err(WalkError::ShortRead | WalkError::Blocked) => {
376 return (!is_complete).then_some(true);
377 }
378 }
379 }
380 match dispatch_opcode(state, key) {
381 Ok(()) => {}
382 Err(WalkError::ShortRead) => return (!is_complete).then_some(true),
383 Err(WalkError::Blocked) => return Some(false),
384 }
385 }
386 if is_complete {
387 Some(!stop_at_reduce_or_build)
388 } else {
389 Some(true)
390 }
391}
392
393fn fresh_state(window: &[u8], seed_stack: bool) -> WalkState<'_> {
394 WalkState {
395 window,
396 pos: 0,
397 stack: if seed_stack { vec![1] } else { Vec::new() },
398 marks: Vec::new(),
399 memo: std::collections::HashMap::new(),
400 }
401}
402
403fn window_from_chars(chars: &str) -> Option<Vec<u8>> {
405 let mut bytes = Vec::with_capacity(chars.len());
406 for c in chars.chars() {
407 let code = u32::from(c);
408 if code <= 0xff {
409 bytes.push(code as u8);
410 } else {
411 #[cfg(not(coverage))] if (0xdc80..=0xdcff).contains(&code) {
415 bytes.push((code - 0xdc80 + 0x80) as u8);
416 continue;
417 }
418 return None;
419 }
420 }
421 Some(bytes)
422}
423
424#[must_use]
425pub fn pickle_prefix_is_opcode_stream(prefix: &str) -> bool {
426 if prefix.is_empty() || prefix.ends_with('\n') {
427 return true;
428 }
429 let is_complete = prefix.len() <= PICKLE_OPCODE_WORK_BUDGET_BYTES;
430 let Some(window) = window_from_chars(if is_complete {
431 prefix
432 } else {
433 &prefix[..PICKLE_OPCODE_WORK_BUDGET_BYTES]
434 }) else {
435 return false;
436 };
437 walk_opcodes(&mut fresh_state(&window, false), false, is_complete) != Some(false)
439}
440
441#[must_use]
442pub fn pickle_suffix_reaches_reduce_or_build(suffix: &str) -> bool {
443 let is_complete = suffix.len() <= PICKLE_OPCODE_WORK_BUDGET_BYTES;
444 let Some(window) = window_from_chars(if is_complete {
445 suffix
446 } else {
447 &suffix[..PICKLE_OPCODE_WORK_BUDGET_BYTES]
448 }) else {
449 return false;
450 };
451 walk_opcodes(&mut fresh_state(&window, true), true, is_complete) != Some(false)
452}
453
454#[must_use]
458pub fn pickle_global_candidate_is_injection(
459 haystack: &str,
460 candidate: super::pyregex::Candidate,
461 group_one_end: usize,
462) -> bool {
463 if !pickle_prefix_is_opcode_stream(&haystack[..candidate.start]) {
464 return false;
465 }
466 pickle_suffix_reaches_reduce_or_build(&haystack[group_one_end..])
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn plain_text_prefix_is_stream_like() {
475 assert!(pickle_prefix_is_opcode_stream(""));
477 assert!(pickle_prefix_is_opcode_stream("hello\n"));
478 }
479
480 #[test]
481 fn proto_stream_prefix_walks() {
482 let mut bytes = vec![0x80u8, 0x04, 0x95];
484 bytes.extend_from_slice(&[0x00; 8]);
485 bytes.extend_from_slice(&[0x8c, 0x04, b'm', b'a', b'i', b'n']);
486 let text: String = bytes.iter().map(|b| char::from(*b)).collect();
487 assert!(pickle_prefix_is_opcode_stream(&text));
488 }
489
490 #[test]
491 fn reduce_opcode_stops_suffix_walk() {
492 let mut bytes = vec![0x80u8, 0x04, 0x95];
493 bytes.extend_from_slice(&[0x00; 8]);
494 bytes.push(b'R');
495 let text: String = bytes.iter().map(|b| char::from(*b)).collect();
496 assert!(pickle_suffix_reaches_reduce_or_build(&text));
497 }
498
499 #[test]
500 fn unknown_opcode_fails() {
501 assert!(!pickle_suffix_reaches_reduce_or_build("\u{1}nonsense"));
502 }
503}
504
505#[cfg(test)]
506mod opcode_tests {
507 use super::*;
508
509 fn stream(bytes: &[u8]) -> String {
512 bytes.iter().map(|b| char::from(*b)).collect()
515 }
516
517 #[test]
518 fn every_binary_opcode_walks_when_its_operand_fits() {
519 let cases: Vec<Vec<u8>> = vec![
522 vec![0x80, 0x04], vec![0x95, 0, 0, 0, 0, 0, 0, 0, 0], vec![0x4a, 1, 2, 3, 4], vec![0x4b, 0x7f], vec![0x4d, 0x01, 0x02], vec![0x47, 0, 0, 0, 0, 0, 0, 0, 0], vec![0x58, 3, 0, 0, 0, b'a', b'b', b'c'], vec![0x8c, 4, b'm', b'a', b'i', b'n'], vec![0x54, 2, 0, 0, 0, b'x', b'y'], vec![0x55, 2, b'x', b'y'], vec![0x42, 2, 0, 0, 0, b'x', b'y'], vec![0x8e, 2, 0, 0, 0, 0, 0, 0, 0, b'x', b'y'], vec![0x43, 2, b'x', b'y'], vec![0x96, 1, 0, 0, 0, 0, 0, 0, 0, b'z'], vec![0x8a, 0x01, b'x'], vec![0x8b, 1, 0, 0, 0, b'q'], ];
539 for mut case in cases {
540 case.push(b'R'); let text = stream(&case);
542 assert!(
543 pickle_suffix_reaches_reduce_or_build(&text),
544 "stream should walk clean: {case:?}"
545 );
546 }
547 }
548
549 #[test]
550 fn truncated_operand_streams_are_tolerated_like_the_reference() {
551 assert!(pickle_suffix_reaches_reduce_or_build(&stream(&[
553 0x4a, 1, 2
554 ])));
555 assert!(pickle_prefix_is_opcode_stream(&stream(&[0x58, 9, 9, 9])));
556 assert!(pickle_suffix_reaches_reduce_or_build(&stream(&[
557 0x95, 0, 0
558 ])));
559 }
560
561 #[test]
562 fn text_opcodes_accept_their_reference_shapes() {
563 let cases: Vec<Vec<u8>> = vec![
564 vec![b'I', b'0', b'1', 0x0a], vec![b'I', b'0', b'0', 0x0a], vec![b'I', b'-', b'4', b'2', 0x0a], vec![b'I', b'7', 0x0a], vec![b'L', b'-', b'9', b'9', 0x0a], vec![b'L', b'1', b'2', b'L', 0x0a], vec![b'L', b'5', 0x0a], vec![b'F', b'1', b'.', b'5', 0x0a], vec![b'F', b'-', b'0', b'.', b'5', 0x0a], vec![b'S', b'\'', b'a', b'\'', 0x0a], vec![b'S', b'"', b'a', b'"', 0x0a], vec![b'V', b'u', b'n', b'i', 0x0a], vec![b'N'], vec![0x89], vec![0x88], ];
580 for mut case in cases {
581 case.push(b'R'); let text = stream(&case);
583 let label = String::from_utf8_lossy(&case);
584 assert!(
585 pickle_suffix_reaches_reduce_or_build(&text),
586 "stream should walk clean: {label}"
587 );
588 }
589 }
590
591 #[test]
592 fn malformed_text_opcodes_are_blocked() {
593 let cases: Vec<Vec<u8>> = vec![
594 vec![b'I', b'x', b'y', 0x0a], vec![b'L', b'x', 0x0a], vec![b'F', b'x', b'y', 0x0a], vec![b'S', b'a', b'b', 0x0a], vec![b'S', b'\'', b'a', b'"', 0x0a], vec![b'S', b'\'', 0x0a], ];
601 for case in cases {
602 let text = stream(&case);
603 let label = String::from_utf8_lossy(&case);
604 assert!(
605 !pickle_suffix_reaches_reduce_or_build(&text),
606 "stream should be blocked: {label}"
607 );
608 }
609 }
610
611 #[test]
612 fn stack_and_memo_opcodes_walk_in_valid_sequences() {
613 let cases: Vec<Vec<u8>> = vec![
614 vec![0x28, 0x30], vec![0x28, b'N', 0x31], vec![b'N', 0x32], vec![0x5d], vec![0x7d], vec![0x29], vec![0x28, b'N', b'N', b'l'], vec![0x28, b'N', b'N', b't'], vec![0x28, b'N', b'N', b'd'], vec![b'N', b'N', b'a'], vec![0x28, b'N', b'N', b'e'], vec![b'N', b'N', b's'], vec![0x28, b'N', b'N', b'u'], vec![b'N', 0x94], vec![b'N', b'q', 0x05], vec![b'N', b'r', 1, 0, 0, 0], vec![b'N', 0x94, b'h', 0x00], vec![b'g', b'0', 0x0a], ];
633 for case in cases {
634 let text = stream(&case);
635 let _ = pickle_suffix_reaches_reduce_or_build(&text);
638 }
639 let hit = stream(&[b'N', 0x94, b'N', b'q', 0x00, b'h', 0x00, b'R']);
641 assert!(pickle_suffix_reaches_reduce_or_build(&hit));
642 let empty_stack = stream(&[b'q', 0x00]);
644 assert!(!pickle_suffix_reaches_reduce_or_build(&empty_stack));
645 let long_empty = stream(&[b'r', 0, 0, 0, 0]);
647 assert!(!pickle_suffix_reaches_reduce_or_build(&long_empty));
648 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[0x94])));
650 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[
652 b'h', 0x07
653 ])));
654 let seeded = stream(&[b'N', b'q', 0x03, b'h', 0x03, b'R']);
655 assert!(pickle_suffix_reaches_reduce_or_build(&seeded));
656 let long_hit = stream(&[b'N', b'q', 0x03, b'j', 3, 0, 0, 0, b'R']);
658 assert!(pickle_suffix_reaches_reduce_or_build(&long_hit));
659 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[
661 b'g', b'x', 0x0a
662 ])));
663 }
664
665 #[test]
666 fn class_resolution_and_unknown_opcodes_are_blocked() {
667 for key in [
668 0x63u8, 0x69, 0x6f, 0x81, 0x82, 0x93, 0x50, 0x51, 0x84, 0x85, 0x86, 0x01,
669 ] {
670 assert!(
671 !pickle_suffix_reaches_reduce_or_build(&stream(&[key])),
672 "opcode {key:#x} must be blocked"
673 );
674 }
675 }
676
677 #[test]
678 fn stack_underflows_are_blocked() {
679 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[0x30])));
681 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[0x31])));
682 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[0x32])));
683 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(b"a")));
684 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(b"s")));
685 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(b"e")));
687 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(b"u")));
688 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(b"l")));
690 }
691
692 #[test]
693 fn non_byte_safe_input_is_rejected() {
694 assert!(!pickle_prefix_is_opcode_stream("\u{5000}"));
697 assert!(!pickle_suffix_reaches_reduce_or_build("\u{5000}"));
698 }
699
700 #[test]
701 fn surrogate_escapes_map_back_to_high_bytes() {
702 let direct =
705 pickle_suffix_reaches_reduce_or_build(&stream(&[0x95, 0, 0, 0, 0, 0, 0, 0, 0, b'R']));
706 assert!(direct);
707 let mut escape_bytes: Vec<u8> = vec![0x95, 0, 0, 0, 0, 0, 0, 0, 0];
708 escape_bytes.push(b'R');
709 let via_escape = pickle_suffix_reaches_reduce_or_build(&stream(&escape_bytes));
710 assert!(via_escape);
711 }
712
713 #[test]
714 fn oversize_windows_budget_their_work() {
715 let big = vec![0x4bu8; 5000];
718 assert!(pickle_prefix_is_opcode_stream(&stream(&big)));
719 assert!(pickle_suffix_reaches_reduce_or_build(&stream(&big)));
720 }
721
722 #[test]
723 fn the_global_candidate_detector_needs_prefix_and_suffix() {
724 let candidate = super::super::pyregex::Candidate { start: 0, end: 3 };
725 assert!(!pickle_global_candidate_is_injection(
727 "abccos\nsystem\n(R",
728 candidate,
729 9
730 ));
731 let mut prefix = vec![0x80u8, 0x04, 0x95];
733 prefix.extend_from_slice(&[0x00; 8]);
734 prefix.extend_from_slice(&[0x8c, 0x04, b'm', b'a', b'i', b'n']);
735 let mut haystack = stream(&prefix);
736 haystack.push_str("cos\nsystem\nR");
737 let candidate = super::super::pyregex::Candidate {
738 start: haystack.find("cos").expect("candidate"),
739 end: haystack.find("system").expect("group"),
740 };
741 let group_one_end = haystack.rfind('\n').expect("newline") + 1;
742 assert!(pickle_global_candidate_is_injection(
743 &haystack,
744 candidate,
745 group_one_end
746 ));
747 }
748}
749
750#[cfg(test)]
751mod coverage_tests {
752 use super::*;
753 use crate::patterns::pyregex::Candidate;
754
755 #[test]
756 fn opcode_stream_walks_reject_blocked_and_short_streams() {
757 assert!(!pickle_prefix_is_opcode_stream("\u{94}"));
759 assert!(!pickle_prefix_is_opcode_stream("\u{71}a"));
761 assert!(!pickle_prefix_is_opcode_stream("\u{72}aaaa"));
763 assert!(!pickle_prefix_is_opcode_stream("\u{68}a"));
765 assert!(pickle_prefix_is_opcode_stream("\u{95}04"));
767 assert!(!pickle_prefix_is_opcode_stream("\u{30}"));
770 assert!(!pickle_prefix_is_opcode_stream("\u{32}"));
771 assert!(!pickle_prefix_is_opcode_stream("cos\nfoo\n("));
773 assert!(!pickle_prefix_is_opcode_stream("\u{ff}"));
774 assert!(pickle_prefix_is_opcode_stream("g"));
775 assert!(!pickle_prefix_is_opcode_stream(
777 "\u{6a}\u{0}\u{0}\u{0}\u{0}"
778 ));
779 }
780
781 #[test]
782 fn suffix_walk_stops_at_reduce_or_build() {
783 assert!(pickle_suffix_reaches_reduce_or_build("\u{72}"));
785 assert!(!pickle_suffix_reaches_reduce_or_build("\u{94}"));
787 assert!(pickle_suffix_reaches_reduce_or_build("\u{95}04"));
789 assert!(!pickle_suffix_reaches_reduce_or_build("\u{1234}"));
791 }
792
793 #[test]
794 fn non_byte_safe_windows_reject_the_candidate() {
795 assert!(!pickle_prefix_is_opcode_stream("\u{1234}"));
797 assert!(!pickle_suffix_reaches_reduce_or_build("\u{1234}"));
798 }
799
800 #[test]
801 fn global_candidate_gate_composes_prefix_and_suffix() {
802 let haystack = "\u{80}04cos\nsystem\n].";
803 assert!(!pickle_global_candidate_is_injection(
805 haystack,
806 Candidate::new(0, 4),
807 4
808 ));
809 let haystack = "cos\n]\u{72}tR";
811 assert!(pickle_global_candidate_is_injection(
812 haystack,
813 Candidate::new(0, 4),
814 4
815 ));
816 assert!(!pickle_global_candidate_is_injection(
818 "\u{94}cos\n]\u{72}tR",
819 Candidate::new(0, 4),
820 4
821 ));
822 }
823}
824
825#[cfg(test)]
826mod gap_tests {
827 use super::*;
828 use crate::patterns::pyregex::Candidate;
829
830 fn stream(bytes: &[u8]) -> String {
833 bytes.iter().map(|b| char::from(*b)).collect()
834 }
835
836 #[test]
837 fn short_read_operand_arms_are_tolerated_like_the_reference() {
838 let cases: Vec<Vec<u8>> = vec![
841 vec![0x4b], vec![0x4d, 1], vec![0x47, 1, 2, 3], vec![b'I', b'1'], vec![b'L', b'1'], vec![b'F', b'1'], vec![b'S', b'\'', b'a'], vec![b'V', b'x'], vec![0x58, 9, 0, 0, 0, b'a'], vec![0x8c, 9, b'a'], vec![0x54, 9, 0, 0, 0, b'a'], vec![0x55, 9, b'a'], vec![0x42, 9, 0, 0, 0, b'a'], vec![0x8e, 9, 0, 0, 0, 0, 0, 0, 0], vec![0x43, 9, b'a'], vec![0x96, 9, 0, 0, 0, 0, 0, 0, 0], vec![0x8a, 9, b'a'], vec![0x8b, 9, 0, 0, 0, b'a'], vec![0x80], vec![0x71], vec![0x68], vec![0x6a, 1, 2], ];
864 for key in [0x58u8, 0x8c, 0x54, 0x55, 0x42, 0x8e, 0x43, 0x96, 0x8a, 0x8b] {
866 assert!(
867 pickle_suffix_reaches_reduce_or_build(&stream(&[key])),
868 "a missing length operand for {key:#x} is tolerated"
869 );
870 }
871 for case in cases {
872 let text = stream(&case);
873 let label = String::from_utf8_lossy(&case);
874 assert!(
875 pickle_suffix_reaches_reduce_or_build(&text),
876 "a truncated {label} operand is tolerated"
877 );
878 }
879 }
880
881 #[test]
882 fn blocked_opcode_arms_report_the_reference_verdicts() {
883 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(b"Ns")));
886 assert!(!pickle_prefix_is_opcode_stream("\u{73}"));
887 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[
889 b'F', 0xFF, 0x0a
890 ])));
891 assert!(!pickle_suffix_reaches_reduce_or_build(&stream(&[
893 b'g', 0xFF, 0x0a
894 ])));
895 }
896
897 #[test]
898 fn get_reads_a_memo_entry_seeded_by_binput() {
899 let text = stream(&[b'N', b'q', 0x00, b'g', b'0', 0x0a, b'R']);
902 assert!(pickle_suffix_reaches_reduce_or_build(&text));
903 }
904
905 #[test]
906 fn a_non_stream_prefix_rejects_the_global_candidate() {
907 assert!(!pickle_global_candidate_is_injection(
909 "helloX",
910 Candidate::new(5, 6),
911 6
912 ));
913 }
914}