1use std::borrow::Cow;
31
32use vyre::{BackendError, DispatchConfig};
33
34const U32_COUNTER_BYTES: usize = 4;
35const MATCH_TRIPLE_BYTES: usize = 12;
36
37#[derive(Debug, Default)]
44pub struct ScanDispatchScratch {
45 pub haystack_bytes: Vec<u8>,
47 pub hit_bytes: Vec<u8>,
49}
50
51#[must_use]
65pub fn pack_haystack_u32(haystack: &[u8]) -> Vec<u8> {
66 match try_pack_haystack_u32(haystack) {
67 Ok(packed) => packed,
68 Err(error) => {
69 panic!(
70 "vyre-libs scan dispatch pack_haystack_u32 failed: {error}. \
71 returning an empty packed buffer would make the GPU scan an empty haystack and silently report the input as clean; \
72 use try_pack_haystack_u32 and split the haystack before dispatch."
73 )
74 }
75 }
76}
77
78pub fn try_pack_haystack_u32(haystack: &[u8]) -> Result<Vec<u8>, BackendError> {
84 let mut packed = Vec::new();
85 pack_haystack_u32_into(haystack, &mut packed)?;
86 Ok(packed)
87}
88
89pub fn pack_haystack_u32_into(haystack: &[u8], packed: &mut Vec<u8>) -> Result<(), BackendError> {
98 let padded_len = haystack_padded_u32_byte_len(haystack.len())?;
99 packed.clear();
100 vyre_foundation::allocation::try_reserve_vec_to_capacity(packed, padded_len).map_err(
101 |source| {
102 BackendError::new(format!(
103 "scan dispatch could not reserve {padded_len} packed haystack byte(s): {source}. Fix: split the haystack before dispatch."
104 ))
105 },
106 )?;
107 packed.extend_from_slice(haystack);
108 packed.resize(padded_len, 0);
109 Ok(())
110}
111
112pub fn haystack_padded_u32_byte_len(byte_len: usize) -> Result<usize, BackendError> {
116 byte_len
117 .checked_add(3)
118 .map(|len| (len / 4) * 4)
119 .ok_or_else(|| {
120 BackendError::new(
121 "scan dispatch haystack padding overflows host usize. Fix: split the haystack before dispatch.",
122 )
123 })
124}
125
126#[cfg(test)]
127mod scratch_reuse_tests {
128 use super::{
129 haystack_padded_u32_byte_len, pack_haystack_u32, pack_haystack_u32_into,
130 try_pack_haystack_u32, ScanDispatchScratch,
131 };
132
133 #[test]
134 fn pack_haystack_into_reuses_capacity_and_matches_owned_helper() {
135 let mut scratch = ScanDispatchScratch::default();
136 pack_haystack_u32_into(b"abcdef", &mut scratch.haystack_bytes)
137 .expect("Fix: packed haystack scratch should reserve");
138 let retained = scratch.haystack_bytes.capacity();
139 assert_eq!(scratch.haystack_bytes, pack_haystack_u32(b"abcdef"));
140
141 pack_haystack_u32_into(b"xy", &mut scratch.haystack_bytes)
142 .expect("Fix: smaller packed haystack should reuse scratch");
143
144 assert_eq!(scratch.haystack_bytes, vec![b'x', b'y', 0, 0]);
145 assert!(scratch.haystack_bytes.capacity() >= retained);
146 }
147
148 #[test]
149 fn try_pack_haystack_owned_matches_compat_helper() {
150 let packed = try_pack_haystack_u32(b"abcde")
151 .expect("Fix: small owned haystack packing must reserve");
152
153 assert_eq!(packed, pack_haystack_u32(b"abcde"));
154 assert_eq!(packed, vec![b'a', b'b', b'c', b'd', b'e', 0, 0, 0]);
155 }
156
157 #[test]
158 fn haystack_padding_overflow_reports_split_fix() {
159 let error = haystack_padded_u32_byte_len(usize::MAX)
160 .expect_err("Fix: usize::MAX padding must overflow instead of wrapping");
161 let message = format!("{error}");
162
163 assert!(message.contains("padding overflows host usize"));
164 assert!(message.contains("Fix: split the haystack"));
165 }
166}
167
168#[must_use]
171pub fn pack_u32_slice(words: &[u32]) -> Vec<u8> {
172 vyre_primitives::wire::pack_u32_slice(words)
173}
174
175#[must_use]
178pub fn u32_words_as_le_bytes(words: &[u32]) -> Cow<'_, [u8]> {
179 if cfg!(target_endian = "little") {
180 Cow::Borrowed(bytemuck::cast_slice(words))
181 } else {
182 Cow::Owned(pack_u32_slice(words))
183 }
184}
185
186pub fn haystack_len_u32(haystack: &[u8], context: &str) -> Result<u32, BackendError> {
198 u32::try_from(haystack.len()).map_err(|_| {
199 BackendError::new(format!(
200 "{context} haystack length exceeds u32 capacity. \
201 Fix: split the scan into chunks smaller than 4 GiB."
202 ))
203 })
204}
205
206pub const DEFAULT_MAX_SCAN_BYTES: u32 = 1 << 30;
213
214pub fn scan_guard(haystack: &[u8], context: &str, max_bytes: u32) -> Result<u32, BackendError> {
231 let len = haystack_len_u32(haystack, context)?;
232 if len > max_bytes {
233 return Err(BackendError::new(format!(
234 "{context} haystack length {len} bytes exceeds scan-guard ceiling {max_bytes} bytes. \
235 Fix: split the scan into chunks <= {max_bytes} bytes, or pass a larger \
236 max_bytes if the larger dispatch is intentional."
237 )));
238 }
239 Ok(len)
240}
241
242#[must_use]
247pub fn byte_scan_dispatch_config(haystack_len: u32, workgroup_x: u32) -> DispatchConfig {
248 let mut config = DispatchConfig::default();
249 let workgroups = haystack_len.div_ceil(workgroup_x.max(1)).max(1);
250 config.grid_override = Some([workgroups, 1, 1]);
251 config.dispatch_elements = Some(haystack_len);
255 config
256}
257
258#[must_use]
263pub fn candidate_start_dispatch_config(haystack_len: u32) -> DispatchConfig {
264 let mut config = DispatchConfig::default();
265 config.grid_override = Some([haystack_len.max(1), 1, 1]);
266 config.dispatch_elements = Some(haystack_len);
268 config
269}
270
271pub fn try_read_u32_prefix(bytes: &[u8], field: &'static str) -> Result<u32, BackendError> {
278 if bytes.len() < U32_COUNTER_BYTES {
279 return Err(BackendError::new(format!(
280 "scan dispatch {field} was {} byte(s) but a u32 counter requires {U32_COUNTER_BYTES} bytes. Fix: preserve the counter output byte range before decoding scan results.",
281 bytes.len()
282 )));
283 }
284
285 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
286}
287
288pub fn try_output_bytes<'a>(
294 outputs: &'a [Vec<u8>],
295 index: usize,
296 field: &'static str,
297) -> Result<&'a [u8], BackendError> {
298 outputs.get(index).map(Vec::as_slice).ok_or_else(|| {
299 BackendError::new(format!(
300 "scan dispatch missing {field} at output index {index}; backend returned {} output buffer(s). Fix: preserve Program output declaration order and return every declared output buffer.",
301 outputs.len()
302 ))
303 })
304}
305
306#[must_use]
323pub fn unpack_match_triples(
324 triples_bytes: &[u8],
325 count: u32,
326) -> Vec<vyre_foundation::match_result::Match> {
327 match try_unpack_match_triples(triples_bytes, count) {
328 Ok(results) => results,
329 Err(error) => {
330 panic!(
334 "vyre-libs scan dispatch unpack_match_triples failed: {error}. \
335 returning an empty match set would silently drop matches the GPU found; \
336 use try_unpack_match_triples and reduce the match count or reserve more storage."
337 )
338 }
339 }
340}
341
342pub fn try_unpack_match_triples(
348 triples_bytes: &[u8],
349 count: u32,
350) -> Result<Vec<vyre_foundation::match_result::Match>, BackendError> {
351 let mut results = Vec::new();
352 try_unpack_match_triples_into(triples_bytes, count, &mut results)?;
353 Ok(results)
354}
355
356pub fn unpack_match_triples_into(
370 triples_bytes: &[u8],
371 count: u32,
372 results: &mut Vec<vyre_foundation::match_result::Match>,
373) {
374 if let Err(error) = try_unpack_match_triples_into(triples_bytes, count, results) {
375 panic!(
379 "vyre-libs scan dispatch unpack_match_triples_into failed: {error}. \
380 clearing the result buffer would silently drop matches the GPU found; \
381 use try_unpack_match_triples_into and reduce the match count or reserve more storage."
382 )
383 }
384}
385
386pub fn try_unpack_match_triples_into(
392 triples_bytes: &[u8],
393 count: u32,
394 results: &mut Vec<vyre_foundation::match_result::Match>,
395) -> Result<(), BackendError> {
396 let n = decoded_match_triple_count(triples_bytes, count);
397 vyre_foundation::allocation::try_reserve_vec_to_capacity(results, n).map_err(|source| {
398 BackendError::new(format!(
399 "scan dispatch could not reserve {n} decoded match record(s): {source}. Fix: lower max_matches or split the scan before dispatch."
400 ))
401 })?;
402 results.clear();
403 for i in 0..n {
404 let off = i * 12;
405 let pid = u32::from_le_bytes([
406 triples_bytes[off],
407 triples_bytes[off + 1],
408 triples_bytes[off + 2],
409 triples_bytes[off + 3],
410 ]);
411 let start = u32::from_le_bytes([
412 triples_bytes[off + 4],
413 triples_bytes[off + 5],
414 triples_bytes[off + 6],
415 triples_bytes[off + 7],
416 ]);
417 let end = u32::from_le_bytes([
418 triples_bytes[off + 8],
419 triples_bytes[off + 9],
420 triples_bytes[off + 10],
421 triples_bytes[off + 11],
422 ]);
423 results.push(vyre_foundation::match_result::Match::new(pid, start, end));
424 }
425 results.sort_unstable();
426 Ok(())
427}
428
429pub fn try_unpack_match_triples_exact_prefix_into(
450 triples_bytes: &[u8],
451 count: u32,
452 results: &mut Vec<vyre_foundation::match_result::Match>,
453) -> Result<(), BackendError> {
454 results.clear();
455 let required = required_match_triple_bytes(count)?;
456 if triples_bytes.len() < required {
457 return Err(BackendError::new(format!(
458 "scan dispatch match triples readback was {} byte(s) but count={count} requires {required} byte(s). Fix: preserve the output byte range for the requested match cap before decoding scan results.",
459 triples_bytes.len()
460 )));
461 }
462 try_unpack_match_triples_into(triples_bytes, count, results)
463}
464
465pub fn try_unpack_match_triples_capped_into(
487 triples_bytes: &[u8],
488 count: u32,
489 cap: u32,
490 context: &str,
491 results: &mut Vec<vyre_foundation::match_result::Match>,
492) -> Result<(), BackendError> {
493 if count > cap {
494 results.clear();
495 return Err(BackendError::new(format!(
496 "{context}: GPU match count {count} exceeds the output-buffer cap {cap}; decoding would silently drop {} match(es). Fix: raise the match cap (max_matches) or split the scan before dispatch.",
497 count - cap
498 )));
499 }
500 try_unpack_match_triples_exact_prefix_into(triples_bytes, count, results)
501}
502
503#[inline]
504fn decoded_match_triple_count(triples_bytes: &[u8], count: u32) -> usize {
505 let max_complete = triples_bytes.len() / MATCH_TRIPLE_BYTES;
506 let requested = match usize::try_from(count) {
507 Ok(requested) => requested,
508 Err(_) => usize::MAX,
509 };
510 requested.min(max_complete)
511}
512
513fn required_match_triple_bytes(count: u32) -> Result<usize, BackendError> {
514 let n = usize::try_from(count).map_err(|source| {
515 BackendError::new(format!(
516 "scan dispatch match count does not fit host usize: {source}. Fix: lower max_matches or split the scan before dispatch."
517 ))
518 })?;
519 n.checked_mul(MATCH_TRIPLE_BYTES).ok_or_else(|| {
520 BackendError::new(
521 "scan dispatch match triple byte count overflowed host usize. Fix: lower max_matches or split the scan before dispatch.",
522 )
523 })
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn dispatch_io_wrappers_fail_loud_not_silent_fallback() {
532 let src = include_str!("dispatch_io.rs");
542 let swallow_marker = concat!("eprintln", "!(\"vyre-libs scan dispatch ");
543 assert!(
544 !src.contains(swallow_marker),
545 "Fix: a dispatch wrapper reintroduced an eprintln!-then-return-empty silent fallback (Law 10) (fail loud via panic!() so callers use the try_ variants)."
546 );
547 }
548
549 #[test]
550 fn capped_decode_fails_closed_when_count_exceeds_cap() {
551 let mut triples = Vec::new();
557 for i in 0..4u32 {
558 triples.extend_from_slice(&i.to_le_bytes()); triples.extend_from_slice(&i.to_le_bytes()); triples.extend_from_slice(&(i + 2).to_le_bytes()); }
562 let mut results = vec![vyre_foundation::match_result::Match::new(7, 7, 7)];
563 let err =
564 try_unpack_match_triples_capped_into(&triples, 9, 4, "unit cap test", &mut results)
565 .expect_err("count 9 over cap 4 must fail closed, not truncate");
566 let msg = err.to_string();
567 assert!(
568 msg.contains("unit cap test")
569 && msg.contains("exceeds the output-buffer cap 4")
570 && msg.contains("drop 5 match(es)"),
571 "error must name the context, cap, and dropped count: {msg}"
572 );
573 assert!(
574 results.is_empty(),
575 "a failed capped decode must expose no partial matches, got {results:?}"
576 );
577 }
578
579 #[test]
580 fn capped_decode_passes_and_decodes_exactly_within_cap() {
581 let mut triples = Vec::new();
584 for i in 0..4u32 {
585 triples.extend_from_slice(&(i + 10).to_le_bytes()); triples.extend_from_slice(&i.to_le_bytes()); triples.extend_from_slice(&(i + 1).to_le_bytes()); }
589 let mut results = Vec::new();
590 try_unpack_match_triples_capped_into(&triples, 3, 4, "unit within-cap", &mut results)
591 .expect("count 3 within cap 4 must decode");
592 assert_eq!(
593 results,
594 vec![
595 vyre_foundation::match_result::Match::new(10, 0, 1),
596 vyre_foundation::match_result::Match::new(11, 1, 2),
597 vyre_foundation::match_result::Match::new(12, 2, 3),
598 ],
599 "within-cap decode must yield exactly the first `count` triples"
600 );
601 let mut at_cap = Vec::new();
603 try_unpack_match_triples_capped_into(&triples, 4, 4, "unit at-cap", &mut at_cap)
604 .expect("count == cap is not an overflow");
605 assert_eq!(at_cap.len(), 4, "count == cap must decode all four");
606 }
607
608 #[test]
609 fn pack_haystack_aligned() {
610 let bytes = b"abcdefgh";
611 let packed = pack_haystack_u32(bytes);
612 assert_eq!(packed, vec![0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68]);
614 }
615
616 #[test]
617 fn pack_haystack_unaligned_zero_pads() {
618 let bytes = b"abc";
619 let packed = pack_haystack_u32(bytes);
620 assert_eq!(packed, vec![0x61, 0x62, 0x63, 0x00]);
622 }
623
624 #[test]
625 fn pack_haystack_empty() {
626 assert!(pack_haystack_u32(&[]).is_empty());
627 }
628
629 #[test]
630 fn pack_u32_slice_layout() {
631 let words: [u32; 2] = [0x01020304, 0xAABBCCDD];
632 assert_eq!(
633 pack_u32_slice(&words),
634 vec![0x04, 0x03, 0x02, 0x01, 0xDD, 0xCC, 0xBB, 0xAA]
635 );
636 }
637
638 #[test]
639 fn u32_words_as_le_bytes_matches_pack_layout() {
640 let words: [u32; 2] = [0x01020304, 0xAABBCCDD];
641 let bytes = u32_words_as_le_bytes(&words);
642 assert_eq!(
643 bytes.as_ref(),
644 [0x04, 0x03, 0x02, 0x01, 0xDD, 0xCC, 0xBB, 0xAA]
645 );
646 if cfg!(target_endian = "little") {
647 assert!(matches!(bytes, std::borrow::Cow::Borrowed(_)));
648 }
649 }
650
651 #[test]
652 fn haystack_len_under_4gib_ok() {
653 let buf = vec![0u8; 1024];
654 assert_eq!(haystack_len_u32(&buf, "test").unwrap(), 1024);
655 }
656
657 #[test]
658 fn scan_guard_under_ceiling_ok() {
659 let buf = vec![0u8; 1024];
660 assert_eq!(
661 scan_guard(&buf, "test", DEFAULT_MAX_SCAN_BYTES).unwrap(),
662 1024
663 );
664 }
665
666 #[test]
667 fn scan_guard_over_ceiling_errors() {
668 let buf = vec![0u8; 1024];
669 let err = scan_guard(&buf, "test", 512).expect_err("over ceiling must err");
670 let msg = format!("{err}");
671 assert!(
672 msg.contains("scan-guard ceiling"),
673 "scan_guard error must name the ceiling, got: {msg}"
674 );
675 assert!(
676 msg.contains("512"),
677 "must echo the ceiling number, got: {msg}"
678 );
679 }
680
681 #[test]
682 fn scan_guard_zero_ceiling_rejects_nonempty() {
683 let buf = vec![0u8; 1];
684 let err = scan_guard(&buf, "ctx", 0).expect_err("nonempty haystack with zero ceiling");
685 let msg = err.to_string();
686 assert!(
687 msg.contains("scan-guard ceiling") && msg.contains('0'),
688 "zero-ceiling rejection must name the ceiling: {msg}"
689 );
690 }
691
692 #[test]
693 fn scan_guard_zero_ceiling_accepts_empty() {
694 let buf: Vec<u8> = vec![];
695 assert_eq!(scan_guard(&buf, "ctx", 0).unwrap(), 0);
696 }
697
698 #[test]
699 fn scan_guard_at_max_u32_ceiling_accepts_real_inputs() {
700 let buf = vec![0u8; 1 << 16];
701 assert_eq!(scan_guard(&buf, "ctx", u32::MAX).unwrap(), 1 << 16);
702 }
703
704 #[test]
705 fn dispatch_config_clamps_at_one() {
706 let cfg = byte_scan_dispatch_config(0, 64);
709 assert_eq!(cfg.grid_override, Some([1, 1, 1]));
710 }
711
712 #[test]
713 fn dispatch_config_divceils() {
714 let cfg = byte_scan_dispatch_config(129, 64);
715 assert_eq!(cfg.grid_override, Some([3, 1, 1]));
716 }
717
718 #[test]
719 fn byte_scan_config_carries_true_element_coverage() {
720 assert_eq!(
726 byte_scan_dispatch_config(129, 64).dispatch_elements,
727 Some(129)
728 );
729 assert_eq!(byte_scan_dispatch_config(0, 64).dispatch_elements, Some(0));
730 assert_eq!(
731 candidate_start_dispatch_config(65_536).dispatch_elements,
732 Some(65_536)
733 );
734 assert_eq!(DispatchConfig::default().dispatch_elements, None);
737 }
738
739 #[test]
740 fn unpack_match_triples_sorts() {
741 let bytes = [
742 2, 0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0,
745 ];
746 let matches = unpack_match_triples(&bytes, 2);
747 assert_eq!(matches.len(), 2);
748 assert!(matches[0].start <= matches[1].start);
750 }
751
752 #[test]
753 fn unpack_match_triples_into_reuses_caller_buffer() {
754 let bytes = [
755 2, 0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0,
756 ];
757 let mut matches = Vec::with_capacity(8);
758 let ptr = matches.as_ptr();
759
760 unpack_match_triples_into(&bytes, 2, &mut matches);
761
762 assert_eq!(matches.len(), 2);
763 assert_eq!(matches.as_ptr(), ptr);
764 assert!(matches[0].start <= matches[1].start);
765 }
766
767 #[test]
768 fn try_unpack_match_triples_into_keeps_fallible_hot_path_reusable() {
769 let bytes = [
770 9, 0, 0, 0, 40, 0, 0, 0, 44, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0,
771 ];
772 let mut matches = Vec::with_capacity(4);
773 let ptr = matches.as_ptr();
774
775 try_unpack_match_triples_into(&bytes, 2, &mut matches)
776 .expect("Fix: small decoded match buffer must reserve");
777
778 assert_eq!(matches.len(), 2);
779 assert_eq!(matches.as_ptr(), ptr);
780 assert_eq!(matches[0].pattern_id, 3);
781 assert_eq!(matches[1].pattern_id, 9);
782 }
783
784 #[test]
785 fn try_unpack_match_triples_owned_matches_compat_helper() {
786 let bytes = [
787 5, 0, 0, 0, 11, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 7, 0, 0, 0,
788 ];
789
790 assert_eq!(
791 try_unpack_match_triples(&bytes, 2)
792 .expect("Fix: small decoded match buffer must reserve"),
793 unpack_match_triples(&bytes, 2)
794 );
795 }
796
797 #[test]
798 fn read_u32_prefix_decodes_counter_and_rejects_short_readback() {
799 assert_eq!(
800 try_read_u32_prefix(&[0x34, 0x12, 0, 0, 0xAA], "test counter")
801 .expect("Fix: four-byte counter prefix must decode"),
802 0x1234
803 );
804
805 let err = try_read_u32_prefix(&[1, 2, 3], "test counter")
806 .expect_err("short scan counter readback must fail closed");
807 let msg = err.to_string();
808 assert!(
809 msg.contains("test counter")
810 && msg.contains("3 byte(s)")
811 && msg.contains("requires 4 bytes"),
812 "short counter error must name the field and required length: {msg}"
813 );
814 }
815
816 #[test]
817 fn output_bytes_rejects_missing_declared_output_slot() {
818 let outputs = vec![vec![1, 2, 3, 4]];
819 assert_eq!(
820 try_output_bytes(&outputs, 0, "first").expect("Fix: present output slot must borrow"),
821 &[1, 2, 3, 4]
822 );
823
824 let err = try_output_bytes(&outputs, 1, "matches")
825 .expect_err("missing backend output slot must fail closed");
826 let msg = err.to_string();
827 assert!(
828 msg.contains("matches")
829 && msg.contains("output index 1")
830 && msg.contains("returned 1 output buffer"),
831 "missing output error must identify the omitted slot: {msg}"
832 );
833 }
834
835 #[test]
836 fn exact_prefix_match_decode_sorts_and_reuses_caller_buffer() {
837 let bytes = [
838 9, 0, 0, 0, 40, 0, 0, 0, 44, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0, 0xAA, 0xBB,
839 ];
840 let mut matches = Vec::with_capacity(4);
841 let ptr = matches.as_ptr();
842
843 try_unpack_match_triples_exact_prefix_into(&bytes, 2, &mut matches)
844 .expect("Fix: exact two-triple prefix must decode");
845
846 assert_eq!(matches.len(), 2);
847 assert_eq!(matches.as_ptr(), ptr);
848 assert_eq!(matches[0].pattern_id, 3);
849 assert_eq!(matches[1].pattern_id, 9);
850 }
851
852 #[test]
853 fn exact_prefix_match_decode_rejects_short_payload_and_clears_results() {
854 let bytes = [
855 7u8, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, ];
859 let mut matches = vec![vyre_foundation::match_result::Match::new(99, 1, 2)];
860
861 let err = try_unpack_match_triples_exact_prefix_into(&bytes, 2, &mut matches)
862 .expect_err("short match triple readback must fail closed");
863
864 let msg = err.to_string();
865 assert!(
866 matches.is_empty(),
867 "malformed readback must clear stale matches"
868 );
869 assert!(
870 msg.contains("readback was 12 byte(s)")
871 && msg.contains("count=2")
872 && msg.contains("requires 24 byte(s)"),
873 "short match readback error must identify observed and required bytes: {msg}"
874 );
875 }
876
877 #[test]
878 fn exact_prefix_match_decode_huge_count_short_payload_fails_closed() {
879 let bytes = [
880 7u8, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, ];
884 let mut matches = vec![vyre_foundation::match_result::Match::new(99, 1, 2)];
885
886 let err = try_unpack_match_triples_exact_prefix_into(&bytes, u32::MAX, &mut matches)
887 .expect_err("huge count with short readback must fail closed");
888
889 let msg = err.to_string();
890 assert!(
891 matches.is_empty(),
892 "malformed readback must clear stale matches"
893 );
894 assert!(
895 msg.contains("requires") || msg.contains("overflowed") || msg.contains("does not fit"),
896 "huge-count error must report required size or host capacity: {msg}"
897 );
898 }
899
900 #[test]
907 fn unpack_match_triples_huge_count_short_buffer_stays_in_bounds() {
908 let bytes = [
909 7u8, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, ];
913 let matches = unpack_match_triples(&bytes, u32::MAX);
914 assert_eq!(matches.len(), 1);
915 assert_eq!(matches[0].pattern_id, 7);
916 assert_eq!(matches[0].start, 1);
917 assert_eq!(matches[0].end, 3);
918 }
919}