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) => {
51            match gzip_compress(data.as_str().as_bytes(), Compression::default()) {
52                Some(compressed) => {
53                    let encoded = STANDARD.encode(&compressed);
54                    let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
55                    unsafe { push(stack, Value::Bool(true)) }
56                }
57                None => {
58                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
59                    unsafe { push(stack, Value::Bool(false)) }
60                }
61            }
62        }
63        _ => panic!("compress.gzip: expected String on stack"),
64    }
65}
66
67/// Compress data using gzip with specified compression level
68///
69/// Stack effect: ( String Int -- String Bool )
70///
71/// Level should be 1-9 (1=fastest, 9=best compression).
72/// Returns base64-encoded compressed data and success flag.
73///
74/// # Safety
75/// Stack must have Int and String values on top
76#[unsafe(no_mangle)]
77pub unsafe extern "C" fn patch_seq_compress_gzip_level(stack: Stack) -> Stack {
78    assert!(!stack.is_null(), "compress.gzip-level: stack is null");
79
80    let (stack, level_val) = unsafe { pop(stack) };
81    let (stack, data_val) = unsafe { pop(stack) };
82
83    match (data_val, level_val) {
84        (Value::String(data), Value::Int(level)) => {
85            let level = level.clamp(1, 9) as u32;
86            match gzip_compress(data.as_str().as_bytes(), Compression::new(level)) {
87                Some(compressed) => {
88                    let encoded = STANDARD.encode(&compressed);
89                    let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
90                    unsafe { push(stack, Value::Bool(true)) }
91                }
92                None => {
93                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
94                    unsafe { push(stack, Value::Bool(false)) }
95                }
96            }
97        }
98        _ => panic!("compress.gzip-level: expected String and Int on stack"),
99    }
100}
101
102/// Decompress gzip data
103///
104/// Stack effect: ( String -- String Bool )
105///
106/// Input should be base64-encoded gzip data.
107/// Returns decompressed string and success flag.
108///
109/// # Safety
110/// Stack must have a String value on top
111#[unsafe(no_mangle)]
112pub unsafe extern "C" fn patch_seq_compress_gunzip(stack: Stack) -> Stack {
113    assert!(!stack.is_null(), "compress.gunzip: stack is null");
114
115    let (stack, data_val) = unsafe { pop(stack) };
116
117    match data_val {
118        Value::String(data) => {
119            // Decode base64
120            let decoded = match STANDARD.decode(data.as_str()) {
121                Ok(d) => d,
122                Err(_) => {
123                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
124                    return unsafe { push(stack, Value::Bool(false)) };
125                }
126            };
127
128            // Decompress
129            match gzip_decompress(&decoded) {
130                Some(decompressed) => {
131                    let stack = unsafe { push(stack, Value::String(global_string(decompressed))) };
132                    unsafe { push(stack, Value::Bool(true)) }
133                }
134                None => {
135                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
136                    unsafe { push(stack, Value::Bool(false)) }
137                }
138            }
139        }
140        _ => panic!("compress.gunzip: expected String on stack"),
141    }
142}
143
144/// Compress data using zstd with default compression level (3)
145///
146/// Stack effect: ( String -- String Bool )
147///
148/// Returns base64-encoded compressed data and success flag.
149///
150/// # Safety
151/// Stack must have a String value on top
152#[unsafe(no_mangle)]
153pub unsafe extern "C" fn patch_seq_compress_zstd(stack: Stack) -> Stack {
154    assert!(!stack.is_null(), "compress.zstd: stack is null");
155
156    let (stack, data_val) = unsafe { pop(stack) };
157
158    match data_val {
159        Value::String(data) => match zstd::encode_all(data.as_str().as_bytes(), 3) {
160            Ok(compressed) => {
161                let encoded = STANDARD.encode(&compressed);
162                let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
163                unsafe { push(stack, Value::Bool(true)) }
164            }
165            Err(_) => {
166                let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
167                unsafe { push(stack, Value::Bool(false)) }
168            }
169        },
170        _ => panic!("compress.zstd: expected String on stack"),
171    }
172}
173
174/// Compress data using zstd with specified compression level
175///
176/// Stack effect: ( String Int -- String Bool )
177///
178/// Level should be 1-22 (higher = better compression but slower).
179/// Returns base64-encoded compressed data and success flag.
180///
181/// # Safety
182/// Stack must have Int and String values on top
183#[unsafe(no_mangle)]
184pub unsafe extern "C" fn patch_seq_compress_zstd_level(stack: Stack) -> Stack {
185    assert!(!stack.is_null(), "compress.zstd-level: stack is null");
186
187    let (stack, level_val) = unsafe { pop(stack) };
188    let (stack, data_val) = unsafe { pop(stack) };
189
190    match (data_val, level_val) {
191        (Value::String(data), Value::Int(level)) => {
192            let level = level.clamp(1, 22) as i32;
193            match zstd::encode_all(data.as_str().as_bytes(), level) {
194                Ok(compressed) => {
195                    let encoded = STANDARD.encode(&compressed);
196                    let stack = unsafe { push(stack, Value::String(global_string(encoded))) };
197                    unsafe { push(stack, Value::Bool(true)) }
198                }
199                Err(_) => {
200                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
201                    unsafe { push(stack, Value::Bool(false)) }
202                }
203            }
204        }
205        _ => panic!("compress.zstd-level: expected String and Int on stack"),
206    }
207}
208
209/// Decompress zstd data
210///
211/// Stack effect: ( String -- String Bool )
212///
213/// Input should be base64-encoded zstd data.
214/// Returns decompressed string and success flag.
215///
216/// # Safety
217/// Stack must have a String value on top
218#[unsafe(no_mangle)]
219pub unsafe extern "C" fn patch_seq_compress_unzstd(stack: Stack) -> Stack {
220    assert!(!stack.is_null(), "compress.unzstd: stack is null");
221
222    let (stack, data_val) = unsafe { pop(stack) };
223
224    match data_val {
225        Value::String(data) => {
226            // Decode base64
227            let decoded = match STANDARD.decode(data.as_str()) {
228                Ok(d) => d,
229                Err(_) => {
230                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
231                    return unsafe { push(stack, Value::Bool(false)) };
232                }
233            };
234
235            // Decompress
236            match zstd::decode_all(decoded.as_slice()) {
237                Ok(decompressed) => match String::from_utf8(decompressed) {
238                    Ok(s) => {
239                        let stack = unsafe { push(stack, Value::String(global_string(s))) };
240                        unsafe { push(stack, Value::Bool(true)) }
241                    }
242                    Err(_) => {
243                        let stack =
244                            unsafe { push(stack, Value::String(global_string(String::new()))) };
245                        unsafe { push(stack, Value::Bool(false)) }
246                    }
247                },
248                Err(_) => {
249                    let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
250                    unsafe { push(stack, Value::Bool(false)) }
251                }
252            }
253        }
254        _ => panic!("compress.unzstd: expected String on stack"),
255    }
256}
257
258// Helper functions
259
260fn gzip_compress(data: &[u8], level: Compression) -> Option<Vec<u8>> {
261    let mut encoder = GzEncoder::new(data, level);
262    let mut compressed = Vec::new();
263    match encoder.read_to_end(&mut compressed) {
264        Ok(_) => Some(compressed),
265        Err(_) => None,
266    }
267}
268
269fn gzip_decompress(data: &[u8]) -> Option<String> {
270    let mut decoder = GzDecoder::new(data);
271    let mut decompressed = String::new();
272    match decoder.read_to_string(&mut decompressed) {
273        Ok(_) => Some(decompressed),
274        Err(_) => None,
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281    use seq_core::stack::alloc_stack;
282
283    #[test]
284    fn test_gzip_roundtrip() {
285        let stack = alloc_stack();
286        let stack = unsafe {
287            push(
288                stack,
289                Value::String(global_string("hello world".to_string())),
290            )
291        };
292
293        // Compress
294        let stack = unsafe { patch_seq_compress_gzip(stack) };
295
296        // Check compress success flag
297        let (stack, compress_success) = unsafe { pop(stack) };
298        assert_eq!(compress_success, Value::Bool(true));
299
300        // Decompress
301        let stack = unsafe { patch_seq_compress_gunzip(stack) };
302
303        // Check decompress success flag
304        let (stack, success) = unsafe { pop(stack) };
305        assert_eq!(success, Value::Bool(true));
306
307        let (_, result) = unsafe { pop(stack) };
308        if let Value::String(s) = result {
309            assert_eq!(s.as_str(), "hello world");
310        } else {
311            panic!("expected string");
312        }
313    }
314
315    #[test]
316    fn test_gzip_level() {
317        let stack = alloc_stack();
318        let stack = unsafe {
319            push(
320                stack,
321                Value::String(global_string("hello world".to_string())),
322            )
323        };
324        let stack = unsafe { push(stack, Value::Int(9)) };
325
326        // Compress with max level
327        let stack = unsafe { patch_seq_compress_gzip_level(stack) };
328
329        // Check compress success flag
330        let (stack, compress_success) = unsafe { pop(stack) };
331        assert_eq!(compress_success, Value::Bool(true));
332
333        // Decompress
334        let stack = unsafe { patch_seq_compress_gunzip(stack) };
335        let (stack, success) = unsafe { pop(stack) };
336        assert_eq!(success, Value::Bool(true));
337
338        let (_, result) = unsafe { pop(stack) };
339        if let Value::String(s) = result {
340            assert_eq!(s.as_str(), "hello world");
341        } else {
342            panic!("expected string");
343        }
344    }
345
346    #[test]
347    fn test_zstd_roundtrip() {
348        let stack = alloc_stack();
349        let stack = unsafe {
350            push(
351                stack,
352                Value::String(global_string("hello world".to_string())),
353            )
354        };
355
356        // Compress
357        let stack = unsafe { patch_seq_compress_zstd(stack) };
358
359        // Check compress success flag
360        let (stack, compress_success) = unsafe { pop(stack) };
361        assert_eq!(compress_success, Value::Bool(true));
362
363        // Decompress
364        let stack = unsafe { patch_seq_compress_unzstd(stack) };
365
366        // Check decompress success flag
367        let (stack, success) = unsafe { pop(stack) };
368        assert_eq!(success, Value::Bool(true));
369
370        let (_, result) = unsafe { pop(stack) };
371        if let Value::String(s) = result {
372            assert_eq!(s.as_str(), "hello world");
373        } else {
374            panic!("expected string");
375        }
376    }
377
378    #[test]
379    fn test_zstd_level() {
380        let stack = alloc_stack();
381        let stack = unsafe {
382            push(
383                stack,
384                Value::String(global_string("hello world".to_string())),
385            )
386        };
387        let stack = unsafe { push(stack, Value::Int(19)) };
388
389        // Compress with high level
390        let stack = unsafe { patch_seq_compress_zstd_level(stack) };
391
392        // Check compress success flag
393        let (stack, compress_success) = unsafe { pop(stack) };
394        assert_eq!(compress_success, Value::Bool(true));
395
396        // Decompress
397        let stack = unsafe { patch_seq_compress_unzstd(stack) };
398        let (stack, success) = unsafe { pop(stack) };
399        assert_eq!(success, Value::Bool(true));
400
401        let (_, result) = unsafe { pop(stack) };
402        if let Value::String(s) = result {
403            assert_eq!(s.as_str(), "hello world");
404        } else {
405            panic!("expected string");
406        }
407    }
408
409    #[test]
410    fn test_gunzip_invalid_base64() {
411        let stack = alloc_stack();
412        let stack = unsafe {
413            push(
414                stack,
415                Value::String(global_string("not valid base64!!!".to_string())),
416            )
417        };
418
419        let stack = unsafe { patch_seq_compress_gunzip(stack) };
420        let (_, success) = unsafe { pop(stack) };
421        assert_eq!(success, Value::Bool(false));
422    }
423
424    #[test]
425    fn test_gunzip_invalid_gzip() {
426        let stack = alloc_stack();
427        // Valid base64 but not gzip data
428        let stack = unsafe {
429            push(
430                stack,
431                Value::String(global_string("aGVsbG8gd29ybGQ=".to_string())),
432            )
433        };
434
435        let stack = unsafe { patch_seq_compress_gunzip(stack) };
436        let (_, success) = unsafe { pop(stack) };
437        assert_eq!(success, Value::Bool(false));
438    }
439
440    #[test]
441    fn test_unzstd_invalid() {
442        let stack = alloc_stack();
443        // Valid base64 but not zstd data
444        let stack = unsafe {
445            push(
446                stack,
447                Value::String(global_string("aGVsbG8gd29ybGQ=".to_string())),
448            )
449        };
450
451        let stack = unsafe { patch_seq_compress_unzstd(stack) };
452        let (_, success) = unsafe { pop(stack) };
453        assert_eq!(success, Value::Bool(false));
454    }
455
456    #[test]
457    fn test_empty_string() {
458        let stack = alloc_stack();
459        let stack = unsafe { push(stack, Value::String(global_string(String::new()))) };
460
461        // Compress empty string
462        let stack = unsafe { patch_seq_compress_gzip(stack) };
463
464        // Check compress success flag
465        let (stack, compress_success) = unsafe { pop(stack) };
466        assert_eq!(compress_success, Value::Bool(true));
467
468        // Decompress
469        let stack = unsafe { patch_seq_compress_gunzip(stack) };
470        let (stack, success) = unsafe { pop(stack) };
471        assert_eq!(success, Value::Bool(true));
472
473        let (_, result) = unsafe { pop(stack) };
474        if let Value::String(s) = result {
475            assert_eq!(s.as_str(), "");
476        } else {
477            panic!("expected string");
478        }
479    }
480
481    #[test]
482    fn test_large_data() {
483        let stack = alloc_stack();
484        let large_data = "x".repeat(10000);
485        let stack = unsafe { push(stack, Value::String(global_string(large_data.clone()))) };
486
487        // Compress
488        let stack = unsafe { patch_seq_compress_zstd(stack) };
489
490        // Check compress success flag
491        let (stack, compress_success) = unsafe { pop(stack) };
492        assert_eq!(compress_success, Value::Bool(true));
493
494        // Decompress
495        let stack = unsafe { patch_seq_compress_unzstd(stack) };
496        let (stack, success) = unsafe { pop(stack) };
497        assert_eq!(success, Value::Bool(true));
498
499        let (_, result) = unsafe { pop(stack) };
500        if let Value::String(s) = result {
501            assert_eq!(s.as_str(), large_data);
502        } else {
503            panic!("expected string");
504        }
505    }
506}