/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (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 /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: checksum.reef Authors: Chris Tusa License: Description: SHA256 checksum verification utilities ******************************************************************************/ module util.checksum import io.file import core.str import sys.process import crypto.sha256 as sha import core.result as res export // Compute SHA256 hash of a file, returns hex string (lowercase) fn sha256_file(path: string): string // Compute SHA256 hash of a string, returns hex string (lowercase) fn sha256_string(data: string): string // Verify a file matches expected checksum fn verify_checksum(path: string, expected: string): bool // Parse checksum format "algo:hash" and verify fn verify_checksum_with_algo(path: string, checksum_str: string): bool end export // Compute SHA256 of a file // Uses Reef's native streaming sha256_file() (Reef 0.1.14+) // Falls back to sha256sum command if native fails fn sha256_file(path: string): string if not file.fileExists(path) return "" end if // Use native streaming API (memory efficient, works on all file sizes) // Reef 0.1.14 fixed the large file hang/segfault issue let hash = sha.sha256_file(path) if str.length(hash) == 64 return str.to_lower(hash) end if // Fallback to sha256sum command if native fails return sha256_file_cmd(path) end sha256_file // Compute SHA256 using sha256sum command (for large files) fn sha256_file_cmd(path: string): string let output_file = "/tmp/coral_checksum_" + extract_basename(path) let cmd = "sha256sum \"" + path + "\" | cut -d' ' -f1 > \"" + output_file + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return "" end if process.process_wait(pid) // Read the hash from output file if not file.fileExists(output_file) return "" end if let hash = str.trim_ws(res.unwrap_or(file.readFile(output_file), "")) // Clean up let rm_pid = process.process_spawn_shell("rm -f \"" + output_file + "\"") if rm_pid > 0 process.process_wait(rm_pid) end if return hash end sha256_file_cmd // Extract basename from path for temp file naming fn extract_basename(path: string): string let last_slash = str.last_index_of_char(path, '/') if last_slash < 0 return path end if return str.substring(path, last_slash + 1, str.length(path) - last_slash - 1) end extract_basename // Compute SHA256 of a string using native crypto fn sha256_string(data: string): string let hash = sha.sha256(data) return str.to_lower(hash) end sha256_string // Verify that a file's SHA256 matches the expected value fn verify_checksum(path: string, expected: string): bool let computed = sha256_file(path) if str.length(computed) == 0 return false end if // Compare case-insensitively (normalize to lowercase) let computed_lower = str.to_lower(computed) let expected_lower = str.to_lower(expected) return computed_lower == expected_lower end verify_checksum // Parse and verify checksum in format "algo:hash" or just "hash" // Supported algorithms: sha256, sha256sum // If no algorithm prefix, assumes sha256 fn verify_checksum_with_algo(path: string, checksum_str: string): bool // Check for algorithm prefix let colon_idx = str.index_of_char(checksum_str, ':') if colon_idx < 0 // No prefix, assume SHA256 return verify_checksum(path, checksum_str) end if // Extract algorithm and hash let algo = str.to_lower(str.substring(checksum_str, 0, colon_idx)) let hash = str.substring(checksum_str, colon_idx + 1, str.length(checksum_str) - colon_idx - 1) // Verify based on algorithm if algo == "sha256" or algo == "sha256sum" return verify_checksum(path, hash) end if // Unsupported algorithm return false end verify_checksum_with_algo end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: color.reef Authors: Chris Tusa License: Description: Terminal color utilities for Coral ******************************************************************************/ module util.color import sys.args import sys.env import sys.platform.runtime as runtime export // Color functions - wrap text in ANSI color codes fn bold(s: string): string fn dim(s: string): string fn red(s: string): string fn green(s: string): string fn yellow(s: string): string fn blue(s: string): string fn cyan(s: string): string fn magenta(s: string): string // Formatted output helpers proc print_error(msg: string) proc print_success(msg: string) proc print_warning(msg: string) proc print_info(msg: string) proc print_action(msg: string) end export // ANSI escape code prefix: ESC[ (ASCII 27) // Reset code: ESC[0m // Helper to convert int to char (for ESC character) fn int_to_char(n: int): char unsafe let result: char = n return result end unsafe end int_to_char // Get ESC character (ASCII 27) as a single-character string fn get_esc(): string unsafe let result: string = runtime.reef_alloc_string_buffer(2) result[0] = int_to_char(27) return result end unsafe end get_esc // Check if color output should be disabled // Respects --no-color flag and NO_COLOR environment variable fn no_color(): bool if args.has_flag("no-color") return true end if if env.has_env("NO_COLOR") return true end if return false end no_color // Helper to wrap text with ANSI codes fn wrap_color(s: string, code: string): string if no_color() return s end if // ESC[ + code + m + text + ESC[0m let esc = get_esc() return esc + "[" + code + "m" + s + esc + "[0m" end wrap_color // Bold text (code 1) fn bold(s: string): string return wrap_color(s, "1") end bold // Dim text (code 2) fn dim(s: string): string return wrap_color(s, "2") end dim // Red text (code 31) fn red(s: string): string return wrap_color(s, "31") end red // Green text (code 32) fn green(s: string): string return wrap_color(s, "32") end green // Yellow text (code 33) fn yellow(s: string): string return wrap_color(s, "33") end yellow // Blue text (code 34) fn blue(s: string): string return wrap_color(s, "34") end blue // Magenta text (code 35) fn magenta(s: string): string return wrap_color(s, "35") end magenta // Cyan text (code 36) fn cyan(s: string): string return wrap_color(s, "36") end cyan // Print error message with red X prefix proc print_error(msg: string) if no_color() println("ERROR: " + msg) else println(red("error:") + " " + msg) end if end print_error // Print success message with green checkmark prefix proc print_success(msg: string) if no_color() println("OK: " + msg) else println(green("ok:") + " " + msg) end if end print_success // Print warning message with yellow prefix proc print_warning(msg: string) if no_color() println("WARNING: " + msg) else println(yellow("warning:") + " " + msg) end if end print_warning // Print info message with blue arrow prefix proc print_info(msg: string) if no_color() println("-> " + msg) else println(blue("->") + " " + msg) end if end print_info // Print action message with bold arrow prefix proc print_action(msg: string) if no_color() println("=> " + msg) else println(bold("=>") + " " + msg) end if end print_action end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: http.reef Authors: Chris Tusa License: Description: HTTP download utilities using native Reef net.http Requires Reef 0.1.16+ for progress bar support ******************************************************************************/ module util.http import net.http import net.extras import io.file import core.str import core.result as res import sys.process import util.color export // Download a file from URL to destination path fn download(url: string, dest: string): bool // Download with verbose progress output fn download_verbose(url: string, dest: string): bool // Fetch URL content as text fn fetch_text(url: string): string // Fetch URL content with timeout fn fetch_text_timeout(url: string, timeout_ms: int): string // Check if a URL is reachable (HEAD request) fn check_url(url: string): bool // Get response status code for a URL fn get_status(url: string): int // Configure proxy usage (call before downloads) proc enable_proxy(use_proxy: bool) end export // Module-level proxy setting mut proxy_enabled = false // Default timeout in milliseconds (30 seconds) fn default_timeout(): int return 30000 end default_timeout // Internal: download with progress bar (Style C: solid block) // Returns true on success, false on failure fn download_with_progress(url: string, dest: string): bool // Configure progress bar - Style C: solid block character extras.progress_bar_set_char("█") extras.progress_bar_set_width(30) extras.progress_bar_init(url) // Download with progress callback let cb = fn(p: http.HttpDownloadProgress): bool => extras.progress_bar(p) return res.is_ok(http.http_download_file_callback(url, dest, cb)) end download_with_progress // Check if URL will redirect (3xx status) - native HTTP download crashes on redirects fn url_will_redirect(url: string): bool let status = get_status(url) return status >= 300 and status < 400 end url_will_redirect // Download a file from URL to destination with progress bar // Uses native Reef http_download_file_callback() (requires Reef 0.1.16+) // Falls back to curl if native download fails or URL redirects fn download(url: string, dest: string): bool let filename = extract_filename(url) print(" " + filename + " ") // Check for redirects first - native HTTP crashes on 302s if not url_will_redirect(url) // Try native Reef download with progress bar if download_with_progress(url, dest) println("") // Newline after progress bar completes return true end if println("") // Newline after failed progress end if // Fallback to curl (handles redirects with -L flag) print(" (using curl) ... ") if download_with_curl(url, dest) println("done") return true end if println("failed") return false end download // Download with verbose progress output // Uses native Reef http_download_file_callback() with progress bar (requires Reef 0.1.16+) fn download_verbose(url: string, dest: string): bool let filename = extract_filename(url) println(" " + url) print(" " + filename + " ") // Check for redirects first - native HTTP crashes on 302s if not url_will_redirect(url) // Try native Reef download with progress bar if download_with_progress(url, dest) println("") // Newline after progress bar completes color.print_success("Download complete: " + filename) return true end if println("") // Newline after failed progress end if // Fallback to curl (handles redirects with -L flag) print(" (using curl) ... ") if download_with_curl(url, dest) println("done") color.print_success("Download complete: " + filename) return true end if println("failed") color.print_error("Download failed: " + url) return false end download_verbose // Fetch URL content as text fn fetch_text(url: string): string return fetch_text_timeout(url, default_timeout()) end fetch_text // Fetch URL content with timeout // Uses http_get_auto() which handles redirects and is safe for text/binary fn fetch_text_timeout(url: string, timeout_ms: int): string // Create request with timeout let req = http.http_request_new("GET", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, timeout_ms) // Check for errors (transport failure is now Err; HttpResponse no longer has an error field) if res.is_err(resp_r) return "" end if let resp = res.unwrap_ok(resp_r) // Check for success if not http.http_response_is_ok(resp) return "" end if return http.http_response_body(resp) end fetch_text_timeout // Check if a URL is reachable fn check_url(url: string): bool let req = http.http_request_new("HEAD", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, 10000) if res.is_err(resp_r) return false end if let resp = res.unwrap_ok(resp_r) // Accept success codes and redirects as "reachable" return resp.status_code >= 200 and resp.status_code < 400 end check_url // Get response status code for a URL fn get_status(url: string): int let req = http.http_request_new("HEAD", url) http.http_request_set_header(req, "User-Agent", "Coral/1.0") let resp_r = http.http_send_timeout(req, 10000) if res.is_err(resp_r) return 0 end if let resp = res.unwrap_ok(resp_r) return resp.status_code end get_status // Extract filename from URL fn extract_filename(url: string): string // Find last slash let last_slash = str.last_index_of_char(url, '/') if last_slash < 0 return url end if let filename = str.substring(url, last_slash + 1, str.length(url) - last_slash - 1) // Remove query string if present let query_pos = str.index_of_char(filename, '?') if query_pos > 0 return str.substring(filename, 0, query_pos) end if return filename end extract_filename // Helper: convert int to string fn int_to_str(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_str proc enable_proxy(use_proxy: bool) proxy_enabled = use_proxy end enable_proxy // Download using curl command // Handles HTTP redirects, chunked encoding, and SSL/TLS properly fn download_with_curl(url: string, dest: string): bool // Use curl with: // -L: follow redirects (required for GitHub, etc.) // -f: fail silently on HTTP errors (returns non-zero exit code) // -s: silent mode (no progress meter) // -S: show errors even in silent mode // -o: output file mut proxy_flag = " --noproxy \"*\"" if proxy_enabled proxy_flag = "" end if let cmd = "curl -L -f -s -S" + proxy_flag + " -o \"" + dest + "\" \"" + url + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 color.print_error("Failed to spawn curl") return false end if let exit_code = process.process_wait(pid) if exit_code != 0 color.print_error("Download failed: " + url) return false end if return true end download_with_curl end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: mtree.reef Authors: Chris Tusa License: Description: mtree-format manifest for package file inventory ******************************************************************************/ module util.mtree import io.file import io.dir import io.path import core.str import fs.stat import fs.link import crypto.sha256 as sha import util.checksum import text.stringbuilder as sb import core.result as res import core.option as opt export // Manifest entry representing a single file, directory, or symlink type ManifestEntry // Parse an mtree manifest string into an array of entries // Returns the number of entries parsed fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int // Generate an mtree manifest string from a package staging directory fn generate_manifest(pkg_dir: string): string // Accessors fn entry_path(entry: ManifestEntry): string fn entry_type(entry: ManifestEntry): string fn entry_mode(entry: ManifestEntry): int fn entry_uname(entry: ManifestEntry): string fn entry_gname(entry: ManifestEntry): string fn entry_sha256(entry: ManifestEntry): string fn entry_link(entry: ManifestEntry): string // Format a ManifestEntry as an mtree line fn format_entry(entry: ManifestEntry): string // Uid/gid name resolution helpers fn uid_to_name(uid: int): string fn gid_to_name(gid: int): string fn name_to_uid(name: string): int fn name_to_gid(name: string): int end export // ============================================================================ // Types // ============================================================================ type ManifestEntry = struct epath: string // Relative path (e.g., ./usr/bin/foo) etype: string // "file", "dir", or "link" mode: int // Permission bits as integer (e.g., 0755 = 493) uname: string // Owner user name gname: string // Owner group name sha256: string // SHA256 checksum (files only) link_target: string // Symlink target (links only) end ManifestEntry // ============================================================================ // Accessors // ============================================================================ fn entry_path(entry: ManifestEntry): string return entry.epath end entry_path fn entry_type(entry: ManifestEntry): string return entry.etype end entry_type fn entry_mode(entry: ManifestEntry): int return entry.mode end entry_mode fn entry_uname(entry: ManifestEntry): string return entry.uname end entry_uname fn entry_gname(entry: ManifestEntry): string return entry.gname end entry_gname fn entry_sha256(entry: ManifestEntry): string return entry.sha256 end entry_sha256 fn entry_link(entry: ManifestEntry): string return entry.link_target end entry_link // ============================================================================ // Parser // ============================================================================ // Parse an mtree manifest string into entries. // Format per line: ./path type=T mode=OOOO uname=U gname=G [sha256=H] [link=T] // Lines starting with # are comments. Empty lines are skipped. fn parse_manifest(content: string, entries: [ManifestEntry], max_entries: int): int if str.length(content) == 0 return 0 end if // Split into lines mut lines: [string] = new [string](max_entries + 64) let line_count = str.split(content, '\n', lines, max_entries + 64) mut count = 0 mut i = 0 while i < line_count and count < max_entries let line = str.trim_ws(lines[i]) let line_len = str.length(line) // Skip empty lines and comments if line_len == 0 i = i + 1 continue end if if line[0] == '#' i = i + 1 continue end if // Parse the line: first token is path, rest are key=value let entry = parse_entry_line(line) if str.length(entry.epath) > 0 entries[count] = entry count = count + 1 end if i = i + 1 end while return count end parse_manifest // Parse a single mtree line into a ManifestEntry fn parse_entry_line(line: string): ManifestEntry mut entry = ManifestEntry{ epath: "", etype: "", mode: 0, uname: "root", gname: "root", sha256: "", link_target: "" } // Split line by spaces mut tokens: [string] = new [string](16) let token_count = str.split(line, ' ', tokens, 16) if token_count == 0 return entry end if // First token is always the path entry.epath = tokens[0] // Remaining tokens are key=value pairs mut t = 1 while t < token_count let token = tokens[t] let eq_pos = str.index_of_char(token, '=') if eq_pos > 0 let key = str.substring(token, 0, eq_pos) let val = str.substring(token, eq_pos + 1, str.length(token) - eq_pos - 1) if str.equals(key, "type") entry.etype = val elif str.equals(key, "mode") entry.mode = octal_to_int(val) elif str.equals(key, "uname") entry.uname = val elif str.equals(key, "gname") entry.gname = val elif str.equals(key, "sha256") entry.sha256 = val elif str.equals(key, "link") entry.link_target = val end if end if t = t + 1 end while return entry end parse_entry_line // ============================================================================ // Generator // ============================================================================ // Generate an mtree manifest for all files in a package staging directory. // Walks the directory tree, collects entries, sorts them, and returns the // formatted manifest string. fn generate_manifest(pkg_dir: string): string // Collect entries by walking the directory tree mut entries: [ManifestEntry] = new [ManifestEntry](8192) let count = walk_directory(pkg_dir, pkg_dir, entries, 0, 8192) if count == 0 return "" end if // Sort entries by path (simple insertion sort — fine for package file counts) sort_entries(entries, count) // Build the output string using StringBuilder to avoid O(n²) heap // fragmentation from repeated string concatenation (Reef BUG-027) let builder = sb.sb_new() sb.sb_append(builder, "#mtree\n") mut i = 0 while i < count sb.sb_append(builder, format_entry(entries[i])) sb.sb_append(builder, "\n") i = i + 1 end while return sb.sb_build(builder) end generate_manifest // Recursively walk a directory, collecting ManifestEntry records. // base_dir is the package root (for computing relative paths). // current_dir is the directory being walked. // Returns total number of entries added (starting from offset). fn walk_directory(base_dir: string, current_dir: string, entries: [ManifestEntry], offset: int, max: int): int mut count = offset // Add the current directory itself (unless it's the base) if not str.equals(current_dir, base_dir) and count < max let rel = get_relative_path(base_dir, current_dir) if not is_metadata_path(rel) let mode = res.unwrap_or(stat.file_mode(current_dir), 0) let uid = res.unwrap_or(stat.file_uid(current_dir), 0) let gid = res.unwrap_or(stat.file_gid(current_dir), 0) entries[count] = ManifestEntry{ epath: "./" + rel, etype: "dir", mode: mode, uname: uid_to_name(uid), gname: gid_to_name(gid), sha256: "", link_target: "" } count = count + 1 end if end if // List directory contents let dir_entries = res.unwrap_or(dir.list_dir(current_dir), new [string](0)) let entry_count = dir_entries.length() mut i = 0 while i < entry_count and count < max let name = dir_entries[i] // Skip . and .. to prevent infinite recursion if str.equals(name, ".") or str.equals(name, "..") i = i + 1 continue end if let full_path = path.join_path(current_dir, name) let rel = get_relative_path(base_dir, full_path) // Skip metadata files and directories if is_metadata_path(rel) i = i + 1 continue end if // Check type: symlink first (symlinks to dirs would match is_directory) if stat.is_symlink(full_path) let target = res.unwrap_or(link.readlink(full_path), "") let uid = res.unwrap_or(stat.file_uid(current_dir), 0) let gid = res.unwrap_or(stat.file_gid(current_dir), 0) entries[count] = ManifestEntry{ epath: "./" + rel, etype: "link", mode: 0, uname: uid_to_name(uid), gname: gid_to_name(gid), sha256: "", link_target: target } count = count + 1 elif stat.is_directory(full_path) // Recurse into subdirectory count = walk_directory(base_dir, full_path, entries, count, max) elif stat.is_file(full_path) let mode = res.unwrap_or(stat.file_mode(full_path), 0) let uid = res.unwrap_or(stat.file_uid(full_path), 0) let gid = res.unwrap_or(stat.file_gid(full_path), 0) let hash = checksum.sha256_file(full_path) entries[count] = ManifestEntry{ epath: "./" + rel, etype: "file", mode: mode, uname: uid_to_name(uid), gname: gid_to_name(gid), sha256: hash, link_target: "" } count = count + 1 end if i = i + 1 end while return count end walk_directory // Check if a relative path is a package metadata file/directory fn is_metadata_path(rel: string): bool if str.equals(rel, ".PKGINFO") or str.equals(rel, ".MANIFEST") return true end if if str.equals(rel, ".FOOTPRINT") or str.equals(rel, ".CONFIG") return true end if if str.equals(rel, ".SCRIPTS") return true end if if str.starts_with(rel, ".SCRIPTS/") return true end if return false end is_metadata_path // Get relative path by stripping the base directory prefix fn get_relative_path(base: string, full: string): string let base_len = str.length(base) let full_len = str.length(full) if full_len <= base_len return "" end if // Skip the base prefix and the trailing / mut start = base_len if start < full_len and full[start] == '/' start = start + 1 end if return str.substring(full, start, full_len - start) end get_relative_path // ============================================================================ // Formatting // ============================================================================ // Format a ManifestEntry as a single mtree line fn format_entry(entry: ManifestEntry): string mut line = entry.epath + " type=" + entry.etype if str.equals(entry.etype, "file") or str.equals(entry.etype, "dir") line = line + " mode=" + int_to_octal(entry.mode) end if line = line + " uname=" + entry.uname line = line + " gname=" + entry.gname if str.equals(entry.etype, "file") and str.length(entry.sha256) > 0 line = line + " sha256=" + entry.sha256 end if if str.equals(entry.etype, "link") and str.length(entry.link_target) > 0 line = line + " link=" + entry.link_target end if return line end format_entry // ============================================================================ // Sorting // ============================================================================ // Insertion sort entries by path (lexicographic). // Package file counts are typically <1000, so insertion sort is adequate. proc sort_entries(entries: [ManifestEntry], count: int) mut i = 1 while i < count let key_entry = entries[i] let key_path = entries[i].epath mut j = i - 1 while j >= 0 and str.compare(entries[j].epath, key_path) > 0 entries[j + 1] = entries[j] j = j - 1 end while entries[j + 1] = key_entry i = i + 1 end while end sort_entries // ============================================================================ // Numeric Conversion Helpers // ============================================================================ // Convert an octal string (e.g., "0755", "4755") to an integer fn octal_to_int(s: string): int let slen = str.length(s) if slen == 0 return 0 end if mut result = 0 mut i = 0 while i < slen let c = s[i] if c >= '0' and c <= '7' result = result * 8 + (c - '0') end if i = i + 1 end while return result end octal_to_int // Convert an integer to a 4-digit octal string (e.g., 493 -> "0755") fn int_to_octal(n: int): string if n == 0 return "0000" end if mut value = n mut digits: [int] = new [int](16) mut count = 0 while value > 0 and count < 16 digits[count] = value - (value / 8 * 8) value = value / 8 count = count + 1 end while // Pad to 4 digits minimum while count < 4 digits[count] = 0 count = count + 1 end while // Build string in reverse (most significant first) mut result = "" mut i = count - 1 while i >= 0 result = result + digit_char(digits[i]) i = i - 1 end while return result end int_to_octal // Single digit to string character fn digit_char(d: int): string if d == 0 return "0" elif d == 1 return "1" elif d == 2 return "2" elif d == 3 return "3" elif d == 4 return "4" elif d == 5 return "5" elif d == 6 return "6" elif d == 7 return "7" end if return "0" end digit_char // Convert int to decimal string (local helper to avoid circular imports) fn int_to_str(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 - (value / 10 * 10) result = str.concat(digit_char(digit), result) value = value / 10 end while if negative result = str.concat("-", result) end if return result end int_to_str // ============================================================================ // UID/GID Name Resolution // ============================================================================ // Resolve a numeric uid to a username. // Uses POSIX getpwuid() via Reef's fs.stat.uid_name() — zero heap allocation. fn uid_to_name(uid: int): string return opt.unwrap_or(stat.uid_name(uid), "") end uid_to_name // Resolve a numeric gid to a group name. // Uses POSIX getgrgid() via Reef's fs.stat.gid_name() — zero heap allocation. fn gid_to_name(gid: int): string return opt.unwrap_or(stat.gid_name(gid), "") end gid_to_name // Resolve a username to numeric uid by reading /etc/passwd. // Only used during verify/install (low call count), not the hot manifest walk. fn name_to_uid(name: string): int if str.equals(name, "root") return 0 end if let content = res.unwrap_or(file.readFile("/etc/passwd"), "") if str.length(content) == 0 return 0 end if mut lines: [string] = new [string](256) let line_count = str.split(content, '\n', lines, 256) mut i = 0 while i < line_count let line = lines[i] if str.length(line) > 0 mut fields: [string] = new [string](8) let field_count = str.split(line, ':', fields, 8) if field_count >= 3 and str.equals(fields[0], name) return str_to_int(fields[2]) end if end if i = i + 1 end while return 0 end name_to_uid // Resolve a group name to numeric gid by reading /etc/group. // Only used during verify/install (low call count), not the hot manifest walk. fn name_to_gid(name: string): int if str.equals(name, "root") return 0 end if let content = res.unwrap_or(file.readFile("/etc/group"), "") if str.length(content) == 0 return 0 end if mut lines: [string] = new [string](256) let line_count = str.split(content, '\n', lines, 256) mut i = 0 while i < line_count let line = lines[i] if str.length(line) > 0 mut fields: [string] = new [string](8) let field_count = str.split(line, ':', fields, 8) if field_count >= 3 and str.equals(fields[0], name) return str_to_int(fields[2]) end if end if i = i + 1 end while return 0 end name_to_gid // Simple string-to-int conversion fn str_to_int(s: string): int let slen = str.length(s) if slen == 0 return 0 end if mut result = 0 mut i = 0 while i < slen let c = s[i] if c >= '0' and c <= '9' result = result * 10 + (c - '0') end if i = i + 1 end while return result end str_to_int end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: priv.reef Authors: Chris Tusa License: Description: Privilege detection helpers ******************************************************************************/ module util.priv import io.file import core.result as res export // Check if we are running as root (UID 0) fn is_root(): bool end export // Check if we are running as root (UID 0) // Uses file test to check write permission on system directory fn is_root(): bool // Try to create a temp file in /var/lib/coral - only root can write there let test_file = "/var/lib/coral/.root_check" // writeFile now returns Result[bool, Error] (Reef 0.7.5); Ok means we wrote it if res.is_ok(file.writeFile(test_file, "1")) // Successfully wrote - we have root-level access // File will be overwritten next time, no need to delete return true end if return false end is_root end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: prompt.reef Authors: Chris Tusa License: Description: User prompt utilities for Coral ******************************************************************************/ module util.prompt import io.console import core.option as opt import core.str import core.convert import util.color import types export fn has_yes_flag(opts: types.GlobalOptions): bool fn confirm(message: string, opts: types.GlobalOptions): bool fn confirm_packages(action: string, packages: [string], count: int, opts: types.GlobalOptions): bool fn confirm_install(to_install: [string], install_count: int, to_upgrade: [string], upgrade_count: int, opts: types.GlobalOptions): bool fn confirm_remove(to_remove: [string], count: int, dependents: [string], dep_count: int, opts: types.GlobalOptions): bool fn confirm_build_plan(packages: [string], versions: [string], count: int, target: string, opts: types.GlobalOptions): bool end export // Check if -y or --yes flag is present fn has_yes_flag(opts: types.GlobalOptions): bool return opts.yes end has_yes_flag // Ask for confirmation, returns true if user confirms fn confirm(message: string, opts: types.GlobalOptions): bool // Skip if -y flag if has_yes_flag(opts) return true end if print(message + " [Y/n] ") let response = opt.unwrap_or(console.readLine(), "") let r = str.trim_ws(response) // Empty or Y/y means yes if str.length(r) == 0 return true end if if r == "Y" or r == "y" or r == "yes" or r == "Yes" or r == "YES" return true end if return false end confirm // Display list of packages and ask for confirmation fn confirm_packages(action: string, packages: [string], count: int, opts: types.GlobalOptions): bool if count == 0 return false end if println("") println(color.bold("Packages to " + action + ":")) println("") // Display packages in columns mut i = 0 while i < count println(" " + packages[i]) i = i + 1 end while println("") println(color.dim("Total: " + convert.to_string(count) + " package(s)")) println("") return confirm("Proceed?", opts) end confirm_packages // Display package info and ask for install confirmation fn confirm_install(to_install: [string], install_count: int, to_upgrade: [string], upgrade_count: int, opts: types.GlobalOptions): bool if install_count == 0 and upgrade_count == 0 return false end if println("") if install_count > 0 println(color.bold("New packages to install:")) mut i = 0 while i < install_count println(" " + color.green("+") + " " + to_install[i]) i = i + 1 end while println("") end if if upgrade_count > 0 println(color.bold("Packages to upgrade:")) mut i = 0 while i < upgrade_count println(" " + color.cyan("^") + " " + to_upgrade[i]) i = i + 1 end while println("") end if let total = install_count + upgrade_count println(color.dim("Total: " + convert.to_string(total) + " package(s)")) println("") return confirm("Proceed with installation?", opts) end confirm_install // Display packages to remove and ask for confirmation fn confirm_remove(to_remove: [string], count: int, dependents: [string], dep_count: int, opts: types.GlobalOptions): bool if count == 0 return false end if println("") println(color.bold("Packages to remove:")) mut i = 0 while i < count println(" " + color.red("-") + " " + to_remove[i]) i = i + 1 end while if dep_count > 0 println("") println(color.yellow("The following packages depend on these and will also be removed:")) i = 0 while i < dep_count println(" " + color.yellow("-") + " " + dependents[i]) i = i + 1 end while end if println("") let total = count + dep_count println(color.dim("Total: " + convert.to_string(total) + " package(s)")) println("") return confirm("Proceed with removal?", opts) end confirm_remove // Display build plan and ask for confirmation fn confirm_build_plan(packages: [string], versions: [string], count: int, target: string, opts: types.GlobalOptions): bool if count == 0 return true end if println("") println(color.bold("Build plan for '" + target + "':")) println("") println("Packages to build from source:") mut i = 0 while i < count mut line = " " + color.green("+") + " " + packages[i] if str.length(versions[i]) > 0 line = line + " " + color.dim(versions[i]) end if println(line) i = i + 1 end while println("") println(color.dim("Total: " + convert.to_string(count) + " package(s) to build")) println("") return confirm("Proceed with build?", opts) end confirm_build_plan end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: version.reef Authors: Chris Tusa License: Description: Semantic version parsing and comparison utilities ******************************************************************************/ module util.version import core.str export // Version constraint parsing type DepConstraint // Compare two version strings semantically // Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 fn compare_versions(v1: string, v2: string): int // Check if v1 is newer than v2 fn is_newer(v1: string, v2: string): bool // Parse a dependency string with optional version constraint // e.g., "libfoo>=1.0" -> { name: "libfoo", op: ">=", version: "1.0" } fn parse_dep_constraint(dep: string): DepConstraint // Check if an installed version satisfies a version constraint fn check_version_constraint(installed_version: string, op: string, required_version: string): bool // Accessor functions for DepConstraint fn constraint_name(c: DepConstraint): string fn constraint_op(c: DepConstraint): string fn constraint_version(c: DepConstraint): string fn has_constraint(c: DepConstraint): bool // Extract version from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" -> "0.1.10" fn extract_version_from_filename(filename: string, pkg_name: string): string // Extract release number from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1 fn extract_release_from_filename(filename: string): int end export // Parsed dependency constraint type DepConstraint = struct name: string op: string version: string end DepConstraint // Compare two version strings semantically // Handles versions like "0.1.10" vs "0.1.7" correctly (0.1.10 > 0.1.7) fn compare_versions(v1: string, v2: string): int // Parse version components mut v1_parts: [int] = new [int](10) mut v2_parts: [int] = new [int](10) let v1_count = parse_version(v1, v1_parts, 10) let v2_count = parse_version(v2, v2_parts, 10) // Compare each component let max_parts = max_int(v1_count, v2_count) mut i = 0 while i < max_parts let p1 = get_part(v1_parts, v1_count, i) let p2 = get_part(v2_parts, v2_count, i) if p1 > p2 return 1 elif p1 < p2 return -1 end if i = i + 1 end while return 0 end compare_versions // Check if v1 is newer than v2 fn is_newer(v1: string, v2: string): bool return compare_versions(v1, v2) > 0 end is_newer // Parse version string into integer components // "0.1.10" -> [0, 1, 10] fn parse_version(version: string, parts: [int], max_parts: int): int mut count = 0 mut current = 0 mut in_number = false mut i = 0 while i < str.length(version) and count < max_parts let c = str.char_at(version, i) if is_digit(c) current = current * 10 + char_to_digit(c) in_number = true elif c == '.' or c == '-' if in_number parts[count] = current count = count + 1 current = 0 in_number = false end if else // Skip non-numeric characters (like 'alpha', 'beta', 'rc') if in_number parts[count] = current count = count + 1 current = 0 in_number = false end if end if i = i + 1 end while // Don't forget the last number if in_number and count < max_parts parts[count] = current count = count + 1 end if return count end parse_version // Extract version from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" with pkg_name "reef" -> "0.1.10" fn extract_version_from_filename(filename: string, pkg_name: string): string // Remove package name prefix and dash let prefix_len = str.length(pkg_name) + 1 if str.length(filename) <= prefix_len return "" end if // Get the part after "pkgname-" let rest = str.substring(filename, prefix_len, str.length(filename) - prefix_len) // Find the last dash before .pkg.tar.xz (that's the release separator) // e.g., "0.1.10-1.pkg.tar.xz" -> version is "0.1.10" let suffix = ".pkg.tar.xz" let suffix_len = str.length(suffix) if not str.ends_with(rest, suffix) return "" end if // Remove suffix: "0.1.10-1.pkg.tar.xz" -> "0.1.10-1" let ver_rel = str.substring(rest, 0, str.length(rest) - suffix_len) // Find last dash (release separator) let last_dash = str.last_index_of_char(ver_rel, '-') if last_dash < 0 return ver_rel end if // Return version part: "0.1.10-1" -> "0.1.10" return str.substring(ver_rel, 0, last_dash) end extract_version_from_filename // Extract release number from package filename // e.g., "reef-0.1.10-1.pkg.tar.xz" -> 1 fn extract_release_from_filename(filename: string): int let suffix = ".pkg.tar.xz" if not str.ends_with(filename, suffix) return 1 end if // Remove suffix let without_suffix = str.substring(filename, 0, str.length(filename) - str.length(suffix)) // Find last dash let last_dash = str.last_index_of_char(without_suffix, '-') if last_dash < 0 return 1 end if // Extract release number let release_str = str.substring(without_suffix, last_dash + 1, str.length(without_suffix) - last_dash - 1) return parse_int(release_str) end extract_release_from_filename // Helper: check if character is a digit fn is_digit(c: char): bool return c >= '0' and c <= '9' end is_digit // Helper: convert digit character to integer fn char_to_digit(c: char): int return c - '0' end char_to_digit // Helper: get part at index, or 0 if out of bounds fn get_part(parts: [int], count: int, idx: int): int if idx < count return parts[idx] end if return 0 end get_part // Helper: max of two integers fn max_int(a: int, b: int): int if a > b return a end if return b end max_int // Helper: parse string to int fn parse_int(s: string): int mut result = 0 mut i = 0 while i < str.length(s) let c = str.char_at(s, i) if is_digit(c) result = result * 10 + char_to_digit(c) else break end if i = i + 1 end while return result end parse_int // Parse a dependency string into name, operator, and version // Handles: "libfoo", "libfoo>=1.0", "libfoo<=2.0", "libfoo>1.0", "libfoo<2.0", "libfoo=1.0" fn parse_dep_constraint(dep: string): DepConstraint let len = str.length(dep) mut i = 0 while i < len let c = str.char_at(dep, i) if c == '>' or c == '<' or c == '=' let name = str.substring(dep, 0, i) // Check for two-char operators (>=, <=) if i + 1 < len let next = str.char_at(dep, i + 1) if next == '=' let op = str.substring(dep, i, 2) let ver = str.substring(dep, i + 2, len - i - 2) return DepConstraint{ name: name, op: op, version: ver } end if end if // Single char operator (>, <, =) let op = str.substring(dep, i, 1) let ver = str.substring(dep, i + 1, len - i - 1) return DepConstraint{ name: name, op: op, version: ver } end if i = i + 1 end while // No constraint return DepConstraint{ name: dep, op: "", version: "" } end parse_dep_constraint // Check if an installed version satisfies a version constraint fn check_version_constraint(installed_version: string, op: string, required_version: string): bool if str.length(op) == 0 return true end if let cmp = compare_versions(installed_version, required_version) if str.equals(op, ">=") return cmp >= 0 elif str.equals(op, "<=") return cmp <= 0 elif str.equals(op, ">") return cmp > 0 elif str.equals(op, "<") return cmp < 0 elif str.equals(op, "=") return cmp == 0 end if return true end check_version_constraint // Accessor functions fn constraint_name(c: DepConstraint): string return c.name end constraint_name fn constraint_op(c: DepConstraint): string return c.op end constraint_op fn constraint_version(c: DepConstraint): string return c.version end constraint_version fn has_constraint(c: DepConstraint): bool return str.length(c.op) > 0 end has_constraint end module