1use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use shape_value::ValueWord;
7use shape_value::heap_value::HeapValue;
8use std::sync::Arc;
9use super::byte_utils::{bytes_from_array, bytes_to_array};
10
11fn extract_entries(val: &ValueWord) -> Result<Vec<(String, String)>, String> {
14 let arr = val
15 .as_any_array()
16 .ok_or_else(|| "expected an Array of entry objects".to_string())?
17 .to_generic();
18
19 let mut entries = Vec::with_capacity(arr.len());
20 for (i, item) in arr.iter().enumerate() {
21 let (name, data) =
22 extract_entry_fields(item).map_err(|e| format!("entry [{}]: {}", i, e))?;
23 entries.push((name, data));
24 }
25 Ok(entries)
26}
27
28fn extract_entry_fields(val: &ValueWord) -> Result<(String, String), String> {
30 if let Some(HeapValue::TypedObject {
32 slots, heap_mask, ..
33 }) = val.as_heap_ref()
34 {
35 if slots.len() >= 2 {
37 let name_nb = if heap_mask & 1 != 0 {
38 slots[0].as_heap_nb()
39 } else {
40 unsafe { ValueWord::clone_from_bits(slots[0].raw()) }
41 };
42 let data_nb = if heap_mask & 2 != 0 {
43 slots[1].as_heap_nb()
44 } else {
45 unsafe { ValueWord::clone_from_bits(slots[1].raw()) }
46 };
47 if let (Some(name), Some(data)) = (name_nb.as_str(), data_nb.as_str()) {
48 return Ok((name.to_string(), data.to_string()));
49 }
50 }
51 }
52
53 if let Some((keys, values, _)) = val.as_hashmap() {
55 let mut name = None;
56 let mut data = None;
57 for (k, v) in keys.iter().zip(values.iter()) {
58 if let Some(key_str) = k.as_str() {
59 match key_str {
60 "name" => name = v.as_str().map(|s| s.to_string()),
61 "data" => data = v.as_str().map(|s| s.to_string()),
62 _ => {}
63 }
64 }
65 }
66 if let (Some(n), Some(d)) = (name, data) {
67 return Ok((n, d));
68 }
69 }
70
71 Err("entry must have 'name' (string) and 'data' (string) fields".to_string())
72}
73
74fn make_entry(name: &str, data: &str) -> ValueWord {
76 let keys = vec![
77 ValueWord::from_string(Arc::new("name".to_string())),
78 ValueWord::from_string(Arc::new("data".to_string())),
79 ];
80 let values = vec![
81 ValueWord::from_string(Arc::new(name.to_string())),
82 ValueWord::from_string(Arc::new(data.to_string())),
83 ];
84 ValueWord::from_hashmap_pairs(keys, values)
85}
86
87pub fn create_archive_module() -> ModuleExports {
89 let mut module = ModuleExports::new("std::core::archive");
90 module.description = "Archive creation and extraction (zip, tar)".to_string();
91
92 module.add_function_with_schema(
94 "zip_create",
95 |args: &[ValueWord], _ctx: &ModuleContext| {
96 use std::io::{Cursor, Write};
97
98 let entries_val = args
99 .first()
100 .ok_or_else(|| "archive.zip_create() requires an entries array".to_string())?;
101 let entries =
102 extract_entries(entries_val).map_err(|e| format!("archive.zip_create(): {}", e))?;
103
104 let buf = Cursor::new(Vec::new());
105 let mut zip_writer = zip::ZipWriter::new(buf);
106
107 let options = zip::write::SimpleFileOptions::default()
108 .compression_method(zip::CompressionMethod::Deflated);
109
110 for (name, data) in &entries {
111 zip_writer.start_file(name.as_str(), options).map_err(|e| {
112 format!(
113 "archive.zip_create() failed to start file '{}': {}",
114 name, e
115 )
116 })?;
117 zip_writer.write_all(data.as_bytes()).map_err(|e| {
118 format!("archive.zip_create() failed to write '{}': {}", name, e)
119 })?;
120 }
121
122 let cursor = zip_writer
123 .finish()
124 .map_err(|e| format!("archive.zip_create() failed to finish: {}", e))?;
125
126 Ok(bytes_to_array(&cursor.into_inner()))
127 },
128 ModuleFunction {
129 description: "Create a zip archive in memory from an array of entries".to_string(),
130 params: vec![ModuleParam {
131 name: "entries".to_string(),
132 type_name: "Array<{name: string, data: string}>".to_string(),
133 required: true,
134 description: "Array of objects with 'name' and 'data' fields".to_string(),
135 ..Default::default()
136 }],
137 return_type: Some("Array<int>".to_string()),
138 },
139 );
140
141 module.add_function_with_schema(
143 "zip_extract",
144 |args: &[ValueWord], _ctx: &ModuleContext| {
145 use std::io::{Cursor, Read};
146
147 let input = args.first().ok_or_else(|| {
148 "archive.zip_extract() requires an Array<int> argument".to_string()
149 })?;
150 let bytes =
151 bytes_from_array(input).map_err(|e| format!("archive.zip_extract(): {}", e))?;
152
153 let cursor = Cursor::new(bytes);
154 let mut archive = zip::ZipArchive::new(cursor)
155 .map_err(|e| format!("archive.zip_extract() invalid zip: {}", e))?;
156
157 let mut entries = Vec::new();
158 for i in 0..archive.len() {
159 let mut file = archive.by_index(i).map_err(|e| {
160 format!("archive.zip_extract() failed to read entry {}: {}", i, e)
161 })?;
162
163 if file.is_dir() {
164 continue;
165 }
166
167 let name = file.name().to_string();
168 let mut contents = String::new();
169 file.read_to_string(&mut contents).map_err(|e| {
170 format!("archive.zip_extract() failed to read '{}': {}", name, e)
171 })?;
172
173 entries.push(make_entry(&name, &contents));
174 }
175
176 Ok(ValueWord::from_array(Arc::new(entries)))
177 },
178 ModuleFunction {
179 description: "Extract a zip archive from a byte array into an array of entries"
180 .to_string(),
181 params: vec![ModuleParam {
182 name: "data".to_string(),
183 type_name: "Array<int>".to_string(),
184 required: true,
185 description: "Zip archive as byte array".to_string(),
186 ..Default::default()
187 }],
188 return_type: Some("Array<{name: string, data: string}>".to_string()),
189 },
190 );
191
192 module.add_function_with_schema(
194 "tar_create",
195 |args: &[ValueWord], _ctx: &ModuleContext| {
196 let entries_val = args
197 .first()
198 .ok_or_else(|| "archive.tar_create() requires an entries array".to_string())?;
199 let entries =
200 extract_entries(entries_val).map_err(|e| format!("archive.tar_create(): {}", e))?;
201
202 let mut builder = tar::Builder::new(Vec::new());
203
204 for (name, data) in &entries {
205 let data_bytes = data.as_bytes();
206 let mut header = tar::Header::new_gnu();
207 header.set_size(data_bytes.len() as u64);
208 header.set_mode(0o644);
209 header.set_cksum();
210
211 builder
212 .append_data(&mut header, name.as_str(), data_bytes)
213 .map_err(|e| format!("archive.tar_create() failed for '{}': {}", name, e))?;
214 }
215
216 let tar_bytes = builder
217 .into_inner()
218 .map_err(|e| format!("archive.tar_create() failed to finish: {}", e))?;
219
220 Ok(bytes_to_array(&tar_bytes))
221 },
222 ModuleFunction {
223 description: "Create a tar archive in memory from an array of entries".to_string(),
224 params: vec![ModuleParam {
225 name: "entries".to_string(),
226 type_name: "Array<{name: string, data: string}>".to_string(),
227 required: true,
228 description: "Array of objects with 'name' and 'data' fields".to_string(),
229 ..Default::default()
230 }],
231 return_type: Some("Array<int>".to_string()),
232 },
233 );
234
235 module.add_function_with_schema(
237 "tar_extract",
238 |args: &[ValueWord], _ctx: &ModuleContext| {
239 use std::io::{Cursor, Read};
240
241 let input = args.first().ok_or_else(|| {
242 "archive.tar_extract() requires an Array<int> argument".to_string()
243 })?;
244 let bytes =
245 bytes_from_array(input).map_err(|e| format!("archive.tar_extract(): {}", e))?;
246
247 let cursor = Cursor::new(bytes);
248 let mut archive = tar::Archive::new(cursor);
249
250 let mut entries = Vec::new();
251 for entry_result in archive
252 .entries()
253 .map_err(|e| format!("archive.tar_extract() invalid tar: {}", e))?
254 {
255 let mut entry = entry_result
256 .map_err(|e| format!("archive.tar_extract() failed to read entry: {}", e))?;
257
258 if entry.header().entry_type().is_dir() {
260 continue;
261 }
262
263 let name = entry
264 .path()
265 .map_err(|e| format!("archive.tar_extract() invalid path: {}", e))?
266 .to_string_lossy()
267 .to_string();
268
269 let mut contents = String::new();
270 entry.read_to_string(&mut contents).map_err(|e| {
271 format!("archive.tar_extract() failed to read '{}': {}", name, e)
272 })?;
273
274 entries.push(make_entry(&name, &contents));
275 }
276
277 Ok(ValueWord::from_array(Arc::new(entries)))
278 },
279 ModuleFunction {
280 description: "Extract a tar archive from a byte array into an array of entries"
281 .to_string(),
282 params: vec![ModuleParam {
283 name: "data".to_string(),
284 type_name: "Array<int>".to_string(),
285 required: true,
286 description: "Tar archive as byte array".to_string(),
287 ..Default::default()
288 }],
289 return_type: Some("Array<{name: string, data: string}>".to_string()),
290 },
291 );
292
293 module
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
301 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
302 crate::module_exports::ModuleContext {
303 schemas: registry,
304 invoke_callable: None,
305 raw_invoker: None,
306 function_hashes: None,
307 vm_state: None,
308 granted_permissions: None,
309 scope_constraints: None,
310 set_pending_resume: None,
311 set_pending_frame_resume: None,
312 }
313 }
314
315 fn make_test_entries() -> ValueWord {
316 let entries = vec![
317 make_entry("hello.txt", "Hello, World!"),
318 make_entry("data/numbers.txt", "1 2 3 4 5"),
319 ];
320 ValueWord::from_array(Arc::new(entries))
321 }
322
323 #[test]
324 fn test_archive_module_creation() {
325 let module = create_archive_module();
326 assert_eq!(module.name, "std::core::archive");
327 assert!(module.has_export("zip_create"));
328 assert!(module.has_export("zip_extract"));
329 assert!(module.has_export("tar_create"));
330 assert!(module.has_export("tar_extract"));
331 }
332
333 #[test]
334 fn test_zip_roundtrip() {
335 let module = create_archive_module();
336 let ctx = test_ctx();
337 let zip_create_fn = module.get_export("zip_create").unwrap();
338 let zip_extract_fn = module.get_export("zip_extract").unwrap();
339
340 let entries = make_test_entries();
341 let zip_bytes = zip_create_fn(&[entries], &ctx).unwrap();
342
343 assert!(zip_bytes.as_any_array().is_some());
345
346 let extracted = zip_extract_fn(&[zip_bytes], &ctx).unwrap();
347 let arr = extracted.as_any_array().unwrap().to_generic();
348 assert_eq!(arr.len(), 2);
349
350 let (name0, data0) = extract_entry_fields(&arr[0]).unwrap();
352 assert_eq!(name0, "hello.txt");
353 assert_eq!(data0, "Hello, World!");
354
355 let (name1, data1) = extract_entry_fields(&arr[1]).unwrap();
357 assert_eq!(name1, "data/numbers.txt");
358 assert_eq!(data1, "1 2 3 4 5");
359 }
360
361 #[test]
362 fn test_tar_roundtrip() {
363 let module = create_archive_module();
364 let ctx = test_ctx();
365 let tar_create_fn = module.get_export("tar_create").unwrap();
366 let tar_extract_fn = module.get_export("tar_extract").unwrap();
367
368 let entries = make_test_entries();
369 let tar_bytes = tar_create_fn(&[entries], &ctx).unwrap();
370
371 assert!(tar_bytes.as_any_array().is_some());
372
373 let extracted = tar_extract_fn(&[tar_bytes], &ctx).unwrap();
374 let arr = extracted.as_any_array().unwrap().to_generic();
375 assert_eq!(arr.len(), 2);
376
377 let (name0, data0) = extract_entry_fields(&arr[0]).unwrap();
378 assert_eq!(name0, "hello.txt");
379 assert_eq!(data0, "Hello, World!");
380
381 let (name1, data1) = extract_entry_fields(&arr[1]).unwrap();
382 assert_eq!(name1, "data/numbers.txt");
383 assert_eq!(data1, "1 2 3 4 5");
384 }
385
386 #[test]
387 fn test_zip_create_empty() {
388 let module = create_archive_module();
389 let ctx = test_ctx();
390 let zip_create_fn = module.get_export("zip_create").unwrap();
391 let zip_extract_fn = module.get_export("zip_extract").unwrap();
392
393 let empty = ValueWord::from_array(Arc::new(Vec::new()));
394 let zip_bytes = zip_create_fn(&[empty], &ctx).unwrap();
395
396 let extracted = zip_extract_fn(&[zip_bytes], &ctx).unwrap();
397 let arr = extracted.as_any_array().unwrap().to_generic();
398 assert_eq!(arr.len(), 0);
399 }
400
401 #[test]
402 fn test_tar_create_empty() {
403 let module = create_archive_module();
404 let ctx = test_ctx();
405 let tar_create_fn = module.get_export("tar_create").unwrap();
406 let tar_extract_fn = module.get_export("tar_extract").unwrap();
407
408 let empty = ValueWord::from_array(Arc::new(Vec::new()));
409 let tar_bytes = tar_create_fn(&[empty], &ctx).unwrap();
410
411 let extracted = tar_extract_fn(&[tar_bytes], &ctx).unwrap();
412 let arr = extracted.as_any_array().unwrap().to_generic();
413 assert_eq!(arr.len(), 0);
414 }
415
416 #[test]
417 fn test_zip_extract_invalid_data() {
418 let module = create_archive_module();
419 let ctx = test_ctx();
420 let zip_extract_fn = module.get_export("zip_extract").unwrap();
421
422 let bad_data = ValueWord::from_array(Arc::new(vec![
423 ValueWord::from_i64(1),
424 ValueWord::from_i64(2),
425 ]));
426 let result = zip_extract_fn(&[bad_data], &ctx);
427 assert!(result.is_err());
428 }
429
430 #[test]
431 fn test_tar_extract_invalid_data() {
432 let module = create_archive_module();
433 let ctx = test_ctx();
434 let tar_extract_fn = module.get_export("tar_extract").unwrap();
435
436 let bad_data = ValueWord::from_array(Arc::new(vec![
437 ValueWord::from_i64(1),
438 ValueWord::from_i64(2),
439 ]));
440 let result = tar_extract_fn(&[bad_data], &ctx);
441 if let Ok(val) = result {
444 let arr = val.as_any_array().unwrap().to_generic();
445 assert_eq!(arr.len(), 0);
446 }
447 }
448
449 #[test]
450 fn test_zip_create_requires_array() {
451 let module = create_archive_module();
452 let ctx = test_ctx();
453 let zip_create_fn = module.get_export("zip_create").unwrap();
454
455 let result = zip_create_fn(&[ValueWord::from_i64(42)], &ctx);
456 assert!(result.is_err());
457 }
458
459 #[test]
460 fn test_schemas() {
461 let module = create_archive_module();
462
463 let zip_create_schema = module.get_schema("zip_create").unwrap();
464 assert_eq!(zip_create_schema.params.len(), 1);
465 assert!(zip_create_schema.params[0].required);
466 assert_eq!(zip_create_schema.return_type.as_deref(), Some("Array<int>"));
467
468 let zip_extract_schema = module.get_schema("zip_extract").unwrap();
469 assert_eq!(
470 zip_extract_schema.return_type.as_deref(),
471 Some("Array<{name: string, data: string}>")
472 );
473
474 let tar_create_schema = module.get_schema("tar_create").unwrap();
475 assert_eq!(tar_create_schema.params.len(), 1);
476
477 let tar_extract_schema = module.get_schema("tar_extract").unwrap();
478 assert_eq!(
479 tar_extract_schema.return_type.as_deref(),
480 Some("Array<{name: string, data: string}>")
481 );
482 }
483
484 #[test]
485 fn test_zip_unicode_content() {
486 let module = create_archive_module();
487 let ctx = test_ctx();
488 let zip_create_fn = module.get_export("zip_create").unwrap();
489 let zip_extract_fn = module.get_export("zip_extract").unwrap();
490
491 let entries = vec![make_entry("unicode.txt", "Hello \u{1F600} World \u{00E9}")];
492 let input = ValueWord::from_array(Arc::new(entries));
493 let zip_bytes = zip_create_fn(&[input], &ctx).unwrap();
494
495 let extracted = zip_extract_fn(&[zip_bytes], &ctx).unwrap();
496 let arr = extracted.as_any_array().unwrap().to_generic();
497 let (_, data) = extract_entry_fields(&arr[0]).unwrap();
498 assert_eq!(data, "Hello \u{1F600} World \u{00E9}");
499 }
500}