Skip to main content

seq_runtime/
compress.rs

1//! Compression operations for Seq
2//!
3//! These functions are exported with C ABI for LLVM codegen to call.
4//! Uses flate2 for gzip and zstd for Zstandard compression.
5//!
6//! Compressed data is returned as base64-encoded strings for easy
7//! storage and transmission in string-based contexts.
8//!
9//! # API
10//!
11//! ```seq
12//! # Gzip compression (base64-encoded output)
13//! "hello world" compress.gzip           # ( String -- String Bool )
14//! compressed compress.gunzip            # ( String -- String Bool )
15//!
16//! # Gzip with compression level (1-9, higher = smaller but slower)
17//! "hello world" 9 compress.gzip-level   # ( String Int -- String Bool )
18//!
19//! # Zstd compression (faster, better ratios)
20//! "hello world" compress.zstd           # ( String -- String Bool )
21//! compressed compress.unzstd            # ( String -- String Bool )
22//!
23//! # Zstd with compression level (1-22, default is 3)
24//! "hello world" 19 compress.zstd-level  # ( String Int -- String Bool )
25//! ```
26
27use base64::{Engine, engine::general_purpose::STANDARD};
28use flate2::Compression;
29use flate2::read::{GzDecoder, GzEncoder};
30use seq_core::seqstring::global_string;
31use seq_core::stack::{Stack, pop, push};
32use seq_core::value::Value;
33use std::io::Read;
34
35/// Compress data using gzip with default compression level (6)
36///
37/// Stack effect: ( String -- String Bool )
38///
39/// Returns base64-encoded compressed data and success flag.
40///
41/// # Safety
42/// Stack must have a String value on top
43#[unsafe(no_mangle)]
44pub unsafe extern "C" fn patch_seq_compress_gzip(stack: Stack) -> Stack {
45    assert!(!stack.is_null(), "compress.gzip: stack is null");
46
47    let (stack, data_val) = unsafe { pop(stack) };
48
49    match data_val {
50        Value::String(data) => match gzip_compress(data.as_bytes(), Compression::default()) {
51            Some(compressed) => {
52                let encoded = STANDARD.encode(&compressed);
53                let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
54                unsafe { push(stack, Value::Bool(true)) }
55            }
56            None => {
57                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
58                unsafe { push(stack, Value::Bool(false)) }
59            }
60        },
61        _ => panic!("compress.gzip: expected String on stack"),
62    }
63}
64
65/// Compress data using gzip with specified compression level
66///
67/// Stack effect: ( String Int -- String Bool )
68///
69/// Level should be 1-9 (1=fastest, 9=best compression).
70/// Returns base64-encoded compressed data and success flag.
71///
72/// # Safety
73/// Stack must have Int and String values on top
74#[unsafe(no_mangle)]
75pub unsafe extern "C" fn patch_seq_compress_gzip_level(stack: Stack) -> Stack {
76    assert!(!stack.is_null(), "compress.gzip-level: stack is null");
77
78    let (stack, level_val) = unsafe { pop(stack) };
79    let (stack, data_val) = unsafe { pop(stack) };
80
81    match (data_val, level_val) {
82        (Value::String(data), Value::Int(level)) => {
83            let level = level.clamp(1, 9) as u32;
84            match gzip_compress(data.as_bytes(), Compression::new(level)) {
85                Some(compressed) => {
86                    let encoded = STANDARD.encode(&compressed);
87                    let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
88                    unsafe { push(stack, Value::Bool(true)) }
89                }
90                None => {
91                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
92                    unsafe { push(stack, Value::Bool(false)) }
93                }
94            }
95        }
96        _ => panic!("compress.gzip-level: expected String and Int on stack"),
97    }
98}
99
100/// Decompress gzip data
101///
102/// Stack effect: ( String -- String Bool )
103///
104/// Input should be base64-encoded gzip data.
105/// Returns decompressed string and success flag.
106///
107/// # Safety
108/// Stack must have a String value on top
109#[unsafe(no_mangle)]
110pub unsafe extern "C" fn patch_seq_compress_gunzip(stack: Stack) -> Stack {
111    assert!(!stack.is_null(), "compress.gunzip: stack is null");
112
113    let (stack, data_val) = unsafe { pop(stack) };
114
115    match data_val {
116        Value::String(data) => {
117            // Decode base64
118            let decoded = match STANDARD.decode(data.as_str_or_empty()) {
119                Ok(d) => d,
120                Err(_) => {
121                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
122                    return unsafe { push(stack, Value::Bool(false)) };
123                }
124            };
125
126            // Decompress
127            match gzip_decompress(&decoded) {
128                Some(decompressed) => {
129                    let stack = unsafe { push(stack, Value::String(global_string(decompressed))) };
130                    unsafe { push(stack, Value::Bool(true)) }
131                }
132                None => {
133                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
134                    unsafe { push(stack, Value::Bool(false)) }
135                }
136            }
137        }
138        _ => panic!("compress.gunzip: expected String on stack"),
139    }
140}
141
142/// Compress data using zstd with default compression level (3)
143///
144/// Stack effect: ( String -- String Bool )
145///
146/// Returns base64-encoded compressed data and success flag.
147///
148/// # Safety
149/// Stack must have a String value on top
150#[unsafe(no_mangle)]
151pub unsafe extern "C" fn patch_seq_compress_zstd(stack: Stack) -> Stack {
152    assert!(!stack.is_null(), "compress.zstd: stack is null");
153
154    let (stack, data_val) = unsafe { pop(stack) };
155
156    match data_val {
157        Value::String(data) => match zstd::encode_all(data.as_bytes(), 3) {
158            Ok(compressed) => {
159                let encoded = STANDARD.encode(&compressed);
160                let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
161                unsafe { push(stack, Value::Bool(true)) }
162            }
163            Err(_) => {
164                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
165                unsafe { push(stack, Value::Bool(false)) }
166            }
167        },
168        _ => panic!("compress.zstd: expected String on stack"),
169    }
170}
171
172/// Compress data using zstd with specified compression level
173///
174/// Stack effect: ( String Int -- String Bool )
175///
176/// Level should be 1-22 (higher = better compression but slower).
177/// Returns base64-encoded compressed data and success flag.
178///
179/// # Safety
180/// Stack must have Int and String values on top
181#[unsafe(no_mangle)]
182pub unsafe extern "C" fn patch_seq_compress_zstd_level(stack: Stack) -> Stack {
183    assert!(!stack.is_null(), "compress.zstd-level: stack is null");
184
185    let (stack, level_val) = unsafe { pop(stack) };
186    let (stack, data_val) = unsafe { pop(stack) };
187
188    match (data_val, level_val) {
189        (Value::String(data), Value::Int(level)) => {
190            let level = level.clamp(1, 22) as i32;
191            match zstd::encode_all(data.as_bytes(), level) {
192                Ok(compressed) => {
193                    let encoded = STANDARD.encode(&compressed);
194                    let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
195                    unsafe { push(stack, Value::Bool(true)) }
196                }
197                Err(_) => {
198                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
199                    unsafe { push(stack, Value::Bool(false)) }
200                }
201            }
202        }
203        _ => panic!("compress.zstd-level: expected String and Int on stack"),
204    }
205}
206
207/// Decompress zstd data
208///
209/// Stack effect: ( String -- String Bool )
210///
211/// Input should be base64-encoded zstd data.
212/// Returns decompressed string and success flag.
213///
214/// # Safety
215/// Stack must have a String value on top
216#[unsafe(no_mangle)]
217pub unsafe extern "C" fn patch_seq_compress_unzstd(stack: Stack) -> Stack {
218    assert!(!stack.is_null(), "compress.unzstd: stack is null");
219
220    let (stack, data_val) = unsafe { pop(stack) };
221
222    match data_val {
223        Value::String(data) => {
224            // Decode base64
225            let decoded = match STANDARD.decode(data.as_str_or_empty()) {
226                Ok(d) => d,
227                Err(_) => {
228                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
229                    return unsafe { push(stack, Value::Bool(false)) };
230                }
231            };
232
233            // Decompress
234            match zstd::decode_all(decoded.as_slice()) {
235                Ok(decompressed) => match String::from_utf8(decompressed) {
236                    Ok(s) => {
237                        let stack = unsafe { push(stack, Value::String(global_string(s))) };
238                        unsafe { push(stack, Value::Bool(true)) }
239                    }
240                    Err(_) => {
241                        let stack =
242                            unsafe { push(stack, Value::String(global_string(String::new()))) };
243                        unsafe { push(stack, Value::Bool(false)) }
244                    }
245                },
246                Err(_) => {
247                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
248                    unsafe { push(stack, Value::Bool(false)) }
249                }
250            }
251        }
252        _ => panic!("compress.unzstd: expected String on stack"),
253    }
254}
255
256// Helper functions
257
258fn gzip_compress(data: &[u8], level: Compression) -> Option<Vec<u8>> {
259    let mut encoder = GzEncoder::new(data, level);
260    let mut compressed = Vec::new();
261    match encoder.read_to_end(&mut compressed) {
262        Ok(_) => Some(compressed),
263        Err(_) => None,
264    }
265}
266
267fn gzip_decompress(data: &[u8]) -> Option<String> {
268    let mut decoder = GzDecoder::new(data);
269    let mut decompressed = String::new();
270    match decoder.read_to_string(&mut decompressed) {
271        Ok(_) => Some(decompressed),
272        Err(_) => None,
273    }
274}
275
276#[cfg(test)]
277mod tests;