/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: archive.reef Authors: Chris Tusa License: Description: Tar/xz archive operations using shell commands ******************************************************************************/ module util.archive import sys.process import io.file import io.dir import io.path import core.str import core.result as res export // Archive type enumeration (as strings for simplicity) const ARCHIVE_TAR_GZ: string const ARCHIVE_TAR_XZ: string const ARCHIVE_TAR_BZ2: string const ARCHIVE_ZIP: string const ARCHIVE_UNKNOWN: string // Detect archive type from filename extension fn detect_type(archive_path: string): string // Extract an archive to destination directory fn extract(archive_path: string, dest_dir: string): bool // Create a .pkg.tar.xz package from a directory fn create_package(source_dir: string, output_path: string): bool // List contents of an archive (returns count, fills files array) fn list_contents(archive_path: string, files: [string], max_count: int): int end export // Archive type constants const ARCHIVE_TAR_GZ: string = "tar.gz" const ARCHIVE_TAR_XZ: string = "tar.xz" const ARCHIVE_TAR_BZ2: string = "tar.bz2" const ARCHIVE_ZIP: string = "zip" const ARCHIVE_UNKNOWN: string = "unknown" // Get the tar command (use gtar on illumos/OmniOS, tar on Linux) fn get_tar_cmd(): string // On illumos, we need GNU tar for full compatibility // Check if gtar exists, otherwise fall back to tar (for Linux) if file.fileExists("/usr/bin/gtar") return "/usr/bin/gtar" elif file.fileExists("/usr/gnu/bin/tar") return "/usr/gnu/bin/tar" end if // Fall back to system tar (Linux) return "tar" end get_tar_cmd // Detect archive type from filename fn detect_type(archive_path: string): string let lower = str.to_lower(archive_path) if str.ends_with(lower, ".tar.xz") or str.ends_with(lower, ".txz") return ARCHIVE_TAR_XZ elif str.ends_with(lower, ".tar.gz") or str.ends_with(lower, ".tgz") return ARCHIVE_TAR_GZ elif str.ends_with(lower, ".tar.bz2") or str.ends_with(lower, ".tbz2") or str.ends_with(lower, ".tbz") return ARCHIVE_TAR_BZ2 elif str.ends_with(lower, ".zip") return ARCHIVE_ZIP elif str.ends_with(lower, ".tar") // Plain tar (no compression) return "tar" end if return ARCHIVE_UNKNOWN end detect_type // Build the tar extraction flags based on archive type fn get_extract_flags(archive_type: string): string if archive_type == ARCHIVE_TAR_XZ return "-Jxf" elif archive_type == ARCHIVE_TAR_GZ return "-zxf" elif archive_type == ARCHIVE_TAR_BZ2 return "-jxf" elif archive_type == "tar" return "-xf" end if return "" end get_extract_flags // Build the tar list flags based on archive type fn get_list_flags(archive_type: string): string if archive_type == ARCHIVE_TAR_XZ return "-Jtf" elif archive_type == ARCHIVE_TAR_GZ return "-ztf" elif archive_type == ARCHIVE_TAR_BZ2 return "-jtf" elif archive_type == "tar" return "-tf" end if return "" end get_list_flags // Extract an archive to the destination directory fn extract(archive_path: string, dest_dir: string): bool // Verify archive exists if not file.fileExists(archive_path) return false end if // Ensure destination directory exists if not dir.dir_exists(dest_dir) if not res.is_ok(dir.create_dir_all(dest_dir)) return false end if end if let archive_type = detect_type(archive_path) if archive_type == ARCHIVE_ZIP // Use unzip for zip files let cmd = "unzip -q -o \"" + archive_path + "\" -d \"" + dest_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end if let flags = get_extract_flags(archive_type) if str.length(flags) == 0 return false end if // Build tar command let tar = get_tar_cmd() let cmd = tar + " " + flags + " \"" + archive_path + "\" -C \"" + dest_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end extract // Create a .pkg.tar.xz package from a source directory fn create_package(source_dir: string, output_path: string): bool // Verify source directory exists if not dir.dir_exists(source_dir) return false end if // Ensure parent directory of output exists let output_dir = path.dirname(output_path) if str.length(output_dir) > 0 and not dir.dir_exists(output_dir) if not res.is_ok(dir.create_dir_all(output_dir)) return false end if end if // Create tar.xz archive // Command: cd source_dir && tar -Jcf output_path . let tar = get_tar_cmd() let cmd = "cd \"" + source_dir + "\" && " + tar + " -Jcf \"" + output_path + "\" ." let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) return exit_code == 0 end create_package // List contents of an archive fn list_contents(archive_path: string, files: [string], max_count: int): int if not file.fileExists(archive_path) return 0 end if let archive_type = detect_type(archive_path) if archive_type == ARCHIVE_ZIP // For zip files, use unzip -l and parse output // This is more complex, so for now return 0 // TODO: Implement zip listing if needed return 0 end if let flags = get_list_flags(archive_type) if str.length(flags) == 0 return 0 end if // Build tar command with output to temp file let tar = get_tar_cmd() let tmp_file = "/tmp/coral_archive_list_" + int_to_string(process.getpid()) + ".txt" let cmd = tar + " " + flags + " \"" + archive_path + "\" > \"" + tmp_file + "\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid < 0 return 0 end if let exit_code = process.process_wait(pid) if exit_code != 0 return 0 end if // Read the temp file if not file.fileExists(tmp_file) return 0 end if let content = res.unwrap_or(file.readFile(tmp_file), "") // Clean up temp file cleanup_temp_file(tmp_file) if str.length(content) == 0 return 0 end if // Split by newlines mut lines: [string] = new [string](max_count) let line_count = str.split(content, '\n', lines, max_count) // Copy non-empty lines to output mut count = 0 mut i = 0 while i < line_count and count < max_count let line = str.trim_ws(lines[i]) if str.length(line) > 0 files[count] = line count = count + 1 end if i = i + 1 end while return count end list_contents // Helper: remove temp file fn cleanup_temp_file(path: string): bool let cmd = "rm -f \"" + path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if process.process_wait(pid) return true end cleanup_temp_file // Helper: convert int to string (local copy to avoid circular import) fn int_to_string(n: int): string if n == 0 return "0" end if mut negative = false mut value = n if n < 0 negative = true value = 0 - n end if mut result = "" while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while if negative result = str.concat("-", result) end if return result end int_to_string end module