use std::borrow::Cow;
use vyre::{BackendError, DispatchConfig};
const U32_COUNTER_BYTES: usize = 4;
const MATCH_TRIPLE_BYTES: usize = 12;
#[derive(Debug, Default)]
pub struct ScanDispatchScratch {
pub haystack_bytes: Vec<u8>,
pub hit_bytes: Vec<u8>,
}
#[must_use]
pub fn pack_haystack_u32(haystack: &[u8]) -> Vec<u8> {
match try_pack_haystack_u32(haystack) {
Ok(packed) => packed,
Err(error) => {
eprintln!("vyre-libs scan dispatch pack_haystack_u32 failed: {error}");
Vec::new()
}
}
}
pub fn try_pack_haystack_u32(haystack: &[u8]) -> Result<Vec<u8>, BackendError> {
let mut packed = Vec::new();
pack_haystack_u32_into(haystack, &mut packed)?;
Ok(packed)
}
pub fn pack_haystack_u32_into(haystack: &[u8], packed: &mut Vec<u8>) -> Result<(), BackendError> {
let padded_len = haystack_padded_u32_byte_len(haystack.len())?;
packed.clear();
vyre_foundation::allocation::try_reserve_vec_to_capacity(packed, padded_len).map_err(
|source| {
BackendError::new(format!(
"scan dispatch could not reserve {padded_len} packed haystack byte(s): {source}. Fix: split the haystack before dispatch."
))
},
)?;
packed.extend_from_slice(haystack);
packed.resize(padded_len, 0);
Ok(())
}
pub fn haystack_padded_u32_byte_len(byte_len: usize) -> Result<usize, BackendError> {
byte_len
.checked_add(3)
.map(|len| (len / 4) * 4)
.ok_or_else(|| {
BackendError::new(
"scan dispatch haystack padding overflows host usize. Fix: split the haystack before dispatch.",
)
})
}
#[cfg(test)]
mod scratch_reuse_tests {
use super::{
haystack_padded_u32_byte_len, pack_haystack_u32, pack_haystack_u32_into,
try_pack_haystack_u32, ScanDispatchScratch,
};
#[test]
fn pack_haystack_into_reuses_capacity_and_matches_owned_helper() {
let mut scratch = ScanDispatchScratch::default();
pack_haystack_u32_into(b"abcdef", &mut scratch.haystack_bytes)
.expect("Fix: packed haystack scratch should reserve");
let retained = scratch.haystack_bytes.capacity();
assert_eq!(scratch.haystack_bytes, pack_haystack_u32(b"abcdef"));
pack_haystack_u32_into(b"xy", &mut scratch.haystack_bytes)
.expect("Fix: smaller packed haystack should reuse scratch");
assert_eq!(scratch.haystack_bytes, vec![b'x', b'y', 0, 0]);
assert!(scratch.haystack_bytes.capacity() >= retained);
}
#[test]
fn try_pack_haystack_owned_matches_compat_helper() {
let packed = try_pack_haystack_u32(b"abcde")
.expect("Fix: small owned haystack packing must reserve");
assert_eq!(packed, pack_haystack_u32(b"abcde"));
assert_eq!(packed, vec![b'a', b'b', b'c', b'd', b'e', 0, 0, 0]);
}
#[test]
fn haystack_padding_overflow_reports_split_fix() {
let error = haystack_padded_u32_byte_len(usize::MAX)
.expect_err("Fix: usize::MAX padding must overflow instead of wrapping");
let message = format!("{error}");
assert!(message.contains("padding overflows host usize"));
assert!(message.contains("Fix: split the haystack"));
}
}
#[must_use]
pub fn pack_u32_slice(words: &[u32]) -> Vec<u8> {
vyre_primitives::wire::pack_u32_slice(words)
}
#[must_use]
pub fn u32_words_as_le_bytes(words: &[u32]) -> Cow<'_, [u8]> {
if cfg!(target_endian = "little") {
Cow::Borrowed(bytemuck::cast_slice(words))
} else {
Cow::Owned(pack_u32_slice(words))
}
}
pub fn haystack_len_u32(haystack: &[u8], context: &str) -> Result<u32, BackendError> {
u32::try_from(haystack.len()).map_err(|_| {
BackendError::new(format!(
"{context} haystack length exceeds u32 capacity. \
Fix: split the scan into chunks smaller than 4 GiB."
))
})
}
pub const DEFAULT_MAX_SCAN_BYTES: u32 = 1 << 30;
pub fn scan_guard(haystack: &[u8], context: &str, max_bytes: u32) -> Result<u32, BackendError> {
let len = haystack_len_u32(haystack, context)?;
if len > max_bytes {
return Err(BackendError::new(format!(
"{context} haystack length {len} bytes exceeds scan-guard ceiling {max_bytes} bytes. \
Fix: split the scan into chunks <= {max_bytes} bytes, or pass a larger \
max_bytes if the larger dispatch is intentional."
)));
}
Ok(len)
}
#[must_use]
pub fn byte_scan_dispatch_config(haystack_len: u32, workgroup_x: u32) -> DispatchConfig {
let mut config = DispatchConfig::default();
let workgroups = haystack_len.div_ceil(workgroup_x.max(1)).max(1);
config.grid_override = Some([workgroups, 1, 1]);
config
}
#[must_use]
pub fn candidate_start_dispatch_config(haystack_len: u32) -> DispatchConfig {
let mut config = DispatchConfig::default();
config.grid_override = Some([haystack_len.max(1), 1, 1]);
config
}
pub fn try_read_u32_prefix(bytes: &[u8], field: &'static str) -> Result<u32, BackendError> {
if bytes.len() < U32_COUNTER_BYTES {
return Err(BackendError::new(format!(
"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.",
bytes.len()
)));
}
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
pub fn try_output_bytes<'a>(
outputs: &'a [Vec<u8>],
index: usize,
field: &'static str,
) -> Result<&'a [u8], BackendError> {
outputs.get(index).map(Vec::as_slice).ok_or_else(|| {
BackendError::new(format!(
"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.",
outputs.len()
))
})
}
#[must_use]
pub fn unpack_match_triples(
triples_bytes: &[u8],
count: u32,
) -> Vec<vyre_foundation::match_result::Match> {
match try_unpack_match_triples(triples_bytes, count) {
Ok(results) => results,
Err(error) => {
eprintln!("vyre-libs scan dispatch unpack_match_triples failed: {error}");
Vec::new()
}
}
}
pub fn try_unpack_match_triples(
triples_bytes: &[u8],
count: u32,
) -> Result<Vec<vyre_foundation::match_result::Match>, BackendError> {
let mut results = Vec::new();
try_unpack_match_triples_into(triples_bytes, count, &mut results)?;
Ok(results)
}
pub fn unpack_match_triples_into(
triples_bytes: &[u8],
count: u32,
results: &mut Vec<vyre_foundation::match_result::Match>,
) {
if let Err(error) = try_unpack_match_triples_into(triples_bytes, count, results) {
eprintln!("vyre-libs scan dispatch unpack_match_triples_into failed: {error}");
results.clear();
}
}
pub fn try_unpack_match_triples_into(
triples_bytes: &[u8],
count: u32,
results: &mut Vec<vyre_foundation::match_result::Match>,
) -> Result<(), BackendError> {
let n = decoded_match_triple_count(triples_bytes, count);
vyre_foundation::allocation::try_reserve_vec_to_capacity(results, n).map_err(|source| {
BackendError::new(format!(
"scan dispatch could not reserve {n} decoded match record(s): {source}. Fix: lower max_matches or split the scan before dispatch."
))
})?;
results.clear();
for i in 0..n {
let off = i * 12;
let pid = u32::from_le_bytes([
triples_bytes[off],
triples_bytes[off + 1],
triples_bytes[off + 2],
triples_bytes[off + 3],
]);
let start = u32::from_le_bytes([
triples_bytes[off + 4],
triples_bytes[off + 5],
triples_bytes[off + 6],
triples_bytes[off + 7],
]);
let end = u32::from_le_bytes([
triples_bytes[off + 8],
triples_bytes[off + 9],
triples_bytes[off + 10],
triples_bytes[off + 11],
]);
results.push(vyre_foundation::match_result::Match::new(pid, start, end));
}
results.sort_unstable();
Ok(())
}
pub fn try_unpack_match_triples_exact_prefix_into(
triples_bytes: &[u8],
count: u32,
results: &mut Vec<vyre_foundation::match_result::Match>,
) -> Result<(), BackendError> {
results.clear();
let required = required_match_triple_bytes(count)?;
if triples_bytes.len() < required {
return Err(BackendError::new(format!(
"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.",
triples_bytes.len()
)));
}
try_unpack_match_triples_into(triples_bytes, count, results)
}
#[inline]
fn decoded_match_triple_count(triples_bytes: &[u8], count: u32) -> usize {
let max_complete = triples_bytes.len() / MATCH_TRIPLE_BYTES;
let requested = match usize::try_from(count) {
Ok(requested) => requested,
Err(_) => usize::MAX,
};
requested.min(max_complete)
}
fn required_match_triple_bytes(count: u32) -> Result<usize, BackendError> {
let n = usize::try_from(count).map_err(|source| {
BackendError::new(format!(
"scan dispatch match count does not fit host usize: {source}. Fix: lower max_matches or split the scan before dispatch."
))
})?;
n.checked_mul(MATCH_TRIPLE_BYTES).ok_or_else(|| {
BackendError::new(
"scan dispatch match triple byte count overflowed host usize. Fix: lower max_matches or split the scan before dispatch.",
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pack_haystack_aligned() {
let bytes = b"abcdefgh";
let packed = pack_haystack_u32(bytes);
assert_eq!(packed, vec![0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68]);
}
#[test]
fn pack_haystack_unaligned_zero_pads() {
let bytes = b"abc";
let packed = pack_haystack_u32(bytes);
assert_eq!(packed, vec![0x61, 0x62, 0x63, 0x00]);
}
#[test]
fn pack_haystack_empty() {
assert!(pack_haystack_u32(&[]).is_empty());
}
#[test]
fn pack_u32_slice_layout() {
let words: [u32; 2] = [0x01020304, 0xAABBCCDD];
assert_eq!(
pack_u32_slice(&words),
vec![0x04, 0x03, 0x02, 0x01, 0xDD, 0xCC, 0xBB, 0xAA]
);
}
#[test]
fn u32_words_as_le_bytes_matches_pack_layout() {
let words: [u32; 2] = [0x01020304, 0xAABBCCDD];
let bytes = u32_words_as_le_bytes(&words);
assert_eq!(
bytes.as_ref(),
[0x04, 0x03, 0x02, 0x01, 0xDD, 0xCC, 0xBB, 0xAA]
);
if cfg!(target_endian = "little") {
assert!(matches!(bytes, std::borrow::Cow::Borrowed(_)));
}
}
#[test]
fn haystack_len_under_4gib_ok() {
let buf = vec![0u8; 1024];
assert_eq!(haystack_len_u32(&buf, "test").unwrap(), 1024);
}
#[test]
fn scan_guard_under_ceiling_ok() {
let buf = vec![0u8; 1024];
assert_eq!(
scan_guard(&buf, "test", DEFAULT_MAX_SCAN_BYTES).unwrap(),
1024
);
}
#[test]
fn scan_guard_over_ceiling_errors() {
let buf = vec![0u8; 1024];
let err = scan_guard(&buf, "test", 512).expect_err("over ceiling must err");
let msg = format!("{err}");
assert!(
msg.contains("scan-guard ceiling"),
"scan_guard error must name the ceiling, got: {msg}"
);
assert!(
msg.contains("512"),
"must echo the ceiling number, got: {msg}"
);
}
#[test]
fn scan_guard_zero_ceiling_rejects_nonempty() {
let buf = vec![0u8; 1];
let err = scan_guard(&buf, "ctx", 0).expect_err("nonempty haystack with zero ceiling");
let msg = err.to_string();
assert!(
msg.contains("scan-guard ceiling") && msg.contains('0'),
"zero-ceiling rejection must name the ceiling: {msg}"
);
}
#[test]
fn scan_guard_zero_ceiling_accepts_empty() {
let buf: Vec<u8> = vec![];
assert_eq!(scan_guard(&buf, "ctx", 0).unwrap(), 0);
}
#[test]
fn scan_guard_at_max_u32_ceiling_accepts_real_inputs() {
let buf = vec![0u8; 1 << 16];
assert_eq!(scan_guard(&buf, "ctx", u32::MAX).unwrap(), 1 << 16);
}
#[test]
fn dispatch_config_clamps_at_one() {
let cfg = byte_scan_dispatch_config(0, 64);
assert_eq!(cfg.grid_override, Some([1, 1, 1]));
}
#[test]
fn dispatch_config_divceils() {
let cfg = byte_scan_dispatch_config(129, 64);
assert_eq!(cfg.grid_override, Some([3, 1, 1]));
}
#[test]
fn unpack_match_triples_sorts() {
let bytes = [
2, 0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0,
];
let matches = unpack_match_triples(&bytes, 2);
assert_eq!(matches.len(), 2);
assert!(matches[0].start <= matches[1].start);
}
#[test]
fn unpack_match_triples_into_reuses_caller_buffer() {
let bytes = [
2, 0, 0, 0, 10, 0, 0, 0, 20, 0, 0, 0, 1, 0, 0, 0, 5, 0, 0, 0, 8, 0, 0, 0,
];
let mut matches = Vec::with_capacity(8);
let ptr = matches.as_ptr();
unpack_match_triples_into(&bytes, 2, &mut matches);
assert_eq!(matches.len(), 2);
assert_eq!(matches.as_ptr(), ptr);
assert!(matches[0].start <= matches[1].start);
}
#[test]
fn try_unpack_match_triples_into_keeps_fallible_hot_path_reusable() {
let bytes = [
9, 0, 0, 0, 40, 0, 0, 0, 44, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 8, 0, 0, 0,
];
let mut matches = Vec::with_capacity(4);
let ptr = matches.as_ptr();
try_unpack_match_triples_into(&bytes, 2, &mut matches)
.expect("Fix: small decoded match buffer must reserve");
assert_eq!(matches.len(), 2);
assert_eq!(matches.as_ptr(), ptr);
assert_eq!(matches[0].pattern_id, 3);
assert_eq!(matches[1].pattern_id, 9);
}
#[test]
fn try_unpack_match_triples_owned_matches_compat_helper() {
let bytes = [
5, 0, 0, 0, 11, 0, 0, 0, 13, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 7, 0, 0, 0,
];
assert_eq!(
try_unpack_match_triples(&bytes, 2)
.expect("Fix: small decoded match buffer must reserve"),
unpack_match_triples(&bytes, 2)
);
}
#[test]
fn read_u32_prefix_decodes_counter_and_rejects_short_readback() {
assert_eq!(
try_read_u32_prefix(&[0x34, 0x12, 0, 0, 0xAA], "test counter")
.expect("Fix: four-byte counter prefix must decode"),
0x1234
);
let err = try_read_u32_prefix(&[1, 2, 3], "test counter")
.expect_err("short scan counter readback must fail closed");
let msg = err.to_string();
assert!(
msg.contains("test counter")
&& msg.contains("3 byte(s)")
&& msg.contains("requires 4 bytes"),
"short counter error must name the field and required length: {msg}"
);
}
#[test]
fn output_bytes_rejects_missing_declared_output_slot() {
let outputs = vec![vec![1, 2, 3, 4]];
assert_eq!(
try_output_bytes(&outputs, 0, "first").expect("Fix: present output slot must borrow"),
&[1, 2, 3, 4]
);
let err = try_output_bytes(&outputs, 1, "matches")
.expect_err("missing backend output slot must fail closed");
let msg = err.to_string();
assert!(
msg.contains("matches")
&& msg.contains("output index 1")
&& msg.contains("returned 1 output buffer"),
"missing output error must identify the omitted slot: {msg}"
);
}
#[test]
fn exact_prefix_match_decode_sorts_and_reuses_caller_buffer() {
let bytes = [
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,
];
let mut matches = Vec::with_capacity(4);
let ptr = matches.as_ptr();
try_unpack_match_triples_exact_prefix_into(&bytes, 2, &mut matches)
.expect("Fix: exact two-triple prefix must decode");
assert_eq!(matches.len(), 2);
assert_eq!(matches.as_ptr(), ptr);
assert_eq!(matches[0].pattern_id, 3);
assert_eq!(matches[1].pattern_id, 9);
}
#[test]
fn exact_prefix_match_decode_rejects_short_payload_and_clears_results() {
let bytes = [
7u8, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, ];
let mut matches = vec![vyre_foundation::match_result::Match::new(99, 1, 2)];
let err = try_unpack_match_triples_exact_prefix_into(&bytes, 2, &mut matches)
.expect_err("short match triple readback must fail closed");
let msg = err.to_string();
assert!(
matches.is_empty(),
"malformed readback must clear stale matches"
);
assert!(
msg.contains("readback was 12 byte(s)")
&& msg.contains("count=2")
&& msg.contains("requires 24 byte(s)"),
"short match readback error must identify observed and required bytes: {msg}"
);
}
#[test]
fn exact_prefix_match_decode_huge_count_short_payload_fails_closed() {
let bytes = [
7u8, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, ];
let mut matches = vec![vyre_foundation::match_result::Match::new(99, 1, 2)];
let err = try_unpack_match_triples_exact_prefix_into(&bytes, u32::MAX, &mut matches)
.expect_err("huge count with short readback must fail closed");
let msg = err.to_string();
assert!(
matches.is_empty(),
"malformed readback must clear stale matches"
);
assert!(
msg.contains("requires") || msg.contains("overflowed") || msg.contains("does not fit"),
"huge-count error must report required size or host capacity: {msg}"
);
}
#[test]
fn unpack_match_triples_huge_count_short_buffer_stays_in_bounds() {
let bytes = [
7u8, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, ];
let matches = unpack_match_triples(&bytes, u32::MAX);
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].pattern_id, 7);
assert_eq!(matches[0].start, 1);
assert_eq!(matches[0].end, 3);
}
}