Skip to main content

zoi_lua/api/
archive.rs

1//! Archive utilities for Lua scripts.
2//!
3//! This module provides functions for extracting and creating various archive
4//! formats (Zip, Tar, Zstd, Xz, etc.) within the Lua environment.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use ar::Archive as ArArchive;
10use flate2::read::GzDecoder;
11use mlua::{self, Lua, Table};
12use sevenz_rust2;
13use xz2::read::XzDecoder;
14use zip::ZipArchive;
15use zstd::stream::read::Decoder as ZstdDecoder;
16
17/// Exposes the `UTILS.EXTRACT` function to the Lua environment.
18///
19/// This utility provides a unified interface for downloading and extracting
20/// various archive formats. It handles:
21/// - Remote Fetching: If the source starts with http(s), it downloads the file
22///   to `BUILD_DIR`.
23/// - Format Detection: Dispatches to the appropriate decoder (Zip, Tar, Zstd,
24///   Xz, 7z, etc.).
25/// - Error Propagation: Any failure (network, filesystem, or corruption) is
26///   converted into an `mlua::Error::RuntimeError`, which halts the Lua
27///   execution and is caught by the Rust build engine to trigger a rollback.
28///
29/// # Errors
30///
31/// Returns an `mlua::Error` if:
32/// - The `UTILS` table cannot be found.
33/// - The output directory is invalid (not a subdirectory of `BUILD_DIR`).
34/// - Filesystem operations (create dir, open file, copy, remove) fail.
35/// - Network download fails.
36/// - Archive extraction fails.
37/// - The archive format is unsupported.
38///
39/// # Panics
40///
41/// This function may panic if:
42/// - Parsing the source URL fails to yield a filename.
43/// - Executing external commands (`hdiutil`, `pkgutil`, `unrar`) fails or their
44///   output cannot be parsed.
45pub fn add_extract_util(lua: &Lua, quiet: bool) -> Result<(), mlua::Error> {
46    let extract_fn = lua.create_function(
47        move |lua, (source, out_name): (String, Option<String>)| {
48            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
49            let build_dir = Path::new(&build_dir_str);
50
51            let archive_file = if source.starts_with("http") {
52                let file_name =
53                    source.split('/').next_back().unwrap_or("download.tmp");
54                let temp_path = build_dir.join(file_name);
55                super::download::download_with_progress(
56                    &source, &temp_path, quiet
57                )?;
58
59                temp_path
60            } else {
61                PathBuf::from(source)
62            };
63
64            let out_dir_name =
65                out_name.unwrap_or_else(|| "extracted".to_string());
66            let out_dir = build_dir.join(&out_dir_name);
67
68            if !out_dir.starts_with(build_dir) || out_dir == build_dir {
69                return Err(mlua::Error::RuntimeError(format!(
70                    "Invalid output directory: {out_dir_name}. Extraction \
71                     must be into a subdirectory of the build directory."
72                )));
73            }
74
75            fs::create_dir_all(&out_dir)
76                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
77
78            if !quiet {
79                println!(
80                    "Extracting {} to {}",
81                    archive_file.display(),
82                    out_dir.display()
83                );
84            }
85
86            let file = fs::File::open(&archive_file)
87                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
88
89            let archive_path = Path::new(&archive_file);
90            let archive_path_str = archive_file.to_string_lossy();
91
92            if archive_path
93                .extension()
94                .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
95            {
96                let mut archive = ZipArchive::new(file)
97                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
98                archive
99                    .extract(&out_dir)
100                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
101            } else if archive_path_str.ends_with(".tar.gz")
102                || archive_path
103                    .extension()
104                    .is_some_and(|ext| ext.eq_ignore_ascii_case("tgz"))
105            {
106                let tar_gz = GzDecoder::new(file);
107                let mut archive = tar::Archive::new(tar_gz);
108                archive
109                    .unpack(&out_dir)
110                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
111            } else if archive_path_str.ends_with(".tar.zst")
112                || archive_path
113                    .extension()
114                    .is_some_and(|ext| ext.eq_ignore_ascii_case("zpa"))
115                || archive_path
116                    .extension()
117                    .is_some_and(|ext| ext.eq_ignore_ascii_case("zsa"))
118            {
119                let tar_zst = ZstdDecoder::new(file)
120                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
121                let mut archive = tar::Archive::new(tar_zst);
122                archive
123                    .unpack(&out_dir)
124                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
125            } else if archive_path_str.ends_with(".tar.xz") {
126                let tar_xz = XzDecoder::new(file);
127                let mut archive = tar::Archive::new(tar_xz);
128                archive
129                    .unpack(&out_dir)
130                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
131            } else if archive_path
132                .extension()
133                .is_some_and(|ext| ext.eq_ignore_ascii_case("7z"))
134            {
135                sevenz_rust2::decompress_file(&archive_file, &out_dir)
136                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
137            } else if archive_path
138                .extension()
139                .is_some_and(|ext| ext.eq_ignore_ascii_case("dmg"))
140            {
141                if !cfg!(target_os = "macos") {
142                    return Err(mlua::Error::RuntimeError(
143                        "Extracting .dmg files is only supported on macOS."
144                            .to_string()
145                    ));
146                }
147                let output = std::process::Command::new("hdiutil")
148                    .arg("attach")
149                    .arg("-nobrowse")
150                    .arg("-readonly")
151                    .arg(&archive_file)
152                    .output()
153                    .map_err(|e| {
154                        mlua::Error::RuntimeError(format!(
155                            "Failed to execute hdiutil: {e}"
156                        ))
157                    })?;
158                if !output.status.success() {
159                    let stderr = String::from_utf8_lossy(&output.stderr);
160                    return Err(mlua::Error::RuntimeError(format!(
161                        "hdiutil failed: {stderr}"
162                    )));
163                }
164                let output_str = String::from_utf8_lossy(&output.stdout);
165                let mut mount_point = None;
166                for line in output_str.lines() {
167                    if line.contains("/Volumes/")
168                        && let Some(idx) = line.find("/Volumes/")
169                    {
170                        mount_point = Some(line[idx..].trim().to_string());
171                        break;
172                    }
173                }
174                let mount_point = mount_point.ok_or_else(|| {
175                    mlua::Error::RuntimeError(
176                        "Failed to parse mount point from hdiutil output."
177                            .to_string()
178                    )
179                })?;
180                let mount_path = std::path::Path::new(&mount_point);
181                if let Err(e) =
182                    zoi_core::utils::copy_dir_all(mount_path, &out_dir)
183                {
184                    let _ = std::process::Command::new("hdiutil")
185                        .arg("detach")
186                        .arg(&mount_point)
187                        .status();
188                    return Err(mlua::Error::RuntimeError(format!(
189                        "Failed to copy contents from dmg: {e}"
190                    )));
191                }
192                let detach_status = std::process::Command::new("hdiutil")
193                    .arg("detach")
194                    .arg(&mount_point)
195                    .status()
196                    .map_err(|e| {
197                        mlua::Error::RuntimeError(format!(
198                            "Failed to execute hdiutil detach: {e}"
199                        ))
200                    })?;
201                if !detach_status.success() {
202                    eprintln!(
203                        "Warning: failed to detach dmg volume at {mount_point}"
204                    );
205                }
206            } else if archive_path
207                .extension()
208                .is_some_and(|ext| ext.eq_ignore_ascii_case("pkg"))
209            {
210                if !cfg!(target_os = "macos") {
211                    return Err(mlua::Error::RuntimeError(
212                        "Extracting .pkg files natively is only supported on \
213                         macOS."
214                            .to_string()
215                    ));
216                }
217                let temp_extract_dir = out_dir.join(".pkg_extract_tmp");
218                let status = std::process::Command::new("pkgutil")
219                    .arg("--expand-full")
220                    .arg(&archive_file)
221                    .arg(&temp_extract_dir)
222                    .status()
223                    .map_err(|e| {
224                        mlua::Error::RuntimeError(format!(
225                            "Failed to execute pkgutil: {e}"
226                        ))
227                    })?;
228                if !status.success() {
229                    return Err(mlua::Error::RuntimeError(
230                        "pkgutil failed to expand the package.".to_string()
231                    ));
232                }
233                zoi_core::utils::copy_dir_all(&temp_extract_dir, &out_dir)
234                    .map_err(|e| {
235                        mlua::Error::RuntimeError(format!(
236                            "Failed to copy pkg contents: {e}"
237                        ))
238                    })?;
239                let _ = fs::remove_dir_all(&temp_extract_dir);
240            } else if archive_path
241                .extension()
242                .is_some_and(|ext| ext.eq_ignore_ascii_case("rar"))
243            {
244                if zoi_core::utils::command_exists("unrar") {
245                    let status = std::process::Command::new("unrar")
246                        .arg("x")
247                        .arg("-y")
248                        .arg(&archive_file)
249                        .arg(&out_dir)
250                        .status()
251                        .map_err(|e| {
252                            mlua::Error::RuntimeError(e.to_string())
253                        })?;
254                    if !status.success() {
255                        return Err(mlua::Error::RuntimeError(
256                            "unrar failed".to_string()
257                        ));
258                    }
259                } else {
260                    return Err(mlua::Error::RuntimeError(
261                        "unrar command not found. Please install unrar to \
262                         extract .rar files."
263                            .to_string()
264                    ));
265                }
266            } else if archive_path
267                .extension()
268                .is_some_and(|ext| ext.eq_ignore_ascii_case("deb"))
269            {
270                let mut ar = ArArchive::new(file);
271                while let Some(entry_result) = ar.next_entry() {
272                    let mut entry = entry_result.map_err(|e| {
273                        mlua::Error::RuntimeError(e.to_string())
274                    })?;
275                    let name =
276                        String::from_utf8_lossy(entry.header().identifier())
277                            .trim()
278                            .trim_end_matches('/')
279                            .to_string();
280                    if name.starts_with("data.tar") {
281                        let temp_data_path = build_dir.join(&name);
282                        let mut temp_file = fs::File::create(&temp_data_path)
283                            .map_err(|e| {
284                            mlua::Error::RuntimeError(format!(
285                                "Failed to create temp file for {name}: {e}"
286                            ))
287                        })?;
288                        std::io::copy(&mut entry, &mut temp_file).map_err(
289                            |e| {
290                                mlua::Error::RuntimeError(format!(
291                                    "Failed to copy entry data for {name}: {e}"
292                                ))
293                            }
294                        )?;
295
296                        let data_file = fs::File::open(&temp_data_path)
297                            .map_err(|e| {
298                                mlua::Error::RuntimeError(format!(
299                                    "Failed to reopen temp file for {name}: \
300                                     {e}"
301                                ))
302                            })?;
303                        let data_path = Path::new(&name);
304                        if data_path
305                            .extension()
306                            .is_some_and(|ext| ext.eq_ignore_ascii_case("gz"))
307                        {
308                            let mut archive =
309                                tar::Archive::new(GzDecoder::new(data_file));
310                            archive.unpack(&out_dir).map_err(|e| {
311                                mlua::Error::RuntimeError(format!(
312                                    "Failed to unpack {name}: {e}"
313                                ))
314                            })?;
315                        } else if data_path
316                            .extension()
317                            .is_some_and(|ext| ext.eq_ignore_ascii_case("xz"))
318                        {
319                            let mut archive =
320                                tar::Archive::new(XzDecoder::new(data_file));
321                            archive.unpack(&out_dir).map_err(|e| {
322                                mlua::Error::RuntimeError(format!(
323                                    "Failed to unpack {name}: {e}"
324                                ))
325                            })?;
326                        } else if data_path
327                            .extension()
328                            .is_some_and(|ext| ext.eq_ignore_ascii_case("zst"))
329                        {
330                            let mut archive = tar::Archive::new(
331                                ZstdDecoder::new(data_file).map_err(|e| {
332                                    mlua::Error::RuntimeError(format!(
333                                        "Failed to initialize zstd for \
334                                         {name}: {e}"
335                                    ))
336                                })?
337                            );
338                            archive.unpack(&out_dir).map_err(|e| {
339                                mlua::Error::RuntimeError(format!(
340                                    "Failed to unpack {name}: {e}"
341                                ))
342                            })?;
343                        }
344                        fs::remove_file(temp_data_path).ok();
345                    }
346                }
347            } else {
348                return Err(mlua::Error::RuntimeError(format!(
349                    "Unsupported archive format for file: {archive_path_str}"
350                )));
351            }
352
353            Ok(())
354        }
355    )?;
356
357    let utils_table: Table = lua.globals().get("UTILS")?;
358    utils_table.set("EXTRACT", extract_fn)?;
359
360    Ok(())
361}
362
363/// Exposes the `UTILS.ARCHIVE` table and `UTILS.MAKE_ARCHIVE` function to the
364/// Lua environment.
365///
366/// `UTILS.ARCHIVE` includes:
367/// - `list(path)`: Lists the contents of an archive.
368///
369/// `UTILS.MAKE_ARCHIVE(source, output, algorithm)`: Creates an archive from a
370/// source path.
371///
372/// # Errors
373///
374/// Returns an `mlua::Error` if:
375/// - The `UTILS` table cannot be found.
376/// - Filesystem operations (open, create, metadata, read, write) fail.
377/// - Archive processing (zip, tar, etc.) fails.
378/// - Unsupported archive format or algorithm is provided.
379///
380/// # Panics
381///
382/// This function may panic if:
383/// - Stripping path prefixes fails during ZIP creation.
384/// - The parent of a source path cannot be determined.
385pub fn add_archive_util(lua: &Lua) -> Result<(), mlua::Error> {
386    let archive_table = lua.create_table()?;
387
388    let list_fn = lua.create_function(|lua, path: String| {
389        let p = Path::new(&path);
390        let actual_path = if p.exists() {
391            p.to_path_buf()
392        } else if let Ok(build_dir) = lua.globals().get::<String>("BUILD_DIR") {
393            Path::new(&build_dir).join(p)
394        } else {
395            p.to_path_buf()
396        };
397
398        let file = fs::File::open(&actual_path).map_err(|e| {
399            mlua::Error::RuntimeError(format!(
400                "Failed to open archive {}: {e}",
401                actual_path.display()
402            ))
403        })?;
404        let mut files = Vec::new();
405
406        let path_obj = Path::new(&path);
407        if path_obj
408            .extension()
409            .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
410        {
411            let mut archive = ZipArchive::new(file)
412                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
413            for i in 0..archive.len() {
414                let file = archive
415                    .by_index(i)
416                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
417                files.push(file.name().to_string());
418            }
419        } else if path.ends_with(".tar.gz")
420            || path_obj
421                .extension()
422                .is_some_and(|ext| ext.eq_ignore_ascii_case("tgz"))
423        {
424            let tar_gz = GzDecoder::new(file);
425            let mut archive = tar::Archive::new(tar_gz);
426            for entry in archive
427                .entries()
428                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
429            {
430                let entry = entry
431                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
432                files.push(
433                    entry
434                        .path()
435                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
436                        .to_string_lossy()
437                        .to_string()
438                );
439            }
440        } else if path.ends_with(".tar.zst")
441            || path_obj
442                .extension()
443                .is_some_and(|ext| ext.eq_ignore_ascii_case("zpa"))
444            || path_obj
445                .extension()
446                .is_some_and(|ext| ext.eq_ignore_ascii_case("zsa"))
447        {
448            let tar_zst = ZstdDecoder::new(file)
449                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
450            let mut archive = tar::Archive::new(tar_zst);
451            for entry in archive
452                .entries()
453                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
454            {
455                let entry = entry
456                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
457                files.push(
458                    entry
459                        .path()
460                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
461                        .to_string_lossy()
462                        .to_string()
463                );
464            }
465        } else if path.ends_with(".tar.xz") {
466            let tar_xz = XzDecoder::new(file);
467            let mut archive = tar::Archive::new(tar_xz);
468            for entry in archive
469                .entries()
470                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
471            {
472                let entry = entry
473                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
474                files.push(
475                    entry
476                        .path()
477                        .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?
478                        .to_string_lossy()
479                        .to_string()
480                );
481            }
482        } else if path_obj
483            .extension()
484            .is_some_and(|ext| ext.eq_ignore_ascii_case("7z"))
485        {
486            let file = fs::File::open(&path)
487                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
488            let reader = sevenz_rust2::ArchiveReader::new(
489                file,
490                sevenz_rust2::Password::empty()
491            )
492            .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
493            for entry in &reader.archive().files {
494                files.push(entry.name.clone());
495            }
496        } else if path_obj
497            .extension()
498            .is_some_and(|ext| ext.eq_ignore_ascii_case("rar"))
499        {
500            if zoi_core::utils::command_exists("unrar") {
501                let output = std::process::Command::new("unrar")
502                    .arg("lb")
503                    .arg(&path)
504                    .output()
505                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
506                if output.status.success() {
507                    let list = String::from_utf8_lossy(&output.stdout);
508                    for line in list.lines() {
509                        files.push(line.to_string());
510                    }
511                }
512            }
513        } else if path_obj
514            .extension()
515            .is_some_and(|ext| ext.eq_ignore_ascii_case("deb"))
516        {
517            let mut ar = ArArchive::new(file);
518            while let Some(entry_result) = ar.next_entry() {
519                let entry = entry_result
520                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
521                let header = entry.header();
522                files.push(
523                    String::from_utf8_lossy(header.identifier()).to_string()
524                );
525            }
526        } else {
527            return Err(mlua::Error::RuntimeError(format!(
528                "Unsupported archive format: {path}"
529            )));
530        }
531
532        Ok(files)
533    })?;
534    archive_table.set("list", list_fn)?;
535
536    let make_archive_fn = lua.create_function(
537        move |lua,
538              (source, output, algorithm): (
539            mlua::Value,
540            String,
541            Option<String>
542        )| {
543            let algo = algorithm
544                .unwrap_or_else(|| "zst".to_string())
545                .to_lowercase();
546            let build_dir_str: String = lua.globals().get("BUILD_DIR")?;
547            let build_dir = Path::new(&build_dir_str);
548
549            let output_path = if Path::new(&output).is_absolute() {
550                PathBuf::from(&output)
551            } else {
552                build_dir.join(&output)
553            };
554
555            if let Some(parent) = output_path.parent() {
556                fs::create_dir_all(parent)
557                    .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
558            }
559
560            let mut source_paths = Vec::new();
561            match source {
562                mlua::Value::String(s) => {
563                    let s_borrowed = s.to_str()?;
564                    let s_str_ref = s_borrowed.as_ref();
565                    let p = build_dir.join(s_str_ref);
566                    if p.exists() {
567                        source_paths.push((p, s_str_ref.to_string()));
568                    } else if Path::new(s_str_ref).exists() {
569                        source_paths.push((
570                            PathBuf::from(s_str_ref),
571                            s_str_ref.to_string()
572                        ));
573                    } else {
574                        return Err(mlua::Error::RuntimeError(format!(
575                            "MAKE_ARCHIVE: source path does not exist: \
576                             {s_str_ref}"
577                        )));
578                    }
579                }
580                mlua::Value::Table(t) => {
581                    for val in t.sequence_values::<String>() {
582                        let s_str = val?;
583                        let p = build_dir.join(&s_str);
584                        if p.exists() {
585                            source_paths.push((p, s_str.clone()));
586                        } else if Path::new(&s_str).exists() {
587                            source_paths
588                                .push((PathBuf::from(&s_str), s_str.clone()));
589                        } else {
590                            return Err(mlua::Error::RuntimeError(format!(
591                                "MAKE_ARCHIVE: source path does not exist: \
592                                 {s_str}"
593                            )));
594                        }
595                    }
596                }
597                _ => {
598                    return Err(mlua::Error::RuntimeError(
599                        "MAKE_ARCHIVE: source must be string or table"
600                            .to_string()
601                    ));
602                }
603            }
604
605            let file = fs::File::create(&output_path)
606                .map_err(|e| mlua::Error::RuntimeError(e.to_string()))?;
607
608            match algo.as_str() {
609                "gz" => {
610                    let mut encoder = flate2::write::GzEncoder::new(
611                        file,
612                        flate2::Compression::default()
613                    );
614                    for (path, _) in source_paths {
615                        let mut f = fs::File::open(path).map_err(|e| {
616                            mlua::Error::RuntimeError(e.to_string())
617                        })?;
618                        std::io::copy(&mut f, &mut encoder).map_err(|e| {
619                            mlua::Error::RuntimeError(e.to_string())
620                        })?;
621                    }
622                    encoder.finish().map_err(|e| {
623                        mlua::Error::RuntimeError(e.to_string())
624                    })?;
625                }
626                "zip" => {
627                    let mut zip = zip::ZipWriter::new(file);
628                    let options = zip::write::SimpleFileOptions::default()
629                        .compression_method(zip::CompressionMethod::Deflated);
630
631                    for (path, rel_name) in source_paths {
632                        if path.is_dir() {
633                            let parent = path
634                                .parent()
635                                .expect("source path should have a parent");
636                            for entry in walkdir::WalkDir::new(&path)
637                                .into_iter()
638                                .filter_map(Result::ok)
639                            {
640                                let rel =
641                                    entry.path().strip_prefix(parent).expect(
642                                        "entry path should be within source \
643                                         path"
644                                    );
645                                if entry.file_type().is_dir() {
646                                    zip.add_directory(
647                                        rel.to_string_lossy(),
648                                        options
649                                    )
650                                    .map_err(|e| {
651                                        mlua::Error::RuntimeError(e.to_string())
652                                    })?;
653                                } else {
654                                    zip.start_file(
655                                        rel.to_string_lossy(),
656                                        options
657                                    )
658                                    .map_err(|e| {
659                                        mlua::Error::RuntimeError(e.to_string())
660                                    })?;
661                                    let mut f = fs::File::open(entry.path())
662                                        .map_err(|e| {
663                                            mlua::Error::RuntimeError(
664                                                e.to_string()
665                                            )
666                                        })?;
667                                    std::io::copy(&mut f, &mut zip).map_err(
668                                        |e| {
669                                            mlua::Error::RuntimeError(
670                                                e.to_string()
671                                            )
672                                        }
673                                    )?;
674                                }
675                            }
676                        } else {
677                            zip.start_file(rel_name, options).map_err(|e| {
678                                mlua::Error::RuntimeError(e.to_string())
679                            })?;
680                            let mut f = fs::File::open(path).map_err(|e| {
681                                mlua::Error::RuntimeError(e.to_string())
682                            })?;
683                            std::io::copy(&mut f, &mut zip).map_err(|e| {
684                                mlua::Error::RuntimeError(e.to_string())
685                            })?;
686                        }
687                    }
688                    zip.finish().map_err(|e| {
689                        mlua::Error::RuntimeError(e.to_string())
690                    })?;
691                }
692                "tar" | "tar.gz" | "tar.xz" | "tar.zst" | "zst" => {
693                    let writer: Box<dyn std::io::Write> = match algo.as_str() {
694                        "tar" => Box::new(file),
695                        "tar.gz" => Box::new(flate2::write::GzEncoder::new(
696                            file,
697                            flate2::Compression::default()
698                        )),
699                        "tar.xz" => {
700                            Box::new(xz2::write::XzEncoder::new(file, 6))
701                        }
702                        "tar.zst" | "zst" => Box::new(
703                            zstd::stream::write::Encoder::new(file, 0)
704                                .map_err(|e| {
705                                    mlua::Error::RuntimeError(e.to_string())
706                                })?
707                                .auto_finish()
708                        ),
709                        _ => unreachable!()
710                    };
711
712                    let mut tar = tar::Builder::new(writer);
713                    for (path, rel_name) in source_paths {
714                        if path.is_dir() {
715                            tar.append_dir_all(rel_name, path).map_err(
716                                |e| mlua::Error::RuntimeError(e.to_string())
717                            )?;
718                        } else {
719                            tar.append_path_with_name(path, rel_name).map_err(
720                                |e| mlua::Error::RuntimeError(e.to_string())
721                            )?;
722                        }
723                    }
724                    tar.finish().map_err(|e| {
725                        mlua::Error::RuntimeError(e.to_string())
726                    })?;
727                }
728                _ => {
729                    return Err(mlua::Error::RuntimeError(format!(
730                        "MAKE_ARCHIVE: unsupported algorithm: {algo}"
731                    )));
732                }
733            }
734
735            Ok(())
736        }
737    )?;
738
739    let utils_table: Table = lua.globals().get("UTILS")?;
740    utils_table.set("ARCHIVE", archive_table)?;
741    utils_table.set("MAKE_ARCHIVE", make_archive_fn)?;
742
743    Ok(())
744}