1use std::error::Error as StdError;
4use std::fmt;
5use std::sync::{Arc, OnceLock};
6
7use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
8use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
9
10pub const BASE64_DECODE_OP_ID: &str = "vyre-primitives::decode::base64_decode";
12pub const PAD: u32 = b'=' as u32;
14pub const INVALID: u32 = 0xFF;
16pub const BASE64_DECODE_TABLE_WORDS: u32 = 256;
18pub const BASE64_WORKGROUP_SIZE: [u32; 3] = [64, 1, 1];
20
21static STANDARD_DECODE_TABLE: OnceLock<[u32; 256]> = OnceLock::new();
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Base64DecodeReferenceError {
26 InvalidLength {
28 len: usize,
30 },
31 CapacityOverflow {
33 blocks: usize,
35 },
36 DecodedLengthOverflow {
38 decoded_words: usize,
40 },
41 Allocation {
43 requested: usize,
45 source: String,
47 },
48}
49
50impl fmt::Display for Base64DecodeReferenceError {
51 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::InvalidLength { len } => write!(
54 formatter,
55 "base64 reference input length {len} is not a multiple of 4. Fix: pad with '=' or reject the payload before decode."
56 ),
57 Self::CapacityOverflow { blocks } => write!(
58 formatter,
59 "base64 reference decoded capacity overflowed for {blocks} input quads. Fix: shard the payload before CPU/GPU parity decode."
60 ),
61 Self::DecodedLengthOverflow { decoded_words } => write!(
62 formatter,
63 "base64 reference decoded capacity {decoded_words} cannot fit u32. Fix: shard the payload before dispatch."
64 ),
65 Self::Allocation { requested, source } => write!(
66 formatter,
67 "base64 reference could not reserve {requested} decoded u32 slots: {source}. Fix: shard the payload before CPU/GPU parity decode."
68 ),
69 }
70 }
71}
72
73impl StdError for Base64DecodeReferenceError {}
74
75fn blocks_for_len(input_len: u32) -> u32 {
76 input_len / 4
77}
78
79#[must_use]
81pub fn standard_decode_table() -> [u32; 256] {
82 *standard_decode_table_ref()
83}
84
85#[must_use]
90pub fn standard_decode_table_ref() -> &'static [u32; 256] {
91 STANDARD_DECODE_TABLE.get_or_init(build_standard_decode_table)
92}
93
94fn build_standard_decode_table() -> [u32; 256] {
95 let mut table = [INVALID; 256];
96 for byte in b'A'..=b'Z' {
97 table[usize::from(byte)] = u32::from(byte - b'A');
98 }
99 for byte in b'a'..=b'z' {
100 table[usize::from(byte)] = u32::from(byte - b'a' + 26);
101 }
102 for byte in b'0'..=b'9' {
103 table[usize::from(byte)] = u32::from(byte - b'0' + 52);
104 }
105 table[usize::from(b'+')] = 62;
106 table[usize::from(b'/')] = 63;
107 table[usize::from(b'=')] = 0;
108 table
109}
110
111#[must_use]
113pub fn decoded_capacity(input_len: u32) -> u32 {
114 blocks_for_len(input_len) * 3
115}
116
117#[must_use]
123#[cfg(any(test, feature = "cpu-parity"))]
124pub fn decode_standard_packed_reference(input: &[u8]) -> (Vec<u32>, u32) {
125 match try_decode_standard_packed_reference(input) {
126 Ok(decoded) => decoded,
127 Err(error) => panic!("vyre-primitives base64 decode reference failed: {error}"),
131 }
132}
133
134#[cfg(any(test, feature = "cpu-parity"))]
140pub fn decode_standard_packed_reference_into(input: &[u8], out: &mut Vec<u32>) -> u32 {
141 match try_decode_standard_packed_reference_into(input, out) {
142 Ok(decoded_len) => decoded_len,
143 Err(error) => panic!("vyre-primitives base64 decode reference failed: {error}"),
146 }
147}
148
149#[cfg(any(test, feature = "cpu-parity"))]
155pub fn try_decode_standard_packed_reference(
156 input: &[u8],
157) -> Result<(Vec<u32>, u32), Base64DecodeReferenceError> {
158 let mut out = Vec::new();
159 let decoded_len = try_decode_standard_packed_reference_into(input, &mut out)?;
160 Ok((out, decoded_len))
161}
162
163#[cfg(any(test, feature = "cpu-parity"))]
168pub fn try_decode_standard_packed_reference_into(
169 input: &[u8],
170 out: &mut Vec<u32>,
171) -> Result<u32, Base64DecodeReferenceError> {
172 if input.len() % 4 != 0 {
173 return Err(Base64DecodeReferenceError::InvalidLength { len: input.len() });
174 }
175 let table = standard_decode_table_ref();
176 let blocks = input.len() / 4;
177 let decoded_words = blocks
178 .checked_mul(3)
179 .ok_or(Base64DecodeReferenceError::CapacityOverflow { blocks })?;
180 vyre_foundation::allocation::reserve_exact_cleared(out, decoded_words).map_err(|source| {
181 Base64DecodeReferenceError::Allocation {
182 requested: decoded_words,
183 source: source.to_string(),
184 }
185 })?;
186 out.resize(decoded_words, 0);
187 for block in 0..blocks {
188 let base = block * 4;
189 let vals = [
190 table[usize::from(input[base])],
191 table[usize::from(input[base + 1])],
192 table[usize::from(input[base + 2])],
193 table[usize::from(input[base + 3])],
194 ]
195 .map(|value| if value == INVALID { 0 } else { value });
196 let out_base = block * 3;
197 out[out_base] = (vals[0] << 2) | (vals[1] >> 4);
198 if input[base + 2] != b'=' {
199 out[out_base + 1] = ((vals[1] & 0x0F) << 4) | (vals[2] >> 2);
200 }
201 if input[base + 3] != b'=' {
202 out[out_base + 2] = ((vals[2] & 0x03) << 6) | vals[3];
203 }
204 }
205 let mut decoded_len = u32::try_from(out.len()).map_err(|_| {
206 Base64DecodeReferenceError::DecodedLengthOverflow {
207 decoded_words: out.len(),
208 }
209 })?;
210 if input.len() >= 2 {
211 if input[input.len() - 1] == b'=' {
212 decoded_len = decoded_len.saturating_sub(1);
213 }
214 if input[input.len() - 2] == b'=' {
215 decoded_len = decoded_len.saturating_sub(1);
216 }
217 }
218 Ok(decoded_len)
219}
220
221fn clamp_lookup(name: &str, table: &str) -> Vec<Node> {
222 let raw = format!("{name}_raw");
223 let value = format!("{name}_v");
224 vec![
225 Node::let_bind(
232 raw.as_str(),
233 crate::ir_safe::byte_table_lookup(table, Expr::var(name)),
234 ),
235 Node::let_bind(
236 value.as_str(),
237 Expr::select(
238 Expr::eq(Expr::var(raw.as_str()), Expr::u32(INVALID)),
239 Expr::u32(0),
240 Expr::var(raw.as_str()),
241 ),
242 ),
243 ]
244}
245
246#[must_use]
248pub fn base64_decode_body(
249 input: &str,
250 table: &str,
251 output: &str,
252 decoded_len_buffer: &str,
253 input_len: u32,
254) -> Vec<Node> {
255 if input_len % 4 != 0 {
256 return vec![Node::trap(
257 Expr::u32(input_len),
258 "Fix: base64_decode requires input_len to be a multiple of 4; pad with '=' or reject the truncated payload upstream",
259 )];
260 }
261 let decoded_len = decoded_capacity(input_len);
262 let mut body = vec![Node::let_bind("j", Expr::InvocationId { axis: 0 })];
263 if input_len >= 2 {
264 body.push(Node::if_then(
265 Expr::eq(Expr::var("j"), Expr::u32(0)),
266 vec![
267 Node::let_bind(
268 "tail_pad_1",
269 Expr::select(
270 Expr::eq(Expr::load(input, Expr::u32(input_len - 1)), Expr::u32(PAD)),
271 Expr::u32(1),
272 Expr::u32(0),
273 ),
274 ),
275 Node::let_bind(
276 "tail_pad_2",
277 Expr::select(
278 Expr::eq(Expr::load(input, Expr::u32(input_len - 2)), Expr::u32(PAD)),
279 Expr::u32(1),
280 Expr::u32(0),
281 ),
282 ),
283 Node::store(
284 decoded_len_buffer,
285 Expr::u32(0),
286 Expr::sub(
287 Expr::sub(Expr::u32(decoded_len), Expr::var("tail_pad_1")),
288 Expr::var("tail_pad_2"),
289 ),
290 ),
291 ],
292 ));
293 } else {
294 body.push(Node::if_then(
295 Expr::eq(Expr::var("j"), Expr::u32(0)),
296 vec![Node::store(decoded_len_buffer, Expr::u32(0), Expr::u32(0))],
297 ));
298 }
299 body.push(Node::if_then(
300 Expr::lt(Expr::var("j"), Expr::u32(decoded_len)),
301 {
302 let mut per_byte = vec![
303 Node::let_bind("quad", Expr::div(Expr::var("j"), Expr::u32(3))),
304 Node::let_bind("in_base", Expr::mul(Expr::var("quad"), Expr::u32(4))),
305 Node::let_bind(
306 "pos",
307 Expr::sub(Expr::var("j"), Expr::mul(Expr::var("quad"), Expr::u32(3))),
308 ),
309 Node::let_bind("c0", Expr::load(input, Expr::var("in_base"))),
310 Node::let_bind(
311 "c1",
312 Expr::load(input, Expr::add(Expr::var("in_base"), Expr::u32(1))),
313 ),
314 Node::let_bind(
315 "c2",
316 Expr::load(input, Expr::add(Expr::var("in_base"), Expr::u32(2))),
317 ),
318 Node::let_bind(
319 "c3",
320 Expr::load(input, Expr::add(Expr::var("in_base"), Expr::u32(3))),
321 ),
322 Node::let_bind("pad2", Expr::eq(Expr::var("c2"), Expr::u32(PAD))),
323 Node::let_bind("pad1", Expr::eq(Expr::var("c3"), Expr::u32(PAD))),
324 ];
325 per_byte.extend(clamp_lookup("c0", table));
326 per_byte.extend(clamp_lookup("c1", table));
327 per_byte.extend(clamp_lookup("c2", table));
328 per_byte.extend(clamp_lookup("c3", table));
329 per_byte.extend([
330 Node::let_bind(
331 "b0",
332 Expr::bitor(
333 Expr::shl(Expr::var("c0_v"), Expr::u32(2)),
334 Expr::shr(Expr::var("c1_v"), Expr::u32(4)),
335 ),
336 ),
337 Node::let_bind(
338 "b1",
339 Expr::bitor(
340 Expr::shl(
341 Expr::bitand(Expr::var("c1_v"), Expr::u32(0x0F)),
342 Expr::u32(4),
343 ),
344 Expr::shr(Expr::var("c2_v"), Expr::u32(2)),
345 ),
346 ),
347 Node::let_bind(
348 "b2",
349 Expr::bitor(
350 Expr::shl(
351 Expr::bitand(Expr::var("c2_v"), Expr::u32(0x03)),
352 Expr::u32(6),
353 ),
354 Expr::var("c3_v"),
355 ),
356 ),
357 Node::if_then(
358 Expr::eq(Expr::var("pos"), Expr::u32(0)),
359 vec![Node::store(output, Expr::var("j"), Expr::var("b0"))],
360 ),
361 Node::if_then(
362 Expr::eq(Expr::var("pos"), Expr::u32(1)),
363 vec![Node::if_then(
364 Expr::eq(Expr::var("pad2"), Expr::bool(false)),
365 vec![Node::store(output, Expr::var("j"), Expr::var("b1"))],
366 )],
367 ),
368 Node::if_then(
369 Expr::eq(Expr::var("pos"), Expr::u32(2)),
370 vec![Node::if_then(
371 Expr::eq(Expr::var("pad1"), Expr::bool(false)),
372 vec![Node::store(output, Expr::var("j"), Expr::var("b2"))],
373 )],
374 ),
375 ]);
376 per_byte
377 },
378 ));
379 body
380}
381
382#[must_use]
384pub fn base64_decode_child(
385 parent_op_id: &str,
386 input: &str,
387 table: &str,
388 output: &str,
389 decoded_len_buffer: &str,
390 input_len: u32,
391) -> Node {
392 Node::Region {
393 generator: Ident::from(BASE64_DECODE_OP_ID),
394 source_region: Some(GeneratorRef {
395 name: parent_op_id.to_string(),
396 }),
397 body: Arc::new(base64_decode_body(
398 input,
399 table,
400 output,
401 decoded_len_buffer,
402 input_len,
403 )),
404 }
405}
406
407#[must_use]
409pub fn base64_decode(
410 input: &str,
411 table: &str,
412 output: &str,
413 decoded_len_buffer: &str,
414 input_len: u32,
415) -> Program {
416 Program::wrapped(
417 vec![
418 BufferDecl::storage(input, 0, BufferAccess::ReadOnly, DataType::U32)
419 .with_count(input_len),
420 BufferDecl::storage(table, 1, BufferAccess::ReadOnly, DataType::U32)
421 .with_count(BASE64_DECODE_TABLE_WORDS),
422 BufferDecl::output(output, 2, DataType::U32).with_count(decoded_capacity(input_len)),
423 BufferDecl::read_write(decoded_len_buffer, 3, DataType::U32).with_count(1),
424 ],
425 BASE64_WORKGROUP_SIZE,
426 vec![Node::Region {
427 generator: Ident::from(BASE64_DECODE_OP_ID),
428 source_region: None,
429 body: Arc::new(base64_decode_body(
430 input,
431 table,
432 output,
433 decoded_len_buffer,
434 input_len,
435 )),
436 }],
437 )
438}
439
440#[cfg(feature = "inventory-registry")]
441inventory::submit! {
442 vyre_foundation::operation::OperationRegistration::primitive(
443 BASE64_DECODE_OP_ID,
444 || base64_decode("input", "table", "output", "decoded_len", 4),
445 Some(|| vec![vec![
446 crate::wire::pack_u32_slice(&[u32::from(b'T'), u32::from(b'W'), u32::from(b'F'), u32::from(b'u')]),
447 crate::wire::pack_u32_slice(standard_decode_table_ref()),
448 vec![0; 12],
449 vec![0; 4],
450 ]]),
451 Some(|| vec![vec![
452 crate::wire::pack_u32_slice(&[u32::from(b'M'), u32::from(b'a'), u32::from(b'n')]),
453 crate::wire::pack_u32_slice(&[3]),
454 ]]),
455 )
456}
457
458#[cfg(any(test, feature = "cpu-parity"))]
464pub fn cpu_base64_table() -> [u32; 256] {
465 standard_decode_table()
466}
467
468#[must_use]
471#[cfg(any(test, feature = "cpu-parity"))]
472pub fn cpu_base64_decode(input: &[u8]) -> Vec<u8> {
473 let (words, decoded_len) = decode_standard_packed_reference(input);
474 let decoded_len = usize::try_from(decoded_len).unwrap_or(words.len());
475 words
476 .into_iter()
477 .take(decoded_len)
478 .map(|word| (word & 0xFF) as u8)
479 .collect()
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn decode_man() {
488 assert_eq!(cpu_base64_decode(b"TWFu"), b"Man");
489 }
490
491 #[test]
492 fn cpu_table_is_the_standard_primitive_table() {
493 assert_eq!(cpu_base64_table(), standard_decode_table());
494 assert_eq!(standard_decode_table()[b'/' as usize], 63);
495 assert_eq!(standard_decode_table()[b'*' as usize], INVALID);
496 }
497
498 #[test]
499 fn standard_decode_table_ref_matches_value_api_and_reuses_allocation() {
500 let first = standard_decode_table_ref();
501 let second = standard_decode_table_ref();
502 assert!(
503 std::ptr::eq(first, second),
504 "Fix: base64 decode setup must reuse the immutable primitive table instead of rebuilding it per dispatch."
505 );
506 assert_eq!(*first, standard_decode_table());
507 }
508
509 #[test]
510 fn try_decode_reference_rejects_unaligned_input_without_panic() {
511 let err = try_decode_standard_packed_reference(b"abc")
512 .expect_err("unaligned base64 input must be rejected");
513 assert_eq!(err, Base64DecodeReferenceError::InvalidLength { len: 3 });
514 }
515
516 #[test]
517 fn try_decode_reference_matches_infallible_wrapper() {
518 let fallible = try_decode_standard_packed_reference(b"Zm9vYmFy")
519 .expect("Fix: unit-test oracle precondition - valid base64 must decode");
520 let infallible = decode_standard_packed_reference(b"Zm9vYmFy");
521 assert_eq!(fallible, infallible);
522 assert_eq!(fallible.1, 6);
523 }
524
525 #[test]
526 fn try_decode_reference_into_reuses_output_and_clears_stale_tail() {
527 let mut out = Vec::with_capacity(16);
528 out.extend_from_slice(&[u32::MAX; 16]);
529 let ptr = out.as_ptr();
530
531 let decoded_len = try_decode_standard_packed_reference_into(b"TWE=", &mut out)
532 .expect("Fix: unit-test oracle precondition - valid padded base64 must decode into caller-owned storage");
533
534 assert_eq!(decoded_len, 2);
535 assert_eq!(out, vec![u32::from(b'M'), u32::from(b'a'), 0]);
536 assert_eq!(out.as_ptr(), ptr);
537 }
538
539 #[test]
540 fn try_decode_reference_into_is_transactional_on_invalid_length() {
541 let mut out = vec![0x1234_5678, 0x9abc_def0];
542 let before = out.clone();
543
544 let err = try_decode_standard_packed_reference_into(b"abc", &mut out)
545 .expect_err("unaligned base64 input must be rejected");
546
547 assert_eq!(err, Base64DecodeReferenceError::InvalidLength { len: 3 });
548 assert_eq!(out, before);
549 }
550
551 #[test]
557 #[should_panic(expected = "vyre-primitives base64 decode reference failed")]
558 fn decode_reference_fails_loud_on_invalid_length() {
559 let _ = decode_standard_packed_reference(b"abc");
560 }
561
562 #[test]
563 #[should_panic(expected = "vyre-primitives base64 decode reference failed")]
564 fn decode_reference_into_fails_loud_on_invalid_length() {
565 let mut out = vec![1, 2, 3];
566 let _ = decode_standard_packed_reference_into(b"abc", &mut out);
567 }
568
569 #[test]
570 #[should_panic(expected = "vyre-primitives base64 decode reference failed")]
571 fn cpu_base64_decode_fails_loud_on_invalid_length() {
572 let _ = cpu_base64_decode(b"abc");
573 }
574
575 #[test]
576 fn decode_padded_1() {
577 assert_eq!(cpu_base64_decode(b"TWE="), b"Ma");
578 }
579
580 #[test]
581 fn decode_padded_2() {
582 assert_eq!(cpu_base64_decode(b"TQ=="), b"M");
583 }
584
585 #[test]
586 fn decode_empty() {
587 assert_eq!(cpu_base64_decode(b""), b"");
588 }
589
590 #[test]
591 fn decode_hello_world() {
592 assert_eq!(cpu_base64_decode(b"SGVsbG8gV29ybGQ="), b"Hello World");
593 }
594
595 #[test]
596 fn decode_roundtrip_rfc4648_vectors() {
597 assert_eq!(cpu_base64_decode(b"Zg=="), b"f");
599 assert_eq!(cpu_base64_decode(b"Zm8="), b"fo");
600 assert_eq!(cpu_base64_decode(b"Zm9v"), b"foo");
601 assert_eq!(cpu_base64_decode(b"Zm9vYg=="), b"foob");
602 assert_eq!(cpu_base64_decode(b"Zm9vYmE="), b"fooba");
603 assert_eq!(cpu_base64_decode(b"Zm9vYmFy"), b"foobar");
604 }
605
606 #[test]
607 fn table_index_is_masked_so_high_bit_input_cannot_read_out_of_bounds() {
608 use vyre_reference::value::Value;
609 let input_len = 4u32;
617 let dirty = [
618 0x0100u32 | u32::from(b'T'),
619 u32::from(b'W'),
620 u32::from(b'F'),
621 u32::from(b'u'),
622 ];
623 let program = base64_decode("input", "table", "output", "decoded_len", input_len);
624 let inputs = vec![
625 Value::from(crate::wire::pack_u32_slice(&dirty)),
626 Value::from(crate::wire::pack_u32_slice(standard_decode_table_ref())),
627 Value::from(vec![0u8; decoded_capacity(input_len) as usize * 4]),
628 Value::from(crate::wire::pack_u32_slice(&[0])),
629 ];
630 let outputs = vyre_reference::reference_eval(&program, &inputs)
631 .expect("Fix: base64 decode with a >255 input element must not fault the interpreter");
632 let out_idx = vyre_reference::output_index(&program, "output")
635 .expect("Fix: base64 output buffer must be a reference output");
636 let len_idx = vyre_reference::output_index(&program, "decoded_len")
637 .expect("Fix: base64 decoded_len buffer must be a reference output");
638 let words = crate::wire::decode_u32_le_bytes_all(&outputs[out_idx].to_bytes());
639 let decoded_len =
640 crate::wire::decode_u32_le_bytes_all(&outputs[len_idx].to_bytes())[0] as usize;
641 let bytes: Vec<u8> = words
642 .into_iter()
643 .take(decoded_len)
644 .map(|word| (word & 0xFF) as u8)
645 .collect();
646 assert_eq!(
647 bytes, b"Man",
648 "Fix: masked table index must decode the high-bit-dirty input identically to the clean 'TWFu'"
649 );
650 }
651}