///|
/// Archive extraction helpers.
///
/// `.zip` archives (mooncakes.io package sources) are decoded in-process with
/// `bobzhang/zip`, which keeps the tool portable to any Linux/macOS host
/// without requiring `unzip`. `.tar.gz` archives (MoonBit toolchain builds)
/// are handled by the system `tar`, which is present on every supported host.
///|
/// Extract a zip archive from memory into `dest`.
///
/// Entry paths are sanitised to prevent `..` escapes from writing outside
/// `dest`. Symlink entries are ignored: only regular files and directories are
/// materialised.
pub fn extract_zip(zip_bytes : Bytes, dest : String) -> Unit raise MbError {
let archive = @zip.Archive::of_bytes(zip_bytes) catch {
_ => raise MbError("failed to parse zip archive")
}
mkdir_p(dest)
for entry in archive.to_array() {
let raw = @fpath.sanitize(entry.path()).to_string()
if raw == "" ||
raw.has_prefix("/") ||
raw == ".." ||
raw.has_prefix("../") ||
raw.contains("/../") {
continue
}
let out = path_join(dest, raw)
if entry.is_dir() {
mkdir_p(out)
} else if entry.is_file() {
let content = match entry.kind() {
File(f) =>
f.to_bytes() catch {
_ => raise MbError("failed to decompress zip member: " + raw)
}
Dir => continue
}
write_bytes(out, content)
}
}
}
///|
/// Extract a `.tar.gz` archive on disk into `dest` using the system `tar`.
pub fn extract_tar_gz(archive : String, dest : String) -> Unit raise MbError {
mkdir_p(dest)
let cmd = "tar -xzf " + shell_quote(archive) + " -C " + shell_quote(dest)
run_checked(cmd) catch {
_ => raise MbError("failed to extract tar archive: " + archive)
}
}