1use std::sync::Arc;
11
12use vyre_foundation::ir::model::expr::Ident;
13use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
14
15pub const OP_ID: &str = "vyre-primitives::decode::ziftsieve_literal_copy";
17pub const WORKGROUP_SIZE: [u32; 3] = [64, 1, 1];
19pub const MAX_BLOCK_SIZE: usize = 4 * 1024 * 1024;
21pub const MAX_SEQUENCES_PER_BLOCK: usize = 100_000;
23
24#[cfg(any(test, feature = "cpu-parity"))]
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct ZiftsieveExtract {
38 pub literals: Vec<u8>,
40 pub decoded_len: usize,
42}
43
44#[cfg(any(test, feature = "cpu-parity"))]
45impl ZiftsieveExtract {
46 #[must_use]
49 pub fn truncated(&self) -> bool {
50 self.decoded_len > self.literals.len()
51 }
52}
53
54#[cfg(any(test, feature = "cpu-parity"))]
65pub fn ziftsieve_reference_extract_literals(
66 compressed: &[u8],
67 max_output: usize,
68) -> Result<ZiftsieveExtract, String> {
69 let initial_cap = compressed
70 .len()
71 .saturating_mul(2)
72 .min(max_output)
73 .min(MAX_BLOCK_SIZE);
74 let mut literals = Vec::with_capacity(initial_cap);
75 let mut decoded_len = 0usize;
76 let mut pos = 0usize;
77 let mut sequence_count = 0usize;
78
79 while pos < compressed.len() {
80 sequence_count += 1;
81 if sequence_count > MAX_SEQUENCES_PER_BLOCK {
82 return Err(format!(
83 "too many LZ4 sequences (max {MAX_SEQUENCES_PER_BLOCK}). \
84 Fix: use a smaller LZ4 block or increase MAX_SEQUENCES_PER_BLOCK"
85 ));
86 }
87
88 let token = compressed[pos];
89 pos += 1;
90
91 let literal_len = (token >> 4) as usize;
92 let match_len = (token & 0x0F) as usize;
93
94 let literal_len = if literal_len == 15 {
95 decode_length(compressed, &mut pos, literal_len)?
96 } else {
97 literal_len
98 };
99
100 if literal_len > MAX_BLOCK_SIZE {
101 return Err(format!(
102 "literal length {literal_len} exceeds MAX_BLOCK_SIZE {MAX_BLOCK_SIZE}. \
103 Fix: use a valid LZ4 stream"
104 ));
105 }
106
107 if pos + literal_len > compressed.len() {
108 return Err(format!(
109 "literal exceeds block bounds at offset {pos}. \
110 Fix: use a valid LZ4 stream"
111 ));
112 }
113
114 decoded_len = decoded_len.saturating_add(literal_len);
117 let remaining_output = max_output.saturating_sub(literals.len());
118 let to_copy = literal_len.min(remaining_output);
119 if to_copy > 0 {
120 literals.extend_from_slice(&compressed[pos..pos + to_copy]);
121 }
122 pos += literal_len;
123
124 if pos < compressed.len() {
125 if pos + 2 > compressed.len() {
126 return Err(format!(
127 "truncated match offset at offset {pos}. \
128 Fix: use a complete LZ4 stream"
129 ));
130 }
131 pos += 2;
132
133 if match_len == 15 {
134 let _match_len_extension = decode_length(compressed, &mut pos, match_len)?;
135 }
136 }
137 }
138
139 Ok(ZiftsieveExtract {
140 literals,
141 decoded_len,
142 })
143}
144
145fn decode_length(data: &[u8], pos: &mut usize, initial: usize) -> Result<usize, String> {
146 let mut len = initial;
147 loop {
148 if *pos >= data.len() {
149 return Err(format!(
150 "truncated length encoding at offset {pos}. \
151 Fix: use a complete LZ4 stream"
152 ));
153 }
154 let byte = data[*pos];
155 *pos += 1;
156 len = len.checked_add(byte as usize).ok_or_else(|| {
157 "length overflow in variable-length encoding. Fix: use a valid LZ4 stream".to_string()
158 })?;
159 if byte < 255 {
160 break;
161 }
162 if len > MAX_BLOCK_SIZE {
163 return Err(format!(
164 "length {len} exceeds MAX_BLOCK_SIZE {MAX_BLOCK_SIZE}. \
165 Fix: use a valid LZ4 stream"
166 ));
167 }
168 }
169 Ok(len)
170}
171
172#[must_use]
174pub fn ziftsieve_literal_copy_body(
175 input: &str,
176 output: &str,
177 seq_literal_start: &str,
178 seq_literal_len: &str,
179 seq_literal_offset: &str,
180 seq_count: u32,
181) -> Vec<Node> {
182 vec![
183 Node::let_bind("seq_idx", Expr::InvocationId { axis: 0 }),
184 Node::if_then(
185 Expr::lt(Expr::var("seq_idx"), Expr::u32(seq_count)),
186 vec![
187 Node::let_bind(
188 "literal_start",
189 Expr::load(seq_literal_start, Expr::var("seq_idx")),
190 ),
191 Node::let_bind(
192 "literal_len",
193 Expr::load(seq_literal_len, Expr::var("seq_idx")),
194 ),
195 Node::let_bind(
196 "literal_offset",
197 Expr::load(seq_literal_offset, Expr::var("seq_idx")),
198 ),
199 Node::loop_for(
200 "i",
201 Expr::u32(0),
202 Expr::var("literal_len"),
203 vec![Node::if_then(
216 Expr::and(
217 Expr::lt(
218 Expr::add(Expr::var("literal_start"), Expr::var("i")),
219 Expr::buf_len(input),
220 ),
221 Expr::lt(
222 Expr::add(Expr::var("literal_offset"), Expr::var("i")),
223 Expr::buf_len(output),
224 ),
225 ),
226 vec![
227 Node::let_bind(
228 "src",
229 Expr::load(
230 input,
231 Expr::add(Expr::var("literal_start"), Expr::var("i")),
232 ),
233 ),
234 Node::store(
235 output,
236 Expr::add(Expr::var("literal_offset"), Expr::var("i")),
237 Expr::var("src"),
238 ),
239 ],
240 )],
241 ),
242 ],
243 ),
244 ]
245}
246
247#[must_use]
249pub fn ziftsieve_literal_copy(
250 input: &str,
251 output: &str,
252 seq_literal_start: &str,
253 seq_literal_len: &str,
254 seq_literal_offset: &str,
255 input_len: u32,
256 seq_count: u32,
257 max_output: u32,
258) -> Program {
259 ziftsieve_literal_copy_with_op_id(
260 OP_ID,
261 input,
262 output,
263 seq_literal_start,
264 seq_literal_len,
265 seq_literal_offset,
266 input_len,
267 seq_count,
268 max_output,
269 )
270}
271
272#[must_use]
277pub fn ziftsieve_literal_copy_with_op_id(
278 op_id: &str,
279 input: &str,
280 output: &str,
281 seq_literal_start: &str,
282 seq_literal_len: &str,
283 seq_literal_offset: &str,
284 input_len: u32,
285 seq_count: u32,
286 max_output: u32,
287) -> Program {
288 let body = ziftsieve_literal_copy_body(
289 input,
290 output,
291 seq_literal_start,
292 seq_literal_len,
293 seq_literal_offset,
294 seq_count,
295 );
296
297 let input_decl = BufferDecl::storage(input, 0, BufferAccess::ReadOnly, DataType::U32);
298 let input_decl = if input_len == 0 {
299 input_decl
300 } else {
301 input_decl.with_count(input_len)
302 };
303
304 Program::wrapped(
305 vec![
306 input_decl,
307 BufferDecl::storage(seq_literal_start, 1, BufferAccess::ReadOnly, DataType::U32)
308 .with_count(seq_count.max(1)),
309 BufferDecl::storage(seq_literal_len, 2, BufferAccess::ReadOnly, DataType::U32)
310 .with_count(seq_count.max(1)),
311 BufferDecl::storage(seq_literal_offset, 3, BufferAccess::ReadOnly, DataType::U32)
312 .with_count(seq_count.max(1)),
313 BufferDecl::storage(output, 4, BufferAccess::ReadWrite, DataType::U32)
314 .with_count(max_output.max(1)),
315 ],
316 WORKGROUP_SIZE,
317 vec![Node::Region {
318 generator: Ident::from(op_id),
319 source_region: None,
320 body: Arc::new(body),
321 }],
322 )
323}
324
325#[cfg(feature = "inventory-registry")]
326fn fixture_inputs() -> Vec<Vec<Vec<u8>>> {
327 let input = crate::wire::pack_u32_slice(&[0x10, b'A' as u32, 0x20, b'B' as u32, b'C' as u32]);
328 let seq_literal_start = crate::wire::pack_u32_slice(&[1, 3]);
329 let seq_literal_len = crate::wire::pack_u32_slice(&[1, 2]);
330 let seq_literal_offset = crate::wire::pack_u32_slice(&[0, 1]);
331 vec![vec![
332 input,
333 seq_literal_start,
334 seq_literal_len,
335 seq_literal_offset,
336 vec![0u8; 3 * 4],
337 ]]
338}
339
340#[cfg(feature = "inventory-registry")]
341fn fixture_outputs() -> Vec<Vec<Vec<u8>>> {
342 vec![vec![crate::wire::pack_u32_slice(&[
343 b'A' as u32,
344 b'B' as u32,
345 b'C' as u32,
346 ])]]
347}
348
349#[cfg(feature = "inventory-registry")]
350inventory::submit! {
351 vyre_foundation::operation::OperationRegistration::primitive(
352 OP_ID,
353 || ziftsieve_literal_copy("input", "output", "seq_start", "seq_len", "seq_off", 5, 2, 3),
354 Some(fixture_inputs),
355 Some(fixture_outputs),
356 )
357}
358
359#[cfg(test)]
360#[allow(clippy::expect_used, clippy::unwrap_used)]
361mod tests {
362 use super::*;
363 use vyre_reference::value::Value;
364
365 fn run(input: &[u8], seq_starts: &[u32], seq_lens: &[u32], seq_offsets: &[u32]) -> Vec<u32> {
366 let seq_count = seq_starts.len() as u32;
367 let max_output = seq_lens.iter().copied().sum::<u32>();
368 let input_words = input.iter().map(|&b| u32::from(b)).collect::<Vec<_>>();
369 let program = ziftsieve_literal_copy(
370 "input",
371 "output",
372 "seq_start",
373 "seq_len",
374 "seq_off",
375 input.len() as u32,
376 seq_count,
377 max_output,
378 );
379 let inputs = vec![
380 Value::from(crate::wire::pack_u32_slice(&input_words)),
381 Value::from(crate::wire::pack_u32_slice(seq_starts)),
382 Value::from(crate::wire::pack_u32_slice(seq_lens)),
383 Value::from(crate::wire::pack_u32_slice(seq_offsets)),
384 Value::from(vec![0u8; (max_output.max(1) as usize) * 4]),
385 ];
386 let outputs = vyre_reference::reference_eval(&program, &inputs)
387 .expect("Fix: ziftsieve literal-copy primitive must run.");
388 let words = crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes());
389 words.into_iter().take(max_output as usize).collect()
390 }
391
392 #[test]
393 fn single_literal() {
394 assert_eq!(run(&[0x10, b'A'], &[1], &[1], &[0]), vec![b'A' as u32]);
395 }
396
397 #[test]
398 fn two_sequences() {
399 assert_eq!(
400 run(&[0x10, b'A', 0x20, b'B', b'C'], &[1, 3], &[1, 2], &[0, 1]),
401 vec![b'A' as u32, b'B' as u32, b'C' as u32]
402 );
403 }
404
405 #[test]
406 fn zero_literal_sequence_is_nop() {
407 assert_eq!(
408 run(&[0x00, 0x10, b'A'], &[0], &[0], &[0]),
409 Vec::<u32>::new()
410 );
411 }
412
413 #[test]
414 fn reference_extracts_simple_literal() {
415 let result = ziftsieve_reference_extract_literals(&[0x10, b'A'], 1024).unwrap();
416 assert_eq!(result.literals, b"A");
417 assert_eq!(result.decoded_len, 1);
418 assert!(!result.truncated());
419 }
420
421 #[test]
422 fn reference_extracts_with_match_skip() {
423 let data = [0x11, b'A', 0x01, 0x00];
424 let result = ziftsieve_reference_extract_literals(&data, 1024).unwrap();
425 assert_eq!(result.literals, b"A");
426 assert!(!result.truncated());
427 }
428
429 #[test]
430 fn reference_rejects_truncated_literal() {
431 let err = ziftsieve_reference_extract_literals(&[0x20, b'A'], 1024).unwrap_err();
432 assert!(err.contains("truncated") || err.contains("literal"));
433 }
434
435 #[test]
436 fn reference_accepts_exact_max_sequence_count() {
437 let mut data = Vec::new();
438 for _ in 1..MAX_SEQUENCES_PER_BLOCK {
439 data.push(0x10);
440 data.push(b'X');
441 data.extend_from_slice(&[0x00, 0x00]);
442 }
443 data.push(0x10);
444 data.push(b'X');
445
446 let result = ziftsieve_reference_extract_literals(&data, MAX_SEQUENCES_PER_BLOCK)
447 .expect("Fix: MAX_SEQUENCES_PER_BLOCK is an inclusive maximum, not an exclusive one.");
448 assert_eq!(result.literals.len(), MAX_SEQUENCES_PER_BLOCK);
449 assert!(result.literals.iter().all(|&byte| byte == b'X'));
450 assert!(!result.truncated());
451 }
452
453 #[test]
454 fn reference_rejects_too_many_sequences() {
455 let mut data = Vec::new();
456 for _ in 0..=MAX_SEQUENCES_PER_BLOCK {
457 data.push(0x10);
458 data.push(b'X');
459 data.extend_from_slice(&[0x00, 0x00]);
460 }
461 let err = ziftsieve_reference_extract_literals(&data, 1024).unwrap_err();
462 assert!(err.contains("sequence") || err.contains("MAX"));
463 }
464
465 fn run_with_sentinel(
470 input_words: &[u32],
471 seq_starts: &[u32],
472 seq_lens: &[u32],
473 seq_offsets: &[u32],
474 max_output: u32,
475 sentinel: u32,
476 ) -> Vec<u32> {
477 let program = ziftsieve_literal_copy(
478 "input",
479 "output",
480 "seq_start",
481 "seq_len",
482 "seq_off",
483 input_words.len() as u32,
484 seq_starts.len() as u32,
485 max_output,
486 );
487 let inputs = vec![
488 Value::from(crate::wire::pack_u32_slice(input_words)),
489 Value::from(crate::wire::pack_u32_slice(seq_starts)),
490 Value::from(crate::wire::pack_u32_slice(seq_lens)),
491 Value::from(crate::wire::pack_u32_slice(seq_offsets)),
492 Value::from(crate::wire::pack_u32_slice(&vec![
493 sentinel;
494 max_output.max(1) as usize
495 ])),
496 ];
497 let outputs = vyre_reference::reference_eval(&program, &inputs)
498 .expect("Fix: out-of-contract ziftsieve copy must not fault the interpreter");
499 crate::wire::decode_u32_le_bytes_all(&outputs[0].to_bytes())
500 }
501
502 #[test]
503 fn out_of_contract_offset_drops_stores_past_output_cap() {
504 const SENTINEL: u32 = 0xDEAD_BEEF;
511 let words = run_with_sentinel(
512 &[b'A' as u32, b'B' as u32, b'C' as u32, b'D' as u32],
513 &[0],
514 &[4],
515 &[1],
516 3,
517 SENTINEL,
518 );
519 assert_eq!(
523 words,
524 vec![SENTINEL, b'A' as u32, b'B' as u32],
525 "Fix: stores past the output cap must be dropped by the IR gate, untouched slots keep their prior value"
526 );
527 }
528
529 #[test]
530 fn out_of_contract_literal_start_gates_oob_source_reads() {
531 const SENTINEL: u32 = 0x1234_5678;
538 let words = run_with_sentinel(
539 &[b'A' as u32, b'B' as u32], &[1], &[3], &[0],
543 4,
544 SENTINEL,
545 );
546 assert_eq!(
551 words,
552 vec![b'B' as u32, SENTINEL, SENTINEL, SENTINEL],
553 "Fix: OOB source reads must be skipped by the IR gate (no OOB load), leaving output untouched"
554 );
555 }
556
557 #[test]
558 fn out_of_contract_copy_records_zero_interpreter_oob_accesses() {
559 let program = ziftsieve_literal_copy(
566 "input",
567 "output",
568 "seq_start",
569 "seq_len",
570 "seq_off",
571 4,
572 1,
573 3,
574 );
575 let (_outputs, report) = vyre_reference::reference_eval_oob_report(
576 &program,
577 &[
578 Value::from(crate::wire::pack_u32_slice(&[10, 20, 30, 40])),
579 Value::from(crate::wire::pack_u32_slice(&[0])), Value::from(crate::wire::pack_u32_slice(&[4])), Value::from(crate::wire::pack_u32_slice(&[1])), Value::from(crate::wire::pack_u32_slice(&[0u32; 3])),
583 ],
584 )
585 .expect("Fix: ziftsieve copy must reference-evaluate");
586 assert_eq!(
587 report.total(),
588 0,
589 "Fix: the bounds-gated copy must never trigger interpreter OOB masking on hostile input"
590 );
591 }
592}