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//! # 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;