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) => {
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;