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//! # API
6//!
7//! ```seq
8//! # Base64 encoding/decoding
9//! "hello" encoding.base64-encode     # ( String -- String ) "aGVsbG8="
10//! "aGVsbG8=" encoding.base64-decode  # ( String -- String Bool )
11//!
12//! # URL-safe Base64 (for JWTs, URLs)
13//! data encoding.base64url-encode     # ( String -- String )
14//! encoded encoding.base64url-decode  # ( String -- String Bool )
15//!
16//! # Hex encoding/decoding
17//! "hello" encoding.hex-encode        # ( String -- String ) "68656c6c6f"
18//! "68656c6c6f" encoding.hex-decode   # ( String -- String Bool )
19//! ```
20
21use crate::seqstring::global_string;
22use crate::stack::{Stack, pop, push};
23use crate::value::Value;
24
25use base64::prelude::*;
26
27/// Encode a string to Base64 (standard alphabet with padding)
28///
29/// Stack effect: ( String -- String )
30///
31/// # Safety
32/// Stack must have a String value on top
33#[unsafe(no_mangle)]
34pub unsafe extern "C" fn patch_seq_base64_encode(stack: Stack) -> Stack {
35    assert!(!stack.is_null(), "base64-encode: stack is empty");
36
37    let (stack, value) = unsafe { pop(stack) };
38
39    match value {
40        Value::String(s) => {
41            let encoded = BASE64_STANDARD.encode(s.as_str().as_bytes());
42            unsafe { push(stack, Value::String(global_string(encoded))) }
43        }
44        _ => panic!("base64-encode: expected String on stack, got {:?}", value),
45    }
46}
47
48/// Decode a Base64 string (standard alphabet)
49///
50/// Stack effect: ( String -- String Bool )
51///
52/// Returns the decoded string and true on success, empty string and false on failure.
53///
54/// # Safety
55/// Stack must have a String value on top
56#[unsafe(no_mangle)]
57pub unsafe extern "C" fn patch_seq_base64_decode(stack: Stack) -> Stack {
58    assert!(!stack.is_null(), "base64-decode: stack is empty");
59
60    let (stack, value) = unsafe { pop(stack) };
61
62    match value {
63        Value::String(s) => match BASE64_STANDARD.decode(s.as_str().as_bytes()) {
64            Ok(bytes) => match String::from_utf8(bytes) {
65                Ok(decoded) => {
66                    let stack = unsafe { push(stack, Value::String(global_string(decoded))) };
67                    unsafe { push(stack, Value::Bool(true)) }
68                }
69                Err(_) => {
70                    // Decoded bytes are not valid UTF-8
71                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
72                    unsafe { push(stack, Value::Bool(false)) }
73                }
74            },
75            Err(_) => {
76                // Invalid Base64 input
77                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
78                unsafe { push(stack, Value::Bool(false)) }
79            }
80        },
81        _ => panic!("base64-decode: expected String on stack, got {:?}", value),
82    }
83}
84
85/// Encode a string to URL-safe Base64 (no padding)
86///
87/// Stack effect: ( String -- String )
88///
89/// Uses URL-safe alphabet (- and _ instead of + and /) with no padding.
90/// Suitable for JWTs, URLs, and filenames.
91///
92/// # Safety
93/// Stack must have a String value on top
94#[unsafe(no_mangle)]
95pub unsafe extern "C" fn patch_seq_base64url_encode(stack: Stack) -> Stack {
96    assert!(!stack.is_null(), "base64url-encode: stack is empty");
97
98    let (stack, value) = unsafe { pop(stack) };
99
100    match value {
101        Value::String(s) => {
102            let encoded = BASE64_URL_SAFE_NO_PAD.encode(s.as_str().as_bytes());
103            unsafe { push(stack, Value::String(global_string(encoded))) }
104        }
105        _ => panic!(
106            "base64url-encode: expected String on stack, got {:?}",
107            value
108        ),
109    }
110}
111
112/// Decode a URL-safe Base64 string (no padding expected)
113///
114/// Stack effect: ( String -- String Bool )
115///
116/// Returns the decoded string and true on success, empty string and false on failure.
117///
118/// # Safety
119/// Stack must have a String value on top
120#[unsafe(no_mangle)]
121pub unsafe extern "C" fn patch_seq_base64url_decode(stack: Stack) -> Stack {
122    assert!(!stack.is_null(), "base64url-decode: stack is empty");
123
124    let (stack, value) = unsafe { pop(stack) };
125
126    match value {
127        Value::String(s) => match BASE64_URL_SAFE_NO_PAD.decode(s.as_str().as_bytes()) {
128            Ok(bytes) => match String::from_utf8(bytes) {
129                Ok(decoded) => {
130                    let stack = unsafe { push(stack, Value::String(global_string(decoded))) };
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            Err(_) => {
139                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
140                unsafe { push(stack, Value::Bool(false)) }
141            }
142        },
143        _ => panic!(
144            "base64url-decode: expected String on stack, got {:?}",
145            value
146        ),
147    }
148}
149
150/// Encode a string to hexadecimal (lowercase)
151///
152/// Stack effect: ( String -- String )
153///
154/// Each byte becomes two hex characters.
155///
156/// # Safety
157/// Stack must have a String value on top
158#[unsafe(no_mangle)]
159pub unsafe extern "C" fn patch_seq_hex_encode(stack: Stack) -> Stack {
160    assert!(!stack.is_null(), "hex-encode: stack is empty");
161
162    let (stack, value) = unsafe { pop(stack) };
163
164    match value {
165        Value::String(s) => {
166            let encoded = hex::encode(s.as_str().as_bytes());
167            unsafe { push(stack, Value::String(global_string(encoded))) }
168        }
169        _ => panic!("hex-encode: expected String on stack, got {:?}", value),
170    }
171}
172
173/// Decode a hexadecimal string
174///
175/// Stack effect: ( String -- String Bool )
176///
177/// Returns the decoded string and true on success, empty string and false on failure.
178/// Accepts both uppercase and lowercase hex characters.
179///
180/// # Safety
181/// Stack must have a String value on top
182#[unsafe(no_mangle)]
183pub unsafe extern "C" fn patch_seq_hex_decode(stack: Stack) -> Stack {
184    assert!(!stack.is_null(), "hex-decode: stack is empty");
185
186    let (stack, value) = unsafe { pop(stack) };
187
188    match value {
189        Value::String(s) => match hex::decode(s.as_str()) {
190            Ok(bytes) => match String::from_utf8(bytes) {
191                Ok(decoded) => {
192                    let stack = unsafe { push(stack, Value::String(global_string(decoded))) };
193                    unsafe { push(stack, Value::Bool(true)) }
194                }
195                Err(_) => {
196                    // Decoded bytes are not valid UTF-8
197                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
198                    unsafe { push(stack, Value::Bool(false)) }
199                }
200            },
201            Err(_) => {
202                // Invalid hex input
203                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
204                unsafe { push(stack, Value::Bool(false)) }
205            }
206        },
207        _ => panic!("hex-decode: expected String on stack, got {:?}", value),
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::stack::pop;
215
216    #[test]
217    fn test_base64_encode() {
218        unsafe {
219            let stack = crate::stack::alloc_test_stack();
220            let stack = push(stack, Value::String(global_string("hello".to_string())));
221            let stack = patch_seq_base64_encode(stack);
222            let (_, value) = pop(stack);
223
224            match value {
225                Value::String(s) => assert_eq!(s.as_str(), "aGVsbG8="),
226                _ => panic!("Expected String"),
227            }
228        }
229    }
230
231    #[test]
232    fn test_base64_decode_success() {
233        unsafe {
234            let stack = crate::stack::alloc_test_stack();
235            let stack = push(stack, Value::String(global_string("aGVsbG8=".to_string())));
236            let stack = patch_seq_base64_decode(stack);
237
238            let (stack, success) = pop(stack);
239            let (_, decoded) = pop(stack);
240
241            match (decoded, success) {
242                (Value::String(s), Value::Bool(true)) => assert_eq!(s.as_str(), "hello"),
243                _ => panic!("Expected (String, true)"),
244            }
245        }
246    }
247
248    #[test]
249    fn test_base64_decode_failure() {
250        unsafe {
251            let stack = crate::stack::alloc_test_stack();
252            let stack = push(
253                stack,
254                Value::String(global_string("not valid base64!!!".to_string())),
255            );
256            let stack = patch_seq_base64_decode(stack);
257
258            let (stack, success) = pop(stack);
259            let (_, decoded) = pop(stack);
260
261            match (decoded, success) {
262                (Value::String(s), Value::Bool(false)) => assert_eq!(s.as_str(), ""),
263                _ => panic!("Expected (empty String, false)"),
264            }
265        }
266    }
267
268    #[test]
269    fn test_base64url_encode() {
270        unsafe {
271            let stack = crate::stack::alloc_test_stack();
272            // Use input that produces + and / in standard base64
273            let stack = push(stack, Value::String(global_string("hello??".to_string())));
274            let stack = patch_seq_base64url_encode(stack);
275            let (_, value) = pop(stack);
276
277            match value {
278                Value::String(s) => {
279                    // Should not contain + or / or =
280                    assert!(!s.as_str().contains('+'));
281                    assert!(!s.as_str().contains('/'));
282                    assert!(!s.as_str().contains('='));
283                }
284                _ => panic!("Expected String"),
285            }
286        }
287    }
288
289    #[test]
290    fn test_base64url_roundtrip() {
291        unsafe {
292            let original = "hello world! 123";
293            let stack = crate::stack::alloc_test_stack();
294            let stack = push(stack, Value::String(global_string(original.to_string())));
295            let stack = patch_seq_base64url_encode(stack);
296            let stack = patch_seq_base64url_decode(stack);
297
298            let (stack, success) = pop(stack);
299            let (_, decoded) = pop(stack);
300
301            match (decoded, success) {
302                (Value::String(s), Value::Bool(true)) => assert_eq!(s.as_str(), original),
303                _ => panic!("Expected (String, true)"),
304            }
305        }
306    }
307
308    #[test]
309    fn test_hex_encode() {
310        unsafe {
311            let stack = crate::stack::alloc_test_stack();
312            let stack = push(stack, Value::String(global_string("hello".to_string())));
313            let stack = patch_seq_hex_encode(stack);
314            let (_, value) = pop(stack);
315
316            match value {
317                Value::String(s) => assert_eq!(s.as_str(), "68656c6c6f"),
318                _ => panic!("Expected String"),
319            }
320        }
321    }
322
323    #[test]
324    fn test_hex_decode_success() {
325        unsafe {
326            let stack = crate::stack::alloc_test_stack();
327            let stack = push(
328                stack,
329                Value::String(global_string("68656c6c6f".to_string())),
330            );
331            let stack = patch_seq_hex_decode(stack);
332
333            let (stack, success) = pop(stack);
334            let (_, decoded) = pop(stack);
335
336            match (decoded, success) {
337                (Value::String(s), Value::Bool(true)) => assert_eq!(s.as_str(), "hello"),
338                _ => panic!("Expected (String, true)"),
339            }
340        }
341    }
342
343    #[test]
344    fn test_hex_decode_uppercase() {
345        unsafe {
346            let stack = crate::stack::alloc_test_stack();
347            let stack = push(
348                stack,
349                Value::String(global_string("68656C6C6F".to_string())),
350            );
351            let stack = patch_seq_hex_decode(stack);
352
353            let (stack, success) = pop(stack);
354            let (_, decoded) = pop(stack);
355
356            match (decoded, success) {
357                (Value::String(s), Value::Bool(true)) => assert_eq!(s.as_str(), "hello"),
358                _ => panic!("Expected (String, true)"),
359            }
360        }
361    }
362
363    #[test]
364    fn test_hex_decode_failure() {
365        unsafe {
366            let stack = crate::stack::alloc_test_stack();
367            let stack = push(stack, Value::String(global_string("not hex!".to_string())));
368            let stack = patch_seq_hex_decode(stack);
369
370            let (stack, success) = pop(stack);
371            let (_, decoded) = pop(stack);
372
373            match (decoded, success) {
374                (Value::String(s), Value::Bool(false)) => assert_eq!(s.as_str(), ""),
375                _ => panic!("Expected (empty String, false)"),
376            }
377        }
378    }
379
380    #[test]
381    fn test_hex_roundtrip() {
382        unsafe {
383            let original = "Hello, World! 123";
384            let stack = crate::stack::alloc_test_stack();
385            let stack = push(stack, Value::String(global_string(original.to_string())));
386            let stack = patch_seq_hex_encode(stack);
387            let stack = patch_seq_hex_decode(stack);
388
389            let (stack, success) = pop(stack);
390            let (_, decoded) = pop(stack);
391
392            match (decoded, success) {
393                (Value::String(s), Value::Bool(true)) => assert_eq!(s.as_str(), original),
394                _ => panic!("Expected (String, true)"),
395            }
396        }
397    }
398}