Skip to main content

seq_runtime/
encoding.rs

1//! Encoding operations for Seq (Base64, Hex)
2//!
3//! These functions are exported with C ABI for LLVM codegen to call.
4//!
5//! All encode/decode pairs are byte-clean: encode accepts arbitrary
6//! input bytes; decode produces arbitrary output bytes wrapped in a
7//! byte-clean SeqString. This is the canonical use case for these
8//! encodings — round-tripping binary content as text.
9//!
10//! # API
11//!
12//! ```seq
13//! # Base64 encoding/decoding
14//! "hello" encoding.base64-encode     # ( String -- String ) "aGVsbG8="
15//! "aGVsbG8=" encoding.base64-decode  # ( String -- String Bool )
16//!
17//! # URL-safe Base64 (for JWTs, URLs)
18//! data encoding.base64url-encode     # ( String -- String )
19//! encoded encoding.base64url-decode  # ( String -- String Bool )
20//!
21//! # Hex encoding/decoding
22//! "hello" encoding.hex-encode        # ( String -- String ) "68656c6c6f"
23//! "68656c6c6f" encoding.hex-decode   # ( String -- String Bool )
24//! ```
25
26use crate::seqstring::{global_bytes, global_string};
27use crate::stack::{Stack, pop, push};
28use crate::value::Value;
29
30use base64::prelude::*;
31
32/// Encode a string to Base64 (standard alphabet with padding)
33///
34/// Stack effect: ( String -- String )
35///
36/// # Safety
37/// Stack must have a String value on top
38#[unsafe(no_mangle)]
39pub unsafe extern "C" fn patch_seq_base64_encode(stack: Stack) -> Stack {
40    assert!(!stack.is_null(), "base64-encode: stack is empty");
41
42    let (stack, value) = unsafe { pop(stack) };
43
44    match value {
45        Value::String(s) => {
46            let encoded = BASE64_STANDARD.encode(s.as_bytes());
47            unsafe { push(stack, Value::String(global_string(encoded))) }
48        }
49        _ => panic!("base64-encode: expected String on stack, got {:?}", value),
50    }
51}
52
53/// Decode a Base64 string (standard alphabet)
54///
55/// Stack effect: ( String -- String Bool )
56///
57/// Returns the decoded string and true on success, empty string and false on failure.
58///
59/// # Safety
60/// Stack must have a String value on top
61#[unsafe(no_mangle)]
62pub unsafe extern "C" fn patch_seq_base64_decode(stack: Stack) -> Stack {
63    assert!(!stack.is_null(), "base64-decode: stack is empty");
64
65    let (stack, value) = unsafe { pop(stack) };
66
67    match value {
68        // Decoded bytes go straight into a byte-clean SeqString —
69        // base64 is the canonical "encode arbitrary bytes as text",
70        // so the decode side must round-trip whatever was encoded.
71        Value::String(s) => match BASE64_STANDARD.decode(s.as_bytes()) {
72            Ok(bytes) => {
73                let stack = unsafe { push(stack, Value::String(global_bytes(bytes))) };
74                unsafe { push(stack, Value::Bool(true)) }
75            }
76            Err(_) => {
77                // Invalid Base64 input
78                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
79                unsafe { push(stack, Value::Bool(false)) }
80            }
81        },
82        _ => panic!("base64-decode: expected String on stack, got {:?}", value),
83    }
84}
85
86/// Encode a string to URL-safe Base64 (no padding)
87///
88/// Stack effect: ( String -- String )
89///
90/// Uses URL-safe alphabet (- and _ instead of + and /) with no padding.
91/// Suitable for JWTs, URLs, and filenames.
92///
93/// # Safety
94/// Stack must have a String value on top
95#[unsafe(no_mangle)]
96pub unsafe extern "C" fn patch_seq_base64url_encode(stack: Stack) -> Stack {
97    assert!(!stack.is_null(), "base64url-encode: stack is empty");
98
99    let (stack, value) = unsafe { pop(stack) };
100
101    match value {
102        Value::String(s) => {
103            let encoded = BASE64_URL_SAFE_NO_PAD.encode(s.as_bytes());
104            unsafe { push(stack, Value::String(global_string(encoded))) }
105        }
106        _ => panic!(
107            "base64url-encode: expected String on stack, got {:?}",
108            value
109        ),
110    }
111}
112
113/// Decode a URL-safe Base64 string (no padding expected)
114///
115/// Stack effect: ( String -- String Bool )
116///
117/// Returns the decoded string and true on success, empty string and false on failure.
118///
119/// # Safety
120/// Stack must have a String value on top
121#[unsafe(no_mangle)]
122pub unsafe extern "C" fn patch_seq_base64url_decode(stack: Stack) -> Stack {
123    assert!(!stack.is_null(), "base64url-decode: stack is empty");
124
125    let (stack, value) = unsafe { pop(stack) };
126
127    match value {
128        Value::String(s) => match BASE64_URL_SAFE_NO_PAD.decode(s.as_bytes()) {
129            Ok(bytes) => {
130                let stack = unsafe { push(stack, Value::String(global_bytes(bytes))) };
131                unsafe { push(stack, Value::Bool(true)) }
132            }
133            Err(_) => {
134                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
135                unsafe { push(stack, Value::Bool(false)) }
136            }
137        },
138        _ => panic!(
139            "base64url-decode: expected String on stack, got {:?}",
140            value
141        ),
142    }
143}
144
145/// Encode a string to hexadecimal (lowercase)
146///
147/// Stack effect: ( String -- String )
148///
149/// Each byte becomes two hex characters.
150///
151/// # Safety
152/// Stack must have a String value on top
153#[unsafe(no_mangle)]
154pub unsafe extern "C" fn patch_seq_hex_encode(stack: Stack) -> Stack {
155    assert!(!stack.is_null(), "hex-encode: stack is empty");
156
157    let (stack, value) = unsafe { pop(stack) };
158
159    match value {
160        Value::String(s) => {
161            let encoded = hex::encode(s.as_bytes());
162            unsafe { push(stack, Value::String(global_string(encoded))) }
163        }
164        _ => panic!("hex-encode: expected String on stack, got {:?}", value),
165    }
166}
167
168/// Decode a hexadecimal string
169///
170/// Stack effect: ( String -- String Bool )
171///
172/// Returns the decoded string and true on success, empty string and false on failure.
173/// Accepts both uppercase and lowercase hex characters.
174///
175/// # Safety
176/// Stack must have a String value on top
177#[unsafe(no_mangle)]
178pub unsafe extern "C" fn patch_seq_hex_decode(stack: Stack) -> Stack {
179    assert!(!stack.is_null(), "hex-decode: stack is empty");
180
181    let (stack, value) = unsafe { pop(stack) };
182
183    match value {
184        // Hex is the same story as base64: an encoding *for* arbitrary
185        // bytes, so the decode result is byte-clean.
186        Value::String(s) => match hex::decode(s.as_bytes()) {
187            Ok(bytes) => {
188                let stack = unsafe { push(stack, Value::String(global_bytes(bytes))) };
189                unsafe { push(stack, Value::Bool(true)) }
190            }
191            Err(_) => {
192                // Invalid hex input
193                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
194                unsafe { push(stack, Value::Bool(false)) }
195            }
196        },
197        _ => panic!("hex-decode: expected String on stack, got {:?}", value),
198    }
199}
200
201#[cfg(test)]
202mod tests;