1use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
7use shape_value::ValueWord;
8use std::sync::Arc;
9use super::byte_utils::{bytes_from_array, bytes_to_array};
10
11pub fn create_compress_module() -> ModuleExports {
13 let mut module = ModuleExports::new("std::core::compress");
14 module.description = "Data compression and decompression (gzip, zstd, deflate)".to_string();
15
16 module.add_function_with_schema(
18 "gzip",
19 |args: &[ValueWord], _ctx: &ModuleContext| {
20 use flate2::Compression;
21 use flate2::write::GzEncoder;
22 use std::io::Write;
23
24 let data = args
25 .first()
26 .and_then(|a| a.as_str())
27 .ok_or_else(|| "compress.gzip() requires a string argument".to_string())?;
28
29 let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
30 encoder
31 .write_all(data.as_bytes())
32 .map_err(|e| format!("compress.gzip() failed: {}", e))?;
33 let compressed = encoder
34 .finish()
35 .map_err(|e| format!("compress.gzip() failed: {}", e))?;
36
37 Ok(bytes_to_array(&compressed))
38 },
39 ModuleFunction {
40 description: "Compress a string using gzip, returning a byte array".to_string(),
41 params: vec![ModuleParam {
42 name: "data".to_string(),
43 type_name: "string".to_string(),
44 required: true,
45 description: "String data to compress".to_string(),
46 ..Default::default()
47 }],
48 return_type: Some("Array<int>".to_string()),
49 },
50 );
51
52 module.add_function_with_schema(
54 "gunzip",
55 |args: &[ValueWord], _ctx: &ModuleContext| {
56 use flate2::read::GzDecoder;
57 use std::io::Read;
58
59 let input = args
60 .first()
61 .ok_or_else(|| "compress.gunzip() requires an Array<int> argument".to_string())?;
62 let bytes = bytes_from_array(input).map_err(|e| format!("compress.gunzip(): {}", e))?;
63
64 let mut decoder = GzDecoder::new(&bytes[..]);
65 let mut output = String::new();
66 decoder
67 .read_to_string(&mut output)
68 .map_err(|e| format!("compress.gunzip() failed: {}", e))?;
69
70 Ok(ValueWord::from_string(Arc::new(output)))
71 },
72 ModuleFunction {
73 description: "Decompress a gzip byte array back to a string".to_string(),
74 params: vec![ModuleParam {
75 name: "data".to_string(),
76 type_name: "Array<int>".to_string(),
77 required: true,
78 description: "Gzip-compressed byte array".to_string(),
79 ..Default::default()
80 }],
81 return_type: Some("string".to_string()),
82 },
83 );
84
85 module.add_function_with_schema(
87 "zstd",
88 |args: &[ValueWord], _ctx: &ModuleContext| {
89 let data = args
90 .first()
91 .and_then(|a| a.as_str())
92 .ok_or_else(|| "compress.zstd() requires a string argument".to_string())?;
93
94 let level = args
95 .get(1)
96 .and_then(|a| a.as_i64().or_else(|| a.as_f64().map(|n| n as i64)))
97 .unwrap_or(3) as i32;
98
99 let compressed = zstd::encode_all(data.as_bytes(), level)
100 .map_err(|e| format!("compress.zstd() failed: {}", e))?;
101
102 Ok(bytes_to_array(&compressed))
103 },
104 ModuleFunction {
105 description: "Compress a string using Zstandard, returning a byte array".to_string(),
106 params: vec![
107 ModuleParam {
108 name: "data".to_string(),
109 type_name: "string".to_string(),
110 required: true,
111 description: "String data to compress".to_string(),
112 ..Default::default()
113 },
114 ModuleParam {
115 name: "level".to_string(),
116 type_name: "int".to_string(),
117 required: false,
118 description: "Compression level (default: 3)".to_string(),
119 default_snippet: Some("3".to_string()),
120 ..Default::default()
121 },
122 ],
123 return_type: Some("Array<int>".to_string()),
124 },
125 );
126
127 module.add_function_with_schema(
129 "unzstd",
130 |args: &[ValueWord], _ctx: &ModuleContext| {
131 let input = args
132 .first()
133 .ok_or_else(|| "compress.unzstd() requires an Array<int> argument".to_string())?;
134 let bytes = bytes_from_array(input).map_err(|e| format!("compress.unzstd(): {}", e))?;
135
136 let decompressed = zstd::decode_all(&bytes[..])
137 .map_err(|e| format!("compress.unzstd() failed: {}", e))?;
138
139 let output = String::from_utf8(decompressed)
140 .map_err(|e| format!("compress.unzstd() invalid UTF-8: {}", e))?;
141
142 Ok(ValueWord::from_string(Arc::new(output)))
143 },
144 ModuleFunction {
145 description: "Decompress a Zstandard byte array back to a string".to_string(),
146 params: vec![ModuleParam {
147 name: "data".to_string(),
148 type_name: "Array<int>".to_string(),
149 required: true,
150 description: "Zstd-compressed byte array".to_string(),
151 ..Default::default()
152 }],
153 return_type: Some("string".to_string()),
154 },
155 );
156
157 module.add_function_with_schema(
159 "deflate",
160 |args: &[ValueWord], _ctx: &ModuleContext| {
161 use flate2::Compression;
162 use flate2::write::DeflateEncoder;
163 use std::io::Write;
164
165 let data = args
166 .first()
167 .and_then(|a| a.as_str())
168 .ok_or_else(|| "compress.deflate() requires a string argument".to_string())?;
169
170 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
171 encoder
172 .write_all(data.as_bytes())
173 .map_err(|e| format!("compress.deflate() failed: {}", e))?;
174 let compressed = encoder
175 .finish()
176 .map_err(|e| format!("compress.deflate() failed: {}", e))?;
177
178 Ok(bytes_to_array(&compressed))
179 },
180 ModuleFunction {
181 description: "Compress a string using raw deflate, returning a byte array".to_string(),
182 params: vec![ModuleParam {
183 name: "data".to_string(),
184 type_name: "string".to_string(),
185 required: true,
186 description: "String data to compress".to_string(),
187 ..Default::default()
188 }],
189 return_type: Some("Array<int>".to_string()),
190 },
191 );
192
193 module.add_function_with_schema(
195 "inflate",
196 |args: &[ValueWord], _ctx: &ModuleContext| {
197 use flate2::read::DeflateDecoder;
198 use std::io::Read;
199
200 let input = args
201 .first()
202 .ok_or_else(|| "compress.inflate() requires an Array<int> argument".to_string())?;
203 let bytes =
204 bytes_from_array(input).map_err(|e| format!("compress.inflate(): {}", e))?;
205
206 let mut decoder = DeflateDecoder::new(&bytes[..]);
207 let mut output = String::new();
208 decoder
209 .read_to_string(&mut output)
210 .map_err(|e| format!("compress.inflate() failed: {}", e))?;
211
212 Ok(ValueWord::from_string(Arc::new(output)))
213 },
214 ModuleFunction {
215 description: "Decompress a raw deflate byte array back to a string".to_string(),
216 params: vec![ModuleParam {
217 name: "data".to_string(),
218 type_name: "Array<int>".to_string(),
219 required: true,
220 description: "Deflate-compressed byte array".to_string(),
221 ..Default::default()
222 }],
223 return_type: Some("string".to_string()),
224 },
225 );
226
227 module
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
235 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
236 crate::module_exports::ModuleContext {
237 schemas: registry,
238 invoke_callable: None,
239 raw_invoker: None,
240 function_hashes: None,
241 vm_state: None,
242 granted_permissions: None,
243 scope_constraints: None,
244 set_pending_resume: None,
245 set_pending_frame_resume: None,
246 }
247 }
248
249 #[test]
250 fn test_compress_module_creation() {
251 let module = create_compress_module();
252 assert_eq!(module.name, "std::core::compress");
253 assert!(module.has_export("gzip"));
254 assert!(module.has_export("gunzip"));
255 assert!(module.has_export("zstd"));
256 assert!(module.has_export("unzstd"));
257 assert!(module.has_export("deflate"));
258 assert!(module.has_export("inflate"));
259 }
260
261 #[test]
262 fn test_gzip_roundtrip() {
263 let module = create_compress_module();
264 let ctx = test_ctx();
265 let gzip_fn = module.get_export("gzip").unwrap();
266 let gunzip_fn = module.get_export("gunzip").unwrap();
267
268 let input = ValueWord::from_string(Arc::new("hello world".to_string()));
269 let compressed = gzip_fn(&[input], &ctx).unwrap();
270
271 assert!(compressed.as_any_array().is_some());
273
274 let decompressed = gunzip_fn(&[compressed], &ctx).unwrap();
275 assert_eq!(decompressed.as_str(), Some("hello world"));
276 }
277
278 #[test]
279 fn test_zstd_roundtrip() {
280 let module = create_compress_module();
281 let ctx = test_ctx();
282 let zstd_fn = module.get_export("zstd").unwrap();
283 let unzstd_fn = module.get_export("unzstd").unwrap();
284
285 let input = ValueWord::from_string(Arc::new("hello zstd compression".to_string()));
286 let compressed = zstd_fn(&[input], &ctx).unwrap();
287
288 assert!(compressed.as_any_array().is_some());
289
290 let decompressed = unzstd_fn(&[compressed], &ctx).unwrap();
291 assert_eq!(decompressed.as_str(), Some("hello zstd compression"));
292 }
293
294 #[test]
295 fn test_zstd_with_level() {
296 let module = create_compress_module();
297 let ctx = test_ctx();
298 let zstd_fn = module.get_export("zstd").unwrap();
299 let unzstd_fn = module.get_export("unzstd").unwrap();
300
301 let input = ValueWord::from_string(Arc::new("level test".to_string()));
302 let level = ValueWord::from_i64(1);
303 let compressed = zstd_fn(&[input, level], &ctx).unwrap();
304
305 let decompressed = unzstd_fn(&[compressed], &ctx).unwrap();
306 assert_eq!(decompressed.as_str(), Some("level test"));
307 }
308
309 #[test]
310 fn test_deflate_roundtrip() {
311 let module = create_compress_module();
312 let ctx = test_ctx();
313 let deflate_fn = module.get_export("deflate").unwrap();
314 let inflate_fn = module.get_export("inflate").unwrap();
315
316 let input = ValueWord::from_string(Arc::new("deflate test data".to_string()));
317 let compressed = deflate_fn(&[input], &ctx).unwrap();
318
319 assert!(compressed.as_any_array().is_some());
320
321 let decompressed = inflate_fn(&[compressed], &ctx).unwrap();
322 assert_eq!(decompressed.as_str(), Some("deflate test data"));
323 }
324
325 #[test]
326 fn test_gzip_requires_string() {
327 let module = create_compress_module();
328 let ctx = test_ctx();
329 let gzip_fn = module.get_export("gzip").unwrap();
330
331 let result = gzip_fn(&[ValueWord::from_i64(42)], &ctx);
332 assert!(result.is_err());
333 }
334
335 #[test]
336 fn test_gunzip_invalid_data() {
337 let module = create_compress_module();
338 let ctx = test_ctx();
339 let gunzip_fn = module.get_export("gunzip").unwrap();
340
341 let bad_data = ValueWord::from_array(Arc::new(vec![
342 ValueWord::from_i64(1),
343 ValueWord::from_i64(2),
344 ValueWord::from_i64(3),
345 ]));
346 let result = gunzip_fn(&[bad_data], &ctx);
347 assert!(result.is_err());
348 }
349
350 #[test]
351 fn test_empty_string_roundtrip() {
352 let module = create_compress_module();
353 let ctx = test_ctx();
354 let gzip_fn = module.get_export("gzip").unwrap();
355 let gunzip_fn = module.get_export("gunzip").unwrap();
356
357 let input = ValueWord::from_string(Arc::new(String::new()));
358 let compressed = gzip_fn(&[input], &ctx).unwrap();
359 let decompressed = gunzip_fn(&[compressed], &ctx).unwrap();
360 assert_eq!(decompressed.as_str(), Some(""));
361 }
362
363 #[test]
364 fn test_large_data_roundtrip() {
365 let module = create_compress_module();
366 let ctx = test_ctx();
367 let gzip_fn = module.get_export("gzip").unwrap();
368 let gunzip_fn = module.get_export("gunzip").unwrap();
369
370 let large = "a".repeat(10_000);
371 let input = ValueWord::from_string(Arc::new(large.clone()));
372 let compressed = gzip_fn(&[input], &ctx).unwrap();
373
374 let arr = compressed.as_any_array().unwrap().to_generic();
376 assert!(arr.len() < 10_000);
377
378 let decompressed = gunzip_fn(&[compressed], &ctx).unwrap();
379 assert_eq!(decompressed.as_str(), Some(large.as_str()));
380 }
381
382 #[test]
383 fn test_schemas() {
384 let module = create_compress_module();
385
386 let gzip_schema = module.get_schema("gzip").unwrap();
387 assert_eq!(gzip_schema.params.len(), 1);
388 assert!(gzip_schema.params[0].required);
389 assert_eq!(gzip_schema.return_type.as_deref(), Some("Array<int>"));
390
391 let zstd_schema = module.get_schema("zstd").unwrap();
392 assert_eq!(zstd_schema.params.len(), 2);
393 assert!(zstd_schema.params[0].required);
394 assert!(!zstd_schema.params[1].required);
395
396 let gunzip_schema = module.get_schema("gunzip").unwrap();
397 assert_eq!(gunzip_schema.return_type.as_deref(), Some("string"));
398 }
399}