/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: config.reef Authors: Chris Tusa License: Description: Configuration file loading and access ******************************************************************************/ module core.config import types import io.file import core.str import encoding.toml import core.result as res export // Configuration structure type Config // Load configuration from file (with defaults if not found) fn load(): Config // Access functions for common settings fn get_arch(cfg: Config): string fn get_jobs(cfg: Config): int fn get_confirm(cfg: Config): bool fn get_color(cfg: Config): bool // Path accessors fn get_ports_dir(cfg: Config): string fn get_packages_dir(cfg: Config): string fn get_sources_dir(cfg: Config): string fn get_database_dir(cfg: Config): string fn get_build_dir(cfg: Config): string fn get_index_dir(cfg: Config): string // Legacy (for compatibility) fn get_work_dir(cfg: Config): string fn get_pkg_dir(cfg: Config): string // Root and prefix accessors fn get_root(cfg: Config): string fn get_prefix(cfg: Config): string fn get_sysconfdir(cfg: Config): string // Root-aware path accessors (prepend root to path) fn get_database_dir_rooted(cfg: Config): string fn get_install_prefix(cfg: Config): string // Apply command-line overrides to config fn apply_overrides(cfg: Config, root: string, prefix: string, no_chroot: bool): Config // Build settings fn get_cflags(cfg: Config): string fn get_cxxflags(cfg: Config): string fn get_makeflags(cfg: Config): string fn get_strip(cfg: Config): bool fn get_fallback_to_source(cfg: Config): bool fn get_compress_man(cfg: Config): bool fn get_clean_work(cfg: Config): bool fn get_clean_pkg(cfg: Config): bool fn get_logging(cfg: Config): bool fn get_quiet(cfg: Config): bool // Network settings fn get_timeout(cfg: Config): int fn get_retries(cfg: Config): int fn get_use_proxy(cfg: Config): bool // Sync settings fn get_auto_refresh(cfg: Config): bool // Cache settings fn get_keep_packages(cfg: Config): bool fn get_keep_sources(cfg: Config): bool // Security settings fn get_require_signed_repos(cfg: Config): bool fn get_require_signed_packages(cfg: Config): bool // Install settings fn get_chroot_scripts(cfg: Config): bool end export // Configuration structure type Config = struct // [general] arch: string jobs: int confirm: bool color: bool // [paths] root: string prefix: string sysconfdir: string ports_dir: string packages_dir: string sources_dir: string database_dir: string build_dir: string index_dir: string // Legacy (kept for compatibility) work_dir: string pkg_dir: string // [build] cflags: string cxxflags: string makeflags: string strip: bool compress_man: bool fallback_to_source: bool clean_work: bool clean_pkg: bool logging: bool quiet: bool // [network] timeout: int retries: int use_proxy: bool // [sync] auto_refresh: bool // [cache] keep_packages: bool keep_sources: bool // [security] require_signed_repos: bool require_signed_packages: bool // [install] chroot_scripts: bool end Config // Create a default configuration fn default_config(): Config return Config{ // General arch: "amd64", jobs: 0, // 0 = auto-detect confirm: true, color: true, // Paths (defaults from types) root: "", prefix: "/usr/local", sysconfdir: "/etc", ports_dir: types.get_ports_dir(), packages_dir: types.get_packages_dir(), sources_dir: types.get_sources_dir(), database_dir: types.get_db_dir(), build_dir: types.get_build_dir(), index_dir: types.get_index_dir(), // Legacy (for compatibility) work_dir: types.get_build_dir(), pkg_dir: types.get_build_dir(), // Build cflags: "-O2 -pipe", cxxflags: "-O2 -pipe", makeflags: "", strip: true, compress_man: true, fallback_to_source: true, clean_work: true, clean_pkg: true, logging: true, quiet: false, // Network timeout: 30, retries: 3, use_proxy: false, // Sync auto_refresh: false, // Cache keep_packages: true, keep_sources: true, // Security require_signed_repos: false, require_signed_packages: false, // Install chroot_scripts: true } end default_config // Load configuration from file fn load(): Config mut cfg = default_config() // Check if config file exists if not file.fileExists(types.get_config_file()) return cfg end if // Read config file let content = res.unwrap_or(file.readFile(types.get_config_file()), "") if str.length(content) == 0 return cfg end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return cfg end if // [general] section if toml.toml_has_key(keys, vals, entry_count, "general.arch") cfg.arch = toml.toml_get(keys, vals, entry_count, "general.arch") end if if toml.toml_has_key(keys, vals, entry_count, "general.jobs") cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "general.jobs") end if if toml.toml_has_key(keys, vals, entry_count, "general.confirm") cfg.confirm = toml.toml_get_bool(keys, vals, entry_count, "general.confirm") end if if toml.toml_has_key(keys, vals, entry_count, "general.color") cfg.color = toml.toml_get_bool(keys, vals, entry_count, "general.color") end if // [paths] section if toml.toml_has_key(keys, vals, entry_count, "paths.root") cfg.root = toml.toml_get(keys, vals, entry_count, "paths.root") end if if toml.toml_has_key(keys, vals, entry_count, "paths.prefix") cfg.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix") end if if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir") cfg.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir") end if if toml.toml_has_key(keys, vals, entry_count, "paths.ports") cfg.ports_dir = toml.toml_get(keys, vals, entry_count, "paths.ports") end if if toml.toml_has_key(keys, vals, entry_count, "paths.packages") cfg.packages_dir = toml.toml_get(keys, vals, entry_count, "paths.packages") end if if toml.toml_has_key(keys, vals, entry_count, "paths.sources") cfg.sources_dir = toml.toml_get(keys, vals, entry_count, "paths.sources") end if if toml.toml_has_key(keys, vals, entry_count, "paths.database") cfg.database_dir = toml.toml_get(keys, vals, entry_count, "paths.database") end if if toml.toml_has_key(keys, vals, entry_count, "paths.build") cfg.build_dir = toml.toml_get(keys, vals, entry_count, "paths.build") end if if toml.toml_has_key(keys, vals, entry_count, "paths.index") cfg.index_dir = toml.toml_get(keys, vals, entry_count, "paths.index") end if // Legacy path support (maps to build_dir) if toml.toml_has_key(keys, vals, entry_count, "paths.work") cfg.work_dir = toml.toml_get(keys, vals, entry_count, "paths.work") cfg.build_dir = cfg.work_dir // Also set new field end if if toml.toml_has_key(keys, vals, entry_count, "paths.pkg") cfg.pkg_dir = toml.toml_get(keys, vals, entry_count, "paths.pkg") end if // [build] section if toml.toml_has_key(keys, vals, entry_count, "build.cflags") cfg.cflags = toml.toml_get(keys, vals, entry_count, "build.cflags") end if if toml.toml_has_key(keys, vals, entry_count, "build.cxxflags") cfg.cxxflags = toml.toml_get(keys, vals, entry_count, "build.cxxflags") end if if toml.toml_has_key(keys, vals, entry_count, "build.makeflags") cfg.makeflags = toml.toml_get(keys, vals, entry_count, "build.makeflags") end if if toml.toml_has_key(keys, vals, entry_count, "build.strip") cfg.strip = toml.toml_get_bool(keys, vals, entry_count, "build.strip") end if if toml.toml_has_key(keys, vals, entry_count, "build.compress_man") cfg.compress_man = toml.toml_get_bool(keys, vals, entry_count, "build.compress_man") end if if toml.toml_has_key(keys, vals, entry_count, "build.fallback_to_source") cfg.fallback_to_source = toml.toml_get_bool(keys, vals, entry_count, "build.fallback_to_source") end if if toml.toml_has_key(keys, vals, entry_count, "build.clean_work") cfg.clean_work = toml.toml_get_bool(keys, vals, entry_count, "build.clean_work") end if if toml.toml_has_key(keys, vals, entry_count, "build.clean_pkg") cfg.clean_pkg = toml.toml_get_bool(keys, vals, entry_count, "build.clean_pkg") end if if toml.toml_has_key(keys, vals, entry_count, "build.logging") cfg.logging = toml.toml_get_bool(keys, vals, entry_count, "build.logging") end if if toml.toml_has_key(keys, vals, entry_count, "build.quiet") cfg.quiet = toml.toml_get_bool(keys, vals, entry_count, "build.quiet") end if // [network] section if toml.toml_has_key(keys, vals, entry_count, "network.timeout") cfg.timeout = toml.toml_get_int(keys, vals, entry_count, "network.timeout") end if if toml.toml_has_key(keys, vals, entry_count, "network.retries") cfg.retries = toml.toml_get_int(keys, vals, entry_count, "network.retries") end if if toml.toml_has_key(keys, vals, entry_count, "network.use_proxy") cfg.use_proxy = toml.toml_get_bool(keys, vals, entry_count, "network.use_proxy") end if // [sync] section if toml.toml_has_key(keys, vals, entry_count, "sync.auto_refresh") cfg.auto_refresh = toml.toml_get_bool(keys, vals, entry_count, "sync.auto_refresh") end if // [cache] section if toml.toml_has_key(keys, vals, entry_count, "cache.keep_packages") cfg.keep_packages = toml.toml_get_bool(keys, vals, entry_count, "cache.keep_packages") end if if toml.toml_has_key(keys, vals, entry_count, "cache.keep_sources") cfg.keep_sources = toml.toml_get_bool(keys, vals, entry_count, "cache.keep_sources") end if // [security] section if toml.toml_has_key(keys, vals, entry_count, "security.require_signed_repos") cfg.require_signed_repos = toml.toml_get_bool(keys, vals, entry_count, "security.require_signed_repos") end if if toml.toml_has_key(keys, vals, entry_count, "security.require_signed_packages") cfg.require_signed_packages = toml.toml_get_bool(keys, vals, entry_count, "security.require_signed_packages") end if // [install] section if toml.toml_has_key(keys, vals, entry_count, "install.chroot_scripts") cfg.chroot_scripts = toml.toml_get_bool(keys, vals, entry_count, "install.chroot_scripts") end if return cfg end load // Accessor functions fn get_arch(cfg: Config): string return cfg.arch end get_arch fn get_jobs(cfg: Config): int return cfg.jobs end get_jobs fn get_confirm(cfg: Config): bool return cfg.confirm end get_confirm fn get_color(cfg: Config): bool return cfg.color end get_color fn get_ports_dir(cfg: Config): string return cfg.ports_dir end get_ports_dir fn get_packages_dir(cfg: Config): string return cfg.packages_dir end get_packages_dir fn get_sources_dir(cfg: Config): string return cfg.sources_dir end get_sources_dir fn get_database_dir(cfg: Config): string return cfg.database_dir end get_database_dir fn get_build_dir(cfg: Config): string return cfg.build_dir end get_build_dir fn get_index_dir(cfg: Config): string return cfg.index_dir end get_index_dir // Legacy accessors (for compatibility) fn get_work_dir(cfg: Config): string return cfg.build_dir end get_work_dir fn get_pkg_dir(cfg: Config): string return cfg.build_dir end get_pkg_dir fn get_cflags(cfg: Config): string return cfg.cflags end get_cflags fn get_cxxflags(cfg: Config): string return cfg.cxxflags end get_cxxflags fn get_makeflags(cfg: Config): string return cfg.makeflags end get_makeflags fn get_strip(cfg: Config): bool return cfg.strip end get_strip fn get_fallback_to_source(cfg: Config): bool return cfg.fallback_to_source end get_fallback_to_source fn get_compress_man(cfg: Config): bool return cfg.compress_man end get_compress_man fn get_clean_work(cfg: Config): bool return cfg.clean_work end get_clean_work fn get_clean_pkg(cfg: Config): bool return cfg.clean_pkg end get_clean_pkg fn get_logging(cfg: Config): bool return cfg.logging end get_logging fn get_quiet(cfg: Config): bool return cfg.quiet end get_quiet fn get_timeout(cfg: Config): int return cfg.timeout end get_timeout fn get_retries(cfg: Config): int return cfg.retries end get_retries fn get_use_proxy(cfg: Config): bool return cfg.use_proxy end get_use_proxy fn get_auto_refresh(cfg: Config): bool return cfg.auto_refresh end get_auto_refresh fn get_keep_packages(cfg: Config): bool return cfg.keep_packages end get_keep_packages fn get_keep_sources(cfg: Config): bool return cfg.keep_sources end get_keep_sources fn get_require_signed_repos(cfg: Config): bool return cfg.require_signed_repos end get_require_signed_repos fn get_require_signed_packages(cfg: Config): bool return cfg.require_signed_packages end get_require_signed_packages fn get_chroot_scripts(cfg: Config): bool return cfg.chroot_scripts end get_chroot_scripts // Get the alternate root path (empty string means /) fn get_root(cfg: Config): string return cfg.root end get_root // Get the installation prefix (default /usr/local) fn get_prefix(cfg: Config): string return cfg.prefix end get_prefix // Get the system configuration directory (default /etc) fn get_sysconfdir(cfg: Config): string return cfg.sysconfdir end get_sysconfdir // Get database directory with root prepended fn get_database_dir_rooted(cfg: Config): string if str.length(cfg.root) == 0 return cfg.database_dir end if return cfg.root + cfg.database_dir end get_database_dir_rooted // Get the full install prefix (root + prefix) // This is where package files should be installed fn get_install_prefix(cfg: Config): string if str.length(cfg.root) == 0 return cfg.prefix end if return cfg.root + cfg.prefix end get_install_prefix // Apply command-line overrides to config // Call this after loading config to override with --root and --prefix flags fn apply_overrides(cfg: Config, root: string, prefix: string, no_chroot: bool): Config mut result = cfg if str.length(root) > 0 result.root = root end if if str.length(prefix) > 0 result.prefix = prefix end if if no_chroot result.chroot_scripts = false end if return result end apply_overrides end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: database.reef Authors: Chris Tusa License: Description: Installed package database management ******************************************************************************/ module core.database import types import io.file import io.dir import io.path import core.str import encoding.toml import util.mtree import sys.process import core.result as res export // Check if a package is installed fn is_installed(name: string): bool // List all installed packages (returns count, fills names array) fn list_installed(names: [string], max_count: int): int // Get installed package details fn get_installed(name: string): types.InstalledPackage // Register a new installed package fn register_package(pkg: types.InstalledPackage): bool // Register package with file list copied from extract dir (more efficient) fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool // Unregister (remove) a package from database fn unregister_package(name: string): bool // Get list of files owned by a package (returns count, fills files array) fn get_files(name: string, files: [string], max_count: int): int // Find which package owns a given file path fn find_owner(file_path: string): string // Find which package provides an alternative fn find_provider(alternative: string): string // Ensure database directories exist fn init_db(): bool // Scripts support fn get_scripts_dir(name: string): string fn store_scripts(name: string, extract_dir: string): bool fn has_stored_scripts(name: string): bool // Config files support fn store_config_files(name: string, extract_dir: string): bool fn get_config_files(name: string, config_files: [string], max_count: int): int // Manifest support fn get_manifest_path(name: string): string // Root-aware versions for alternate root installation fn init_db_rooted(root: string): bool fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool fn is_installed_rooted(name: string, root: string): bool fn list_installed_rooted(names: [string], max_count: int, root: string): int fn get_installed_rooted(name: string, root: string): types.InstalledPackage fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int fn find_owner_rooted(file_path: string, root: string): string fn unregister_package_rooted(name: string, root: string): bool end export // Get the package database directory for a specific package fn pkg_db_dir(name: string): string return path.join_path(types.get_db_dir(), name) end pkg_db_dir // Get the pkg.toml path for a package fn pkg_toml_path(name: string): string return path.join_path(pkg_db_dir(name), "pkg.toml") end pkg_toml_path // Get the files.txt path for a package fn pkg_files_path(name: string): string return path.join_path(pkg_db_dir(name), "files.txt") end pkg_files_path // Get the manifest.mtree path for a package fn get_manifest_path(name: string): string return path.join_path(pkg_db_dir(name), "manifest.mtree") end get_manifest_path // Initialize the database directory structure fn init_db(): bool if not dir.dir_exists(types.get_db_dir()) return res.is_ok(dir.create_dir_all(types.get_db_dir())) end if return true end init_db // Check if a package is installed by looking for its database entry fn is_installed(name: string): bool let pkg_dir = pkg_db_dir(name) if not dir.dir_exists(pkg_dir) return false end if // Also verify pkg.toml exists return file.fileExists(pkg_toml_path(name)) end is_installed // List all installed packages by scanning the database directory fn list_installed(names: [string], max_count: int): int if not dir.dir_exists(types.get_db_dir()) return 0 end if let entries = res.unwrap_or(dir.list_dir(types.get_db_dir()), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Skip hidden files if str.length(entry) > 0 and entry[0] != '.' // Verify it's a valid package (has pkg.toml) let toml_path = pkg_toml_path(entry) if file.fileExists(toml_path) names[count] = entry count = count + 1 end if end if i = i + 1 end while return count end list_installed // Create an empty InstalledPackage fn new_installed_package(): types.InstalledPackage return types.new_installed_package() end new_installed_package // Get details of an installed package fn get_installed(name: string): types.InstalledPackage if not is_installed(name) return new_installed_package() end if // Read pkg.toml let toml_path = pkg_toml_path(name) let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) == 0 return new_installed_package() end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return new_installed_package() end if // Build package info mut pkg = new_installed_package() pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name") pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version") pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description") pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url") pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license") pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch") // Install metadata pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date") pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason") // Read files list let files_path = pkg_files_path(name) if file.fileExists(files_path) let files_content = res.unwrap_or(file.readFile(files_path), "") if str.length(files_content) > 0 let alloc_size = count_newlines(files_content) + 16 mut files_arr: [string] = new [string](alloc_size) let files_count = str.split(files_content, '\n', files_arr, alloc_size) // Filter out empty lines mut valid_count = 0 mut j = 0 while j < files_count if str.length(files_arr[j]) > 0 valid_count = valid_count + 1 end if j = j + 1 end while pkg.files = files_arr pkg.files_count = valid_count end if end if return pkg end get_installed // Generate TOML content for a package fn generate_pkg_toml(pkg: types.InstalledPackage): string mut content = "[package]\n" content = content + "name = \"" + pkg.info.name + "\"\n" content = content + "version = \"" + pkg.info.version + "\"\n" content = content + "release = " + int_to_str(pkg.info.release) + "\n" content = content + "description = \"" + pkg.info.description + "\"\n" content = content + "url = \"" + pkg.info.url + "\"\n" content = content + "license = \"" + pkg.info.license + "\"\n" content = content + "maintainer = \"" + pkg.info.maintainer + "\"\n" content = content + "arch = \"" + pkg.info.arch + "\"\n" content = content + "\n" content = content + "[install]\n" content = content + "date = \"" + pkg.install_date + "\"\n" content = content + "reason = \"" + pkg.install_reason + "\"\n" return content end generate_pkg_toml // 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 // Register a newly installed package fn register_package(pkg: types.InstalledPackage): bool // Ensure database directory exists if not init_db() return false end if // Create package directory let pkg_dir = pkg_db_dir(pkg.info.name) if not dir.dir_exists(pkg_dir) if not res.is_ok(dir.create_dir_all(pkg_dir)) return false end if end if // Write pkg.toml let toml_content = generate_pkg_toml(pkg) let toml_path = pkg_toml_path(pkg.info.name) if not res.is_ok(file.writeFile(toml_path, toml_content)) return false end if // Note: files.txt is written by register_from_extract_dir for performance return true end register_package // Count newline characters in a string to estimate line count fn count_newlines(s: string): int let slen = str.length(s) mut count = 0 mut i = 0 while i < slen if s[i] == '\n' count = count + 1 end if i = i + 1 end while return count end count_newlines // Process a .FOOTPRINT file content: filter and normalize paths // Uses str.join() for O(n) single-allocation string building fn process_footprint(content: string): string let newline_count = count_newlines(content) let alloc_size = newline_count + 16 mut lines: [string] = new [string](alloc_size) let line_count = str.split(content, '\n', lines, alloc_size) mut result_lines: [string] = new [string](alloc_size) mut result_count = 0 mut i = 0 while i < line_count mut line = str.trim_ws(lines[i]) if str.length(line) == 0 i = i + 1 continue end if // Strip leading ./ if str.starts_with(line, "./") line = str.substring(line, 2, str.length(line) - 2) end if // Skip metadata files if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT") i = i + 1 continue end if if str.starts_with(line, ".SCRIPTS") i = i + 1 continue end if if str.equals(line, ".CONFIG") i = i + 1 continue end if // Skip directories (end with /) let len = str.length(line) if len > 0 and line[len - 1] == '/' i = i + 1 continue end if result_lines[result_count] = line result_count = result_count + 1 i = i + 1 end while return str.join(result_lines, result_count, "\n") end process_footprint // Extract file paths from mtree manifest content for files.txt // Parses manifest entries and returns a newline-joined list of relative paths // (files and symlinks only, no directories, no metadata) fn extract_paths_from_manifest(content: string): string mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let count = mtree.parse_manifest(content, entries, 8192) mut result_lines: [string] = new [string](8192) mut result_count = 0 mut i = 0 while i < count let etype = mtree.entry_type(entries[i]) let epath = mtree.entry_path(entries[i]) // Only include files and symlinks (matches legacy files.txt behavior) if str.equals(etype, "file") or str.equals(etype, "link") // Strip leading ./ if str.starts_with(epath, "./") result_lines[result_count] = str.substring(epath, 2, str.length(epath) - 2) else result_lines[result_count] = epath end if result_count = result_count + 1 end if i = i + 1 end while return str.join(result_lines, result_count, "\n") end extract_paths_from_manifest // Register package and copy files list from extracted package directory fn register_from_extract_dir(pkg: types.InstalledPackage, extract_dir: string): bool // Register the package metadata if not register_package(pkg) return false end if let files_path = pkg_files_path(pkg.info.name) // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy) let manifest_path = path.join_path(extract_dir, ".MANIFEST") if file.fileExists(manifest_path) // Store the full manifest for future use (verify, manifest-based remove) let manifest_store = path.join_path(pkg_db_dir(pkg.info.name), "manifest.mtree") let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") file.writeFile(manifest_store, manifest_content) // Also generate files.txt for backward compatibility let result = extract_paths_from_manifest(manifest_content) return res.is_ok(file.writeFile(files_path, result)) end if // Legacy: .FOOTPRINT let footprint_path = path.join_path(extract_dir, ".FOOTPRINT") if not file.fileExists(footprint_path) file.writeFile(files_path, "") return true end if let content = res.unwrap_or(file.readFile(footprint_path), "") if str.length(content) == 0 file.writeFile(files_path, "") return true end if let result = process_footprint(content) return res.is_ok(file.writeFile(files_path, result)) end register_from_extract_dir // Helper to remove a file fn remove_file(file_path: string): bool let cmd = "rm -f \"" + file_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 remove_file // Unregister a package from the database fn unregister_package(name: string): bool if not is_installed(name) return true // Already not installed end if let pkg_dir = pkg_db_dir(name) // Remove pkg.toml let toml_path = pkg_toml_path(name) if file.fileExists(toml_path) if not remove_file(toml_path) return false end if end if // Remove files.txt let files_path = pkg_files_path(name) if file.fileExists(files_path) if not remove_file(files_path) return false end if end if // Remove manifest.mtree let manifest_path = get_manifest_path(name) if file.fileExists(manifest_path) remove_file(manifest_path) end if // Remove scripts directory if exists let scripts_dir = get_scripts_dir(name) if dir.dir_exists(scripts_dir) let cmd = "rm -rf \"" + scripts_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if // Remove config.txt if exists let config_path = pkg_config_path(name) if file.fileExists(config_path) remove_file(config_path) end if // Remove the package directory return res.is_ok(dir.remove_dir(pkg_dir)) end unregister_package // Get list of files owned by a package fn get_files(name: string, files: [string], max_count: int): int if not is_installed(name) return 0 end if let files_path = pkg_files_path(name) if not file.fileExists(files_path) return 0 end if let content = res.unwrap_or(file.readFile(files_path), "") if str.length(content) == 0 return 0 end if // Split by newlines — size dynamically let alloc_size = count_newlines(content) + 16 mut all_lines: [string] = new [string](alloc_size) let line_count = str.split(content, '\n', all_lines, alloc_size) // 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(all_lines[i]) if str.length(line) > 0 files[count] = line count = count + 1 end if i = i + 1 end while return count end get_files // Find which package owns a given file fn find_owner(file_path: string): string // Normalize the path - strip leading "/" if present mut search_path = file_path if str.length(search_path) > 0 and search_path[0] == '/' search_path = str.substring(search_path, 1, str.length(search_path) - 1) end if // Get list of all installed packages mut packages: [string] = new [string](256) let pkg_count = list_installed(packages, 256) mut i = 0 while i < pkg_count let pkg_name = packages[i] // Get files for this package mut files: [string] = new [string](4096) let file_count = get_files(pkg_name, files, 4096) // Search for matching file mut j = 0 while j < file_count if files[j] == search_path return pkg_name end if j = j + 1 end while i = i + 1 end while return "" // No owner found end find_owner // Find which package provides an alternative fn find_provider(alternative: string): string // Get list of all installed packages mut packages: [string] = new [string](256) let pkg_count = list_installed(packages, 256) mut i = 0 while i < pkg_count let pkg_name = packages[i] let pkg = get_installed(pkg_name) // Check alternatives array // Note: We'd need to parse the alternatives array from pkg.toml // For now, check if this package provides the alternative let toml_path = pkg_toml_path(pkg_name) let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) > 0 let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if toml.toml_has_key(keys, vals, entry_count, "package.alternatives") let alts_val = toml.toml_get(keys, vals, entry_count, "package.alternatives") // Check if the alternative is in the array if str.contains(alts_val, alternative) return pkg_name end if end if end if i = i + 1 end while return "" // No provider found end find_provider // Get the scripts directory path for a package in the database fn get_scripts_dir(name: string): string return path.join_path(pkg_db_dir(name), "scripts") end get_scripts_dir // Check if a package has stored scripts in the database fn has_stored_scripts(name: string): bool let scripts_path = get_scripts_dir(name) return dir.dir_exists(scripts_path) end has_stored_scripts // Store scripts from an extracted package to the database fn store_scripts(name: string, extract_dir: string): bool let src_scripts = path.join_path(extract_dir, ".SCRIPTS") // Check if source has scripts if not dir.dir_exists(src_scripts) return true // No scripts is not an error end if let dest_scripts = get_scripts_dir(name) // Create scripts directory in database if not dir.dir_exists(dest_scripts) if not res.is_ok(dir.create_dir_all(dest_scripts)) return false end if end if // Copy all script files let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if let exit_code = process.process_wait(pid) // Don't fail if cp fails (might be empty directory) return true end store_scripts // Get the config.txt path for a package fn pkg_config_path(name: string): string return path.join_path(pkg_db_dir(name), "config.txt") end pkg_config_path // Store config files list from an extracted package to the database fn store_config_files(name: string, extract_dir: string): bool let src_config = path.join_path(extract_dir, ".CONFIG") // Check if source has config files list if not file.fileExists(src_config) return true // No config files is not an error end if let dest_config = pkg_config_path(name) // Copy the config files list let content = res.unwrap_or(file.readFile(src_config), "") if str.length(content) > 0 return res.is_ok(file.writeFile(dest_config, content)) end if return true end store_config_files // Get list of config files for a package fn get_config_files(name: string, config_files: [string], max_count: int): int let config_path = pkg_config_path(name) if not file.fileExists(config_path) return 0 end if let content = res.unwrap_or(file.readFile(config_path), "") 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) // Filter out empty lines and comments 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 and line[0] != '#' config_files[count] = line count = count + 1 end if i = i + 1 end while return count end get_config_files // ============================================================================ // Root-aware functions for alternate root installation // ============================================================================ // Get the rooted database directory fn get_db_dir_rooted(root: string): string if str.length(root) == 0 return types.get_db_dir() end if return root + types.get_db_dir() end get_db_dir_rooted // Get the rooted package database directory fn pkg_db_dir_rooted(name: string, root: string): string return path.join_path(get_db_dir_rooted(root), name) end pkg_db_dir_rooted // Get the rooted pkg.toml path fn pkg_toml_path_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "pkg.toml") end pkg_toml_path_rooted // Get the rooted files.txt path fn pkg_files_path_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "files.txt") end pkg_files_path_rooted // Initialize database with alternate root fn init_db_rooted(root: string): bool let db_dir = get_db_dir_rooted(root) if not dir.dir_exists(db_dir) return res.is_ok(dir.create_dir_all(db_dir)) end if return true end init_db_rooted // Check if package is installed in alternate root fn is_installed_rooted(name: string, root: string): bool let pkg_dir = pkg_db_dir_rooted(name, root) if not dir.dir_exists(pkg_dir) return false end if return file.fileExists(pkg_toml_path_rooted(name, root)) end is_installed_rooted // Register package with alternate root fn register_package_rooted(pkg: types.InstalledPackage, root: string): bool // Ensure database directory exists if not init_db_rooted(root) return false end if // Create package directory let pkg_dir = pkg_db_dir_rooted(pkg.info.name, root) if not dir.dir_exists(pkg_dir) if not res.is_ok(dir.create_dir_all(pkg_dir)) return false end if end if // Write pkg.toml let toml_content = generate_pkg_toml(pkg) let toml_path = pkg_toml_path_rooted(pkg.info.name, root) if not res.is_ok(file.writeFile(toml_path, toml_content)) return false end if return true end register_package_rooted // Register package from extract dir with alternate root fn register_from_extract_dir_rooted(pkg: types.InstalledPackage, extract_dir: string, root: string): bool // Register the package metadata if not register_package_rooted(pkg, root) return false end if let files_path = pkg_files_path_rooted(pkg.info.name, root) // Try .MANIFEST first (v2 format), fall back to .FOOTPRINT (legacy) let manifest_path = path.join_path(extract_dir, ".MANIFEST") if file.fileExists(manifest_path) // Store the full manifest let manifest_store = path.join_path(pkg_db_dir_rooted(pkg.info.name, root), "manifest.mtree") let manifest_content = res.unwrap_or(file.readFile(manifest_path), "") file.writeFile(manifest_store, manifest_content) // Also generate files.txt for backward compatibility let result = extract_paths_from_manifest(manifest_content) return res.is_ok(file.writeFile(files_path, result)) end if // Legacy: .FOOTPRINT let footprint_path = path.join_path(extract_dir, ".FOOTPRINT") if not file.fileExists(footprint_path) file.writeFile(files_path, "") return true end if let content = res.unwrap_or(file.readFile(footprint_path), "") if str.length(content) == 0 file.writeFile(files_path, "") return true end if let result = process_footprint(content) return res.is_ok(file.writeFile(files_path, result)) end register_from_extract_dir_rooted // Get scripts directory with alternate root fn get_scripts_dir_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "scripts") end get_scripts_dir_rooted // Store scripts with alternate root fn store_scripts_rooted(name: string, extract_dir: string, root: string): bool let src_scripts = path.join_path(extract_dir, ".SCRIPTS") if not dir.dir_exists(src_scripts) return true end if let dest_scripts = get_scripts_dir_rooted(name, root) if not dir.dir_exists(dest_scripts) if not res.is_ok(dir.create_dir_all(dest_scripts)) return false end if end if let cmd = "cp -r \"" + src_scripts + "/\"* \"" + dest_scripts + "/\" 2>/dev/null" let pid = process.process_spawn_shell(cmd) if pid < 0 return false end if process.process_wait(pid) return true end store_scripts_rooted // Get config path with alternate root fn pkg_config_path_rooted(name: string, root: string): string return path.join_path(pkg_db_dir_rooted(name, root), "config.txt") end pkg_config_path_rooted // Store config files with alternate root fn store_config_files_rooted(name: string, extract_dir: string, root: string): bool let src_config = path.join_path(extract_dir, ".CONFIG") if not file.fileExists(src_config) return true end if let dest_config = pkg_config_path_rooted(name, root) let content = res.unwrap_or(file.readFile(src_config), "") if str.length(content) > 0 return res.is_ok(file.writeFile(dest_config, content)) end if return true end store_config_files_rooted // List all installed packages in alternate root fn list_installed_rooted(names: [string], max_count: int, root: string): int let db_dir = get_db_dir_rooted(root) if not dir.dir_exists(db_dir) return 0 end if let entries = res.unwrap_or(dir.list_dir(db_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Skip hidden files if str.length(entry) > 0 and entry[0] != '.' // Verify it's a valid package (has pkg.toml) let toml_path = pkg_toml_path_rooted(entry, root) if file.fileExists(toml_path) names[count] = entry count = count + 1 end if end if i = i + 1 end while return count end list_installed_rooted // Get details of an installed package in alternate root fn get_installed_rooted(name: string, root: string): types.InstalledPackage if not is_installed_rooted(name, root) return new_installed_package() end if // Read pkg.toml let toml_path = pkg_toml_path_rooted(name, root) let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) == 0 return new_installed_package() end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return new_installed_package() end if // Build package info mut pkg = new_installed_package() pkg.info.name = toml.toml_get(keys, vals, entry_count, "package.name") pkg.info.version = toml.toml_get(keys, vals, entry_count, "package.version") pkg.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") pkg.info.description = toml.toml_get(keys, vals, entry_count, "package.description") pkg.info.url = toml.toml_get(keys, vals, entry_count, "package.url") pkg.info.license = toml.toml_get(keys, vals, entry_count, "package.license") pkg.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") pkg.info.arch = toml.toml_get(keys, vals, entry_count, "package.arch") // Install metadata pkg.install_date = toml.toml_get(keys, vals, entry_count, "install.date") pkg.install_reason = toml.toml_get(keys, vals, entry_count, "install.reason") // Read files list let files_path = pkg_files_path_rooted(name, root) if file.fileExists(files_path) let files_content = res.unwrap_or(file.readFile(files_path), "") if str.length(files_content) > 0 let alloc_size = count_newlines(files_content) + 16 mut files_arr: [string] = new [string](alloc_size) let files_count = str.split(files_content, '\n', files_arr, alloc_size) // Filter out empty lines mut valid_count = 0 mut j = 0 while j < files_count if str.length(files_arr[j]) > 0 valid_count = valid_count + 1 end if j = j + 1 end while pkg.files = files_arr pkg.files_count = valid_count end if end if return pkg end get_installed_rooted // Get list of files owned by a package in alternate root fn get_files_rooted(name: string, files: [string], max_count: int, root: string): int if not is_installed_rooted(name, root) return 0 end if let files_path = pkg_files_path_rooted(name, root) if not file.fileExists(files_path) return 0 end if let content = res.unwrap_or(file.readFile(files_path), "") if str.length(content) == 0 return 0 end if // Split by newlines — size dynamically let alloc_size = count_newlines(content) + 16 mut all_lines: [string] = new [string](alloc_size) let line_count = str.split(content, '\n', all_lines, alloc_size) // 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(all_lines[i]) if str.length(line) > 0 files[count] = line count = count + 1 end if i = i + 1 end while return count end get_files_rooted // Find which package owns a given file in alternate root fn find_owner_rooted(file_path: string, root: string): string // Normalize the path - strip leading "/" if present mut search_path = file_path if str.length(search_path) > 0 and search_path[0] == '/' search_path = str.substring(search_path, 1, str.length(search_path) - 1) end if // Get list of all installed packages in this root mut packages: [string] = new [string](256) let pkg_count = list_installed_rooted(packages, 256, root) mut i = 0 while i < pkg_count let pkg_name = packages[i] // Get files for this package mut files: [string] = new [string](4096) let file_count = get_files_rooted(pkg_name, files, 4096, root) // Search for matching file mut j = 0 while j < file_count if files[j] == search_path return pkg_name end if j = j + 1 end while i = i + 1 end while return "" // No owner found end find_owner_rooted // Unregister a package from the database in alternate root fn unregister_package_rooted(name: string, root: string): bool if not is_installed_rooted(name, root) return true // Already not installed end if let pkg_dir = pkg_db_dir_rooted(name, root) // Remove pkg.toml let toml_path = pkg_toml_path_rooted(name, root) if file.fileExists(toml_path) if not remove_file(toml_path) return false end if end if // Remove files.txt let files_path = pkg_files_path_rooted(name, root) if file.fileExists(files_path) if not remove_file(files_path) return false end if end if // Remove scripts directory if exists let scripts_dir = get_scripts_dir_rooted(name, root) if dir.dir_exists(scripts_dir) let cmd = "rm -rf \"" + scripts_dir + "\"" let pid = process.process_spawn_shell(cmd) if pid > 0 process.process_wait(pid) end if end if // Remove config.txt if exists let config_path = pkg_config_path_rooted(name, root) if file.fileExists(config_path) remove_file(config_path) end if // Remove the package directory return res.is_ok(dir.remove_dir(pkg_dir)) end unregister_package_rooted end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: groups.reef Authors: Chris Tusa License: Description: Package groups support for Coral (@groupname) ******************************************************************************/ module core.groups import core.str import io.file import io.dir import io.path import encoding.toml import types import core.result as res export type GroupResult fn load_group(name: string): GroupResult fn list_groups(groups: [types.PackageGroup], max_count: int): int fn expand_group(name: string, packages: [string], max_count: int): int fn is_group(name: string): bool end export // Result for group loading type GroupResult = struct success: bool group: types.PackageGroup error: string end GroupResult // Get the groups directory fn get_groups_dir(): string return types.get_groups_dir() end get_groups_dir // Create empty group fn new_group(): types.PackageGroup return types.new_package_group() end new_group // Create success result fn success_result(g: types.PackageGroup): GroupResult return GroupResult{ success: true, group: g, error: "" } end success_result // Create error result fn error_result(msg: string): GroupResult return GroupResult{ success: false, group: new_group(), error: msg } end error_result // Check if a package name is a group reference (starts with @) fn is_group(name: string): bool if str.length(name) < 2 return false end if return name[0] == '@' end is_group // Load a group definition from TOML file fn load_group(name: string): GroupResult // Remove @ prefix if present mut group_name = name if name[0] == '@' group_name = str.substring(name, 1, str.length(name) - 1) end if let groups_dir = get_groups_dir() let group_file = path.join_path(groups_dir, group_name + ".toml") if not file.fileExists(group_file) return error_result("Group not found: " + group_name) end if let content = res.unwrap_or(file.readFile(group_file), "") if str.length(content) == 0 return error_result("Failed to read group file") end if // Parse TOML using key/value arrays let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return error_result("Invalid group file format") end if mut group = new_group() group.name = group_name // Get description if toml.toml_has_key(keys, vals, entry_count, "group.description") group.description = toml.toml_get(keys, vals, entry_count, "group.description") end if // Get members list (packages array) if toml.toml_has_key(keys, vals, entry_count, "group.packages") let members_str = toml.toml_get(keys, vals, entry_count, "group.packages") group.members_count = parse_toml_array(members_str, group.members, 256) end if return success_result(group) end load_group // List all available groups fn list_groups(groups: [types.PackageGroup], max_count: int): int let groups_dir = get_groups_dir() if not dir.dir_exists(groups_dir) return 0 end if let entries = res.unwrap_or(dir.list_dir(groups_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Look for .toml files if str.ends_with(entry, ".toml") let name_len = str.length(entry) - 5 let name = str.substring(entry, 0, name_len) let result = load_group(name) if result.success groups[count] = result.group count = count + 1 end if end if i = i + 1 end while return count end list_groups // Expand a group to its member packages fn expand_group(name: string, packages: [string], max_count: int): int let result = load_group(name) if not result.success return 0 end if let group = result.group mut count = 0 while count < group.members_count and count < max_count packages[count] = group.members[count] count = count + 1 end while return count end expand_group // Parse TOML array value into string array // Format: ["item1", "item2", "item3"] fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if // Check for array brackets if value[0] != '[' return 0 end if mut count = 0 mut i = 1 mut in_string = false mut item_start = 0 while i < len and count < max_items let c = value[i] if c == '"' and not in_string in_string = true item_start = i + 1 elif c == '"' and in_string // End of string item let item_len = i - item_start if item_len > 0 result[count] = str.substring(value, item_start, item_len) count = count + 1 end if in_string = false elif c == ']' and not in_string // End of array return count end if i = i + 1 end while return count end parse_toml_array end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: package.reef Authors: Chris Tusa License: Description: Package archive handling - extract, read metadata, install files ******************************************************************************/ module core.package import types import io.file import io.dir import io.path import core.str import encoding.toml import util.archive import util.mtree import util.checksum import sys.process import fs.stat import fs.perm import fs.link import fs.ops import core.result as res export // Package extraction result type ExtractResult // Extract a package to a temporary directory fn extract_package(pkg_path: string, dest_dir: string): ExtractResult // Read package info from an extracted package or archive fn read_pkginfo(pkg_path: string): types.PackageInfo // Read file list from an extracted package (legacy .FOOTPRINT or .MANIFEST) fn read_footprint(pkg_path: string, files: [string], max_count: int): int // Read manifest entries from an extracted package fn read_manifest(pkg_dir: string, entries: [mtree.ManifestEntry], max_count: int): int // Install files from extracted package to root (manifest-driven) fn install_files(src_dir: string, root: string): bool // Remove files listed in footprint from root fn remove_files(files: [string], file_count: int, root: string): bool // Remove files using manifest entries (with explicit directory tracking) fn remove_files_manifest(entries: [mtree.ManifestEntry], entry_count: int, root: string): bool // Verify package integrity (check all files exist in extracted dir) fn verify_package(pkg_dir: string): bool // Install files with old manifest for upgrade config protection fn install_files_upgrade(src_dir: string, root: string, old_entries: [mtree.ManifestEntry], old_entry_count: int): bool // Script execution context (chroot-confined maintainer scripts) type ScriptCtx fn new_script_ctx(root: string, do_chroot: bool): ScriptCtx fn script_wants_chroot(root: string, chroot_scripts: bool): bool fn build_script_cmd(ctx: ScriptCtx, script_path: string, script_name: string, args: string, is_sh: bool): string fn resolve_script_ctx(root: string, chroot_scripts: bool, caller_is_root: bool): ScriptCtx // Install scripts support (version passed as $VERSION to scripts) fn has_scripts(pkg_dir: string): bool fn run_pre_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool fn run_post_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool fn run_pre_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool fn run_post_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool // Upgrade scripts ($OLD_VERSION $NEW_VERSION passed to scripts) fn run_pre_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool fn run_post_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool // Run scripts from a stored scripts directory (for remove/upgrade operations) fn run_stored_pre_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool fn run_stored_post_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool fn run_stored_pre_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool fn run_stored_post_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool // Config file protection fn read_config_files(pkg_dir: string, config_files: [string], max_count: int): int fn is_config_file(rel_path: string, config_files: [string], config_count: int): bool fn install_config_file(src_path: string, dest_path: string, manifest_sha256: string, stored_sha256: string): bool end export // Extraction result type ExtractResult = struct success: bool extract_dir: string error: string end ExtractResult // Create a failed extraction result fn extract_error(msg: string): ExtractResult return ExtractResult{ success: false, extract_dir: "", error: msg } end extract_error // Create a successful extraction result fn extract_success(dir: string): ExtractResult return ExtractResult{ success: true, extract_dir: dir, error: "" } end extract_success // Extract a package archive to destination directory fn extract_package(pkg_path: string, dest_dir: string): ExtractResult // Verify package exists if not file.fileExists(pkg_path) return extract_error("Package file not found: " + pkg_path) 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 extract_error("Failed to create extraction directory: " + dest_dir) end if end if // Extract using archive module if not archive.extract(pkg_path, dest_dir) return extract_error("Failed to extract package: " + pkg_path) end if // Verify .PKGINFO exists let pkginfo_path = path.join_path(dest_dir, ".PKGINFO") if not file.fileExists(pkginfo_path) return extract_error("Invalid package: missing .PKGINFO") end if return extract_success(dest_dir) end extract_package // Read package info from .PKGINFO in an extracted package directory fn read_pkginfo(pkg_dir: string): types.PackageInfo mut info = types.new_package_info() let pkginfo_path = path.join_path(pkg_dir, ".PKGINFO") if not file.fileExists(pkginfo_path) return info end if let content = res.unwrap_or(file.readFile(pkginfo_path), "") if str.length(content) == 0 return info end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return info end if // Extract fields info.name = toml.toml_get(keys, vals, entry_count, "package.name") info.version = toml.toml_get(keys, vals, entry_count, "package.version") info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") info.description = toml.toml_get(keys, vals, entry_count, "package.description") info.url = toml.toml_get(keys, vals, entry_count, "package.url") info.license = toml.toml_get(keys, vals, entry_count, "package.license") info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") info.arch = toml.toml_get(keys, vals, entry_count, "package.arch") // Parse [dependencies] section (optional — legacy packages won't have it) if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime") let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime") info.runtime_deps_count = parse_toml_array(rt_val, info.runtime_deps, 64) end if if toml.toml_has_key(keys, vals, entry_count, "dependencies.build") let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build") info.build_deps_count = parse_toml_array(bd_val, info.build_deps, 64) end if return info end read_pkginfo // Read file list from .MANIFEST (v2) or .FOOTPRINT (legacy) // Returns a flat list of relative file paths (files and symlinks only) fn read_footprint(pkg_dir: string, files: [string], max_count: int): int // Try .MANIFEST first (v2 format) let manifest_path = path.join_path(pkg_dir, ".MANIFEST") if file.fileExists(manifest_path) let content = res.unwrap_or(file.readFile(manifest_path), "") if str.length(content) > 0 mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](max_count) let entry_count = mtree.parse_manifest(content, entries, max_count) mut count = 0 mut i = 0 while i < entry_count and count < max_count let etype = mtree.entry_type(entries[i]) // Only include files and symlinks (matches legacy behavior) if str.equals(etype, "file") or str.equals(etype, "link") let epath = mtree.entry_path(entries[i]) if str.starts_with(epath, "./") files[count] = str.substring(epath, 2, str.length(epath) - 2) else files[count] = epath end if count = count + 1 end if i = i + 1 end while return count end if end if // Fallback: legacy .FOOTPRINT let footprint_path = path.join_path(pkg_dir, ".FOOTPRINT") if not file.fileExists(footprint_path) return 0 end if let content = res.unwrap_or(file.readFile(footprint_path), "") if str.length(content) == 0 return 0 end if mut lines: [string] = new [string](max_count) let line_count = str.split(content, '\n', lines, max_count) 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 i = i + 1 continue end if if str.equals(line, "./.PKGINFO") or str.equals(line, "./.FOOTPRINT") i = i + 1 continue end if if str.equals(line, ".PKGINFO") or str.equals(line, ".FOOTPRINT") i = i + 1 continue end if let len = str.length(line) if len > 0 and line[len - 1] == '/' i = i + 1 continue end if if str.starts_with(line, "./") files[count] = str.substring(line, 2, len - 2) else files[count] = line end if count = count + 1 i = i + 1 end while return count end read_footprint // Read manifest entries from .MANIFEST in an extracted package directory fn read_manifest(pkg_dir: string, entries: [mtree.ManifestEntry], max_count: int): int let manifest_path = path.join_path(pkg_dir, ".MANIFEST") if not file.fileExists(manifest_path) return 0 end if let content = res.unwrap_or(file.readFile(manifest_path), "") if str.length(content) == 0 return 0 end if return mtree.parse_manifest(content, entries, max_count) end read_manifest // Install files from extracted package to root directory using manifest fn install_files(src_dir: string, root: string): bool // Read config file list for protection checks mut config_files: [string] = new [string](256) let config_count = read_config_files(src_dir, config_files, 256) // Parse .MANIFEST mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let entry_count = read_manifest(src_dir, entries, 8192) if entry_count == 0 return false end if // Walk manifest entries in order (dirs before their contents) mut i = 0 while i < entry_count let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) // Strip leading ./ from manifest path to get relative path mut rel_path = epath if str.starts_with(epath, "./") rel_path = str.substring(epath, 2, str.length(epath) - 2) end if let dest_path = path.join_path(root, rel_path) let src_path = path.join_path(src_dir, rel_path) let mode = mtree.entry_mode(entry) let uid = mtree.name_to_uid(mtree.entry_uname(entry)) let gid = mtree.name_to_gid(mtree.entry_gname(entry)) if str.equals(etype, "dir") // Create directory if it doesn't exist if not dir.dir_exists(dest_path) if not res.is_ok(dir.create_dir_all(dest_path)) return false end if end if perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) elif str.equals(etype, "file") // Check if this is a config-protected file if is_config_file(rel_path, config_files, config_count) // Config file — use checksum-based protection let manifest_sha = mtree.entry_sha256(entry) install_config_file(src_path, dest_path, manifest_sha, "") else // Ensure parent directory exists let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if // Remove any conflicting target (symlink, file, or dir) if stat.is_symlink(dest_path) ops.remove_file(dest_path) elif stat.exists(dest_path) ops.remove_file(dest_path) end if // Copy file preserving permissions from source if not res.is_ok(ops.copy_file_preserve(src_path, dest_path)) return false end if // Set permissions and ownership from manifest perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) // Verify sha256 after copy let manifest_sha = mtree.entry_sha256(entry) if str.length(manifest_sha) > 0 let installed_sha = checksum.sha256_file(dest_path) if not str.equals(installed_sha, manifest_sha) return false end if end if end if elif str.equals(etype, "link") let link_target = mtree.entry_link(entry) // Remove any conflicting target if stat.is_symlink(dest_path) or stat.exists(dest_path) ops.remove_file(dest_path) end if // Ensure parent directory exists let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if // Create symlink if not res.is_ok(link.symlink(link_target, dest_path)) return false end if end if i = i + 1 end while return true end install_files // Remove files from root directory based on flat file list (legacy) fn remove_files(files: [string], file_count: int, root: string): bool // Remove files in reverse order mut i = file_count - 1 while i >= 0 let rel_path = files[i] let full_path = path.join_path(root, rel_path) if stat.is_symlink(full_path) or stat.exists(full_path) ops.remove_file(full_path) end if i = i - 1 end while // Try to remove empty parent directories i = file_count - 1 while i >= 0 let rel_path = files[i] let full_path = path.join_path(root, rel_path) let parent = path.dirname(full_path) if str.length(parent) > 0 and dir.dir_exists(parent) dir.remove_dir(parent) end if i = i - 1 end while return true end remove_files // Remove files using manifest entries (with explicit directory tracking) // Processes in reverse order: files/symlinks first, then directories fn remove_files_manifest(entries: [mtree.ManifestEntry], entry_count: int, root: string): bool mut all_success = true // First pass: remove files and symlinks in reverse order mut i = entry_count - 1 while i >= 0 let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) mut rel_path = epath if str.starts_with(epath, "./") rel_path = str.substring(epath, 2, str.length(epath) - 2) end if let full_path = path.join_path(root, rel_path) if str.equals(etype, "file") or str.equals(etype, "link") if stat.is_symlink(full_path) or stat.exists(full_path) if not res.is_ok(ops.remove_file(full_path)) all_success = false end if end if end if i = i - 1 end while // Second pass: remove directories in reverse order (deepest first) i = entry_count - 1 while i >= 0 let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) if str.equals(etype, "dir") mut rel_path = epath if str.starts_with(epath, "./") rel_path = str.substring(epath, 2, str.length(epath) - 2) end if let full_path = path.join_path(root, rel_path) // Only remove empty directories (remove_dir fails on non-empty) if dir.dir_exists(full_path) dir.remove_dir(full_path) end if end if i = i - 1 end while return all_success end remove_files_manifest // Verify package has required files fn verify_package(pkg_dir: string): bool // Check for .PKGINFO let pkginfo_path = path.join_path(pkg_dir, ".PKGINFO") if not file.fileExists(pkginfo_path) return false end if // Check for .MANIFEST (or legacy .FOOTPRINT) let manifest_path = path.join_path(pkg_dir, ".MANIFEST") let footprint_path = path.join_path(pkg_dir, ".FOOTPRINT") if not file.fileExists(manifest_path) and not file.fileExists(footprint_path) return false end if // Verify we can read package info let info = read_pkginfo(pkg_dir) if str.length(info.name) == 0 return false end if return true end verify_package // Check if package has install/remove scripts fn has_scripts(pkg_dir: string): bool let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") return dir.dir_exists(scripts_dir) end has_scripts // Context describing where a package's maintainer scripts run. // root == "/" -> normal install on the live system // do_chroot -> confine scripts to root via chroot (root must be != "/") type ScriptCtx = struct root: string do_chroot: bool end ScriptCtx // Construct a ScriptCtx. fn new_script_ctx(root: string, do_chroot: bool): ScriptCtx return ScriptCtx{ root: root, do_chroot: do_chroot } end new_script_ctx // Pure predicate: should scripts be chrooted for this root + config? fn script_wants_chroot(root: string, chroot_scripts: bool): bool if str.equals(root, "/") return false end if return chroot_scripts end script_wants_chroot // Resolve the script execution context for a command flow. // want_chroot requires both a non-"/" root with chroot enabled AND a root caller; // the caller is responsible for aborting first when chroot is wanted but caller_is_root // is false (see resolve in the command flows). fn resolve_script_ctx(root: string, chroot_scripts: bool, caller_is_root: bool): ScriptCtx mut want_chroot = script_wants_chroot(root, chroot_scripts) if not caller_is_root want_chroot = false end if return new_script_ctx(root, want_chroot) end resolve_script_ctx // The single place a maintainer-script shell command is assembled. // is_sh true -> run under zsh; false -> execute the file directly. fn build_script_cmd(ctx: ScriptCtx, script_path: string, script_name: string, args: string, is_sh: bool): string mut cmd = "" if ctx.do_chroot // Confined: run inside ; from the script's view the target is "/". let in_root_path = "/.SCRIPTS/" + script_name mut body = "" if is_sh body = "zsh \"" + in_root_path + "\"" else body = "\"" + in_root_path + "\"" end if cmd = "chroot \"" + ctx.root + "\" env CORAL_ROOT=\"/\" " + body else // Host: normal install or --no-chroot-scripts. Export CORAL_ROOT=ctx.root. mut body = "" if is_sh body = "zsh \"" + script_path + "\"" else body = "\"" + script_path + "\"" end if cmd = "CORAL_ROOT=\"" + ctx.root + "\" " + body end if if str.length(args) > 0 cmd = cmd + " " + args end if return cmd end build_script_cmd // Copy a package's .SCRIPTS dir into /.SCRIPTS so it is reachable post-chroot. fn stage_scripts_in_root(pkg_scripts_dir: string, root: string): bool let dest = path.join_path(root, ".SCRIPTS") // Clean any stale staging first. let rm_cmd = "rm -rf \"" + dest + "\"" let rm_pid = process.process_spawn_shell(rm_cmd) process.process_wait(rm_pid) // Copy preserving perms/owner/symlinks. let cp_cmd = "cp -a \"" + pkg_scripts_dir + "\" \"" + dest + "\"" let pid = process.process_spawn_shell(cp_cmd) if pid < 0 return false end if let code = process.process_wait(pid) return code == 0 end stage_scripts_in_root // Remove staged scripts from /.SCRIPTS. proc unstage_scripts_in_root(root: string) let dest = path.join_path(root, ".SCRIPTS") let cmd = "rm -rf \"" + dest + "\"" let pid = process.process_spawn_shell(cmd) process.process_wait(pid) end unstage_scripts_in_root // Run a script from the .SCRIPTS directory fn run_script(ctx: ScriptCtx, pkg_dir: string, script_name: string, script_args: string): bool let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let script_path = path.join_path(scripts_dir, script_name) if not file.fileExists(script_path) return true end if if ctx.do_chroot if not stage_scripts_in_root(scripts_dir, ctx.root) // Staging may have left a partial /.SCRIPTS; clean it before bailing. unstage_scripts_in_root(ctx.root) return false end if end if let is_sh = str.ends_with(script_name, ".sh") let cmd = build_script_cmd(ctx, script_path, script_name, script_args, is_sh) let pid = process.process_spawn_shell(cmd) mut ok = false if pid >= 0 let exit_code = process.process_wait(pid) ok = exit_code == 0 end if if ctx.do_chroot unstage_scripts_in_root(ctx.root) end if return ok end run_script // Run pre-install script (passes $VERSION) fn run_pre_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") // Try .sh first, then bare executable let sh_path = path.join_path(scripts_dir, "pre-install.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "pre-install.sh", args) end if return run_script(ctx, pkg_dir, "pre-install", args) end run_pre_install // Run post-install script (passes $VERSION) fn run_post_install(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "post-install.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "post-install.sh", args) end if return run_script(ctx, pkg_dir, "post-install", args) end run_post_install // Run pre-remove script (passes $VERSION) fn run_pre_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "pre-remove.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "pre-remove.sh", args) end if return run_script(ctx, pkg_dir, "pre-remove", args) end run_pre_remove // Run post-remove script (passes $VERSION) fn run_post_remove(ctx: ScriptCtx, pkg_dir: string, version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "post-remove.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "post-remove.sh", args) end if return run_script(ctx, pkg_dir, "post-remove", args) end run_post_remove // Run pre-upgrade script (passes $OLD_VERSION $NEW_VERSION) fn run_pre_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "pre-upgrade.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "pre-upgrade.sh", args) end if return run_script(ctx, pkg_dir, "pre-upgrade", args) end run_pre_upgrade // Run post-upgrade script (passes $OLD_VERSION $NEW_VERSION) fn run_post_upgrade(ctx: ScriptCtx, pkg_dir: string, old_version: string, new_version: string): bool if not has_scripts(pkg_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let scripts_dir = path.join_path(pkg_dir, ".SCRIPTS") let sh_path = path.join_path(scripts_dir, "post-upgrade.sh") if file.fileExists(sh_path) return run_script(ctx, pkg_dir, "post-upgrade.sh", args) end if return run_script(ctx, pkg_dir, "post-upgrade", args) end run_post_upgrade // Run a script directly from a stored scripts directory fn run_stored_script(ctx: ScriptCtx, scripts_dir: string, script_name: string, script_args: string): bool let script_path = path.join_path(scripts_dir, script_name) if not file.fileExists(script_path) return true end if if ctx.do_chroot if not stage_scripts_in_root(scripts_dir, ctx.root) // Staging may have left a partial /.SCRIPTS; clean it before bailing. unstage_scripts_in_root(ctx.root) return false end if end if let is_sh = str.ends_with(script_name, ".sh") let cmd = build_script_cmd(ctx, script_path, script_name, script_args, is_sh) let pid = process.process_spawn_shell(cmd) mut ok = false if pid >= 0 let exit_code = process.process_wait(pid) ok = exit_code == 0 end if if ctx.do_chroot unstage_scripts_in_root(ctx.root) end if return ok end run_stored_script // Run pre-remove script from stored scripts directory (passes $VERSION) fn run_stored_pre_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + version + "\"" let sh_path = path.join_path(scripts_dir, "pre-remove.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "pre-remove.sh", args) end if return run_stored_script(ctx, scripts_dir, "pre-remove", args) end run_stored_pre_remove // Run post-remove script from stored scripts directory (passes $VERSION) fn run_stored_post_remove(ctx: ScriptCtx, scripts_dir: string, version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + version + "\"" let sh_path = path.join_path(scripts_dir, "post-remove.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "post-remove.sh", args) end if return run_stored_script(ctx, scripts_dir, "post-remove", args) end run_stored_post_remove // Run pre-upgrade script from stored scripts directory (passes $OLD_VERSION $NEW_VERSION) fn run_stored_pre_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let sh_path = path.join_path(scripts_dir, "pre-upgrade.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "pre-upgrade.sh", args) end if return run_stored_script(ctx, scripts_dir, "pre-upgrade", args) end run_stored_pre_upgrade // Run post-upgrade script from stored scripts directory (passes $OLD_VERSION $NEW_VERSION) fn run_stored_post_upgrade(ctx: ScriptCtx, scripts_dir: string, old_version: string, new_version: string): bool if not dir.dir_exists(scripts_dir) return true end if let args = "\"" + old_version + "\" \"" + new_version + "\"" let sh_path = path.join_path(scripts_dir, "post-upgrade.sh") if file.fileExists(sh_path) return run_stored_script(ctx, scripts_dir, "post-upgrade.sh", args) end if return run_stored_script(ctx, scripts_dir, "post-upgrade", args) end run_stored_post_upgrade // Read list of config files from .CONFIG file in extracted package fn read_config_files(pkg_dir: string, config_files: [string], max_count: int): int let config_path = path.join_path(pkg_dir, ".CONFIG") if not file.fileExists(config_path) return 0 end if let content = res.unwrap_or(file.readFile(config_path), "") 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) // Filter out empty lines 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 and line[0] != '#' // Normalize path (remove leading ./) if str.starts_with(line, "./") config_files[count] = str.substring(line, 2, str.length(line) - 2) else config_files[count] = line end if count = count + 1 end if i = i + 1 end while return count end read_config_files // Check if a file path is in the config files list fn is_config_file(rel_path: string, config_files: [string], config_count: int): bool mut i = 0 while i < config_count if str.equals(rel_path, config_files[i]) return true end if i = i + 1 end while return false end is_config_file // Install a config file with checksum-based protection. // manifest_sha256: checksum of the new package's config file // stored_sha256: checksum from the previously installed package version (empty on fresh install) fn install_config_file(src_path: string, dest_path: string, manifest_sha256: string, stored_sha256: string): bool if not stat.exists(dest_path) // Fresh install — file doesn't exist yet // Create parent directory if needed let dest_dir = path.dirname(dest_path) if str.length(dest_dir) > 0 and not dir.dir_exists(dest_dir) dir.create_dir_all(dest_dir) end if return res.is_ok(ops.copy_file_preserve(src_path, dest_path)) end if // File exists — check if user has modified it if str.length(stored_sha256) == 0 // No stored checksum (fresh install over existing file or legacy package) // Be safe: install as .pkg-new, preserve user's file let new_path = dest_path + ".pkg-new" return res.is_ok(ops.copy_file_preserve(src_path, new_path)) end if let installed_sha = checksum.sha256_file(dest_path) if str.equals(installed_sha, stored_sha256) // User has not modified the config — safe to overwrite return res.is_ok(ops.copy_file_preserve(src_path, dest_path)) else // User has modified the config — preserve their version let new_path = dest_path + ".pkg-new" return res.is_ok(ops.copy_file_preserve(src_path, new_path)) end if end install_config_file // Look up sha256 for a path in manifest entries fn find_manifest_sha256(rel_path: string, entries: [mtree.ManifestEntry], count: int): string mut i = 0 while i < count let epath = mtree.entry_path(entries[i]) mut entry_rel = epath if str.starts_with(epath, "./") entry_rel = str.substring(epath, 2, str.length(epath) - 2) end if if str.equals(entry_rel, rel_path) return mtree.entry_sha256(entries[i]) end if i = i + 1 end while return "" end find_manifest_sha256 // Install files from extracted package with old manifest for upgrade config protection fn install_files_upgrade(src_dir: string, root: string, old_entries: [mtree.ManifestEntry], old_entry_count: int): bool mut config_files: [string] = new [string](256) let config_count = read_config_files(src_dir, config_files, 256) mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let entry_count = read_manifest(src_dir, entries, 8192) if entry_count == 0 return false end if mut i = 0 while i < entry_count let entry = entries[i] let etype = mtree.entry_type(entry) let epath = mtree.entry_path(entry) mut rel_path = epath if str.starts_with(epath, "./") rel_path = str.substring(epath, 2, str.length(epath) - 2) end if let dest_path = path.join_path(root, rel_path) let src_path = path.join_path(src_dir, rel_path) let mode = mtree.entry_mode(entry) let uid = mtree.name_to_uid(mtree.entry_uname(entry)) let gid = mtree.name_to_gid(mtree.entry_gname(entry)) if str.equals(etype, "dir") if not dir.dir_exists(dest_path) if not res.is_ok(dir.create_dir_all(dest_path)) return false end if end if perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) elif str.equals(etype, "file") if is_config_file(rel_path, config_files, config_count) let manifest_sha = mtree.entry_sha256(entry) // Look up old sha256 from previous version's manifest let stored_sha = find_manifest_sha256(rel_path, old_entries, old_entry_count) install_config_file(src_path, dest_path, manifest_sha, stored_sha) else let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if if stat.is_symlink(dest_path) ops.remove_file(dest_path) elif stat.exists(dest_path) ops.remove_file(dest_path) end if if not res.is_ok(ops.copy_file_preserve(src_path, dest_path)) return false end if perm.chmod(dest_path, mode) perm.chown(dest_path, uid, gid) let manifest_sha = mtree.entry_sha256(entry) if str.length(manifest_sha) > 0 let installed_sha = checksum.sha256_file(dest_path) if not str.equals(installed_sha, manifest_sha) return false end if end if end if elif str.equals(etype, "link") let link_target = mtree.entry_link(entry) if stat.is_symlink(dest_path) or stat.exists(dest_path) ops.remove_file(dest_path) end if let parent = path.dirname(dest_path) if str.length(parent) > 0 and not dir.dir_exists(parent) dir.create_dir_all(parent) end if if not res.is_ok(link.symlink(link_target, dest_path)) return false end if end if i = i + 1 end while return true end install_files_upgrade // Parse a TOML inline array string like ["foo", "bar"] into a string array fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if if value[0] != '[' if len > 0 result[0] = value return 1 end if return 0 end if mut pos = 1 mut count = 0 while pos < len and count < max_items while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while if pos >= len or value[pos] == ']' break end if if value[pos] == '"' pos = pos + 1 mut item_start = pos while pos < len and value[pos] != '"' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if pos = pos + 1 elif value[pos] != ']' and value[pos] != ',' mut item_start = pos while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' ' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if end if while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while end while return count end parse_toml_array end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: pkgdb.reef Authors: Chris Tusa License: Description: MsgPack-based package database for fast lookups The package database provides fast indexed access to: - Installed packages (packages.db) - File ownership (files.db) Source of truth remains in /var/lib/coral/installed//pkg.toml These indexes can be rebuilt with 'coral db rebuild' Index files are stored as Base64-encoded MsgPack data for compatibility with Reef's string-based file I/O. ******************************************************************************/ module core.pkgdb import types import io.file import io.dir import io.path import core.str import core.database import encoding.msgpack import encoding.base64 import core.result as res export // Package index entry (for packages.db) type PkgIndexEntry // Initialize/ensure db directory exists fn init_db(): bool // Check if indexes exist and are valid fn indexes_exist(): bool // Get package count from index (fast) fn get_package_count(): int // List all packages from index (fast) fn list_packages(names: [string], versions: [string], max_count: int): int // Check if package is in index fn is_indexed(name: string): bool // Get package info from index fn get_indexed_package(name: string): PkgIndexEntry // Find file owner from index (fast) fn find_file_owner(file_path: string): string // Update index when package is installed fn index_add_package(name: string, version: string, release: int, description: string): bool // Update index when package is removed fn index_remove_package(name: string): bool // Add files to file index fn index_add_files(pkg_name: string, files: [string], file_count: int): bool // Remove files from file index fn index_remove_files(pkg_name: string): bool // Rebuild all indexes from installed packages (source of truth) fn rebuild_indexes(): bool // Get index statistics fn get_index_stats(): IndexStats // Root-aware versions for alternate root fn init_db_rooted(root: string): bool fn indexes_exist_rooted(root: string): bool fn rebuild_indexes_rooted(root: string): bool fn get_index_stats_rooted(root: string): IndexStats fn find_file_owner_rooted(file_path: string, root: string): string // Debug: dump files index contents fn dump_files_index_rooted(root: string, paths: [string], owners: [string], max_count: int): int type IndexStats end export // ============================================================================ // Types // ============================================================================ type PkgIndexEntry = struct name: string version: string release: int description: string installed_date: string file_count: int found: bool end PkgIndexEntry type IndexStats = struct package_count: int file_count: int packages_db_size: int files_db_size: int last_updated: string end IndexStats // Loaded packages index (deserialized from packages.db) type PackagesIndex = struct version: int generated: string package_count: int entries: [PkgIndexEntry] loaded: bool end PackagesIndex // Loaded files index (deserialized from files.db) // Uses parallel arrays for binary search capability type FilesIndex = struct version: int generated: string file_count: int file_paths: [string] // Sorted alphabetically for binary search owners: [string] // Package name at same index as file_paths loaded: bool end FilesIndex // ============================================================================ // Constants // ============================================================================ const INDEX_VERSION: int = 1 const MAX_PACKAGES: int = 2048 const MAX_FILES: int = 131072 // 128K files max // ============================================================================ // Path Constants // ============================================================================ fn db_dir(): string return types.get_index_dir() end db_dir fn packages_db_path(): string return types.get_index_dir() + "/packages.db" end packages_db_path fn files_db_path(): string return types.get_index_dir() + "/files.db" end files_db_path // ============================================================================ // Buffer Size Estimation Helpers // ============================================================================ // Estimate buffer size needed for packages.db fn estimate_packages_buffer_size(pkg_count: int): int // Header overhead: ~100 bytes // Per package: ~200 bytes (name, version, description, dates) return 100 + (pkg_count * 200) end estimate_packages_buffer_size // Estimate buffer size needed for files.db fn estimate_files_buffer_size(file_count: int): int // Header overhead: ~100 bytes // Per file: ~100 bytes (path + package name) return 100 + (file_count * 100) end estimate_files_buffer_size // Get current timestamp as string fn get_timestamp(): string // Simple timestamp - just use a placeholder for now // In production would use sys.time module return "2026-01-29" end get_timestamp // ============================================================================ // Empty/Default Constructors // ============================================================================ fn empty_packages_index(): PackagesIndex return PackagesIndex{ version: 0, generated: "", package_count: 0, entries: new [PkgIndexEntry](MAX_PACKAGES), loaded: false } end empty_packages_index fn empty_files_index(): FilesIndex return FilesIndex{ version: 0, generated: "", file_count: 0, file_paths: new [string](MAX_FILES), owners: new [string](MAX_FILES), loaded: false } end empty_files_index fn empty_pkg_entry(): PkgIndexEntry return PkgIndexEntry{ name: "", version: "", release: 0, description: "", installed_date: "", file_count: 0, found: false } end empty_pkg_entry // Get just the header size for a map (not the entire map contents) fn map_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 // Fixmap (0x80-0x8F): 1 byte header if (b & 0xF0) == 0x80 return 1 // map16 (0xDE): 3 byte header elif b == 0xDE return 3 // map32 (0xDF): 5 byte header elif b == 0xDF return 5 end if return 1 // Default end map_header_size // Get just the header size for an array (not the entire array contents) fn array_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 // Fixarray (0x90-0x9F): 1 byte header if (b & 0xF0) == 0x90 return 1 // array16 (0xDC): 3 byte header elif b == 0xDC return 3 // array32 (0xDD): 5 byte header elif b == 0xDD return 5 end if return 1 // Default end array_header_size // ============================================================================ // MsgPack Deserialization // ============================================================================ // Load packages index from disk fn load_packages_index_rooted(root: string): PackagesIndex mut index = empty_packages_index() // Check if file exists let db_path = packages_db_path_rooted(root) if not file.fileExists(db_path) return index end if // Read base64-encoded content let encoded = res.unwrap_or(file.readFile(db_path), "") if str.length(encoded) == 0 return index end if // Decode base64 to buffer let buf_size = str.length(encoded) // Decoded will be smaller mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len == 0 return index end if // Parse MsgPack structure mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) // Iterate through map entries mut entry_idx = 0 while entry_idx < map_len and offset < buf_len // Get key let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "package_count") index.package_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "packages") // Parse packages array let arr_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut pkg_idx = 0 while pkg_idx < arr_len and pkg_idx < MAX_PACKAGES and offset < buf_len // Each package is a map let pkg_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry = empty_pkg_entry() entry.found = true mut pkg_field = 0 while pkg_field < pkg_map_len and offset < buf_len let field_key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(field_key, "name") entry.name = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "version") entry.version = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "release") entry.release = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) elif str.equals(field_key, "description") entry.description = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "installed_date") entry.installed_date = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") elif str.equals(field_key, "file_count") entry.file_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) end if offset = offset + msgpack.msgpack_value_size(buf, offset) pkg_field = pkg_field + 1 end while index.entries[pkg_idx] = entry pkg_idx = pkg_idx + 1 end while else // Skip unknown key's value offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_packages_index_rooted // Load files index from disk fn load_files_index_rooted(root: string): FilesIndex mut index = empty_files_index() // Check if file exists let db_path = files_db_path_rooted(root) if not file.fileExists(db_path) return index end if // Read base64-encoded content let encoded = res.unwrap_or(file.readFile(db_path), "") let encoded_len = str.length(encoded) if encoded_len == 0 return index end if // Decode base64 to buffer let buf_size = encoded_len // Decoded will be smaller mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len <= 0 return index end if // Parse MsgPack structure mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) // Iterate through map entries mut entry_idx = 0 while entry_idx < map_len and offset < buf_len // Get key let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "file_count") index.file_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "files") // Parse files map let files_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut file_idx = 0 while file_idx < files_map_len and file_idx < MAX_FILES and offset < buf_len // Key is file path, value is owner let file_path = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) let owner = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) index.file_paths[file_idx] = file_path index.owners[file_idx] = owner file_idx = file_idx + 1 end while index.file_count = file_idx else // Skip unknown key's value offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_files_index_rooted // Binary search for file owner in sorted files index // Returns package name or empty string if not found fn binary_search_file_owner(index: FilesIndex, path: string): string if not index.loaded or index.file_count == 0 return "" end if mut low = 0 mut high = index.file_count - 1 while low <= high let mid = (low + high) / 2 let cmp = str.compare(path, index.file_paths[mid]) if cmp == 0 return index.owners[mid] elif cmp < 0 high = mid - 1 else low = mid + 1 end if end while return "" // Not found end binary_search_file_owner // Normalize file path for lookup (strip leading /) fn normalize_path(path: string): string let len = str.length(path) if len > 0 and path[0] == '/' return str.substring(path, 1, len - 1) end if return path end normalize_path // Linear search fallback for file owner (when binary search fails) fn linear_search_file_owner(index: FilesIndex, path: string): string if not index.loaded or index.file_count == 0 return "" end if mut i = 0 while i < index.file_count if str.equals(path, index.file_paths[i]) return index.owners[i] end if i = i + 1 end while return "" end linear_search_file_owner // ============================================================================ // Initialization // ============================================================================ fn init_db(): bool let db = db_dir() if not dir.dir_exists(db) return res.is_ok(dir.create_dir_all(db)) end if return true end init_db fn indexes_exist(): bool return file.fileExists(packages_db_path()) and file.fileExists(files_db_path()) end indexes_exist // ============================================================================ // Package Index Operations - Uses MsgPack index with fallback // ============================================================================ fn get_package_count(): int // Try index first let index = load_packages_index_rooted("") if index.loaded return index.package_count end if // Fall back to database scan mut names: [string] = new [string](1024) return database.list_installed(names, 1024) end get_package_count fn list_packages(names: [string], versions: [string], max_count: int): int // Try index first let index = load_packages_index_rooted("") if index.loaded mut count = 0 while count < index.package_count and count < max_count names[count] = index.entries[count].name versions[count] = index.entries[count].version count = count + 1 end while return count end if // Fall back to database scan let count = database.list_installed(names, max_count) // Fill in versions mut i = 0 while i < count let pkg = database.get_installed(names[i]) versions[i] = pkg.info.version i = i + 1 end while return count end list_packages fn is_indexed(name: string): bool // Try index first let index = load_packages_index_rooted("") if index.loaded mut i = 0 while i < index.package_count if str.equals(index.entries[i].name, name) return true end if i = i + 1 end while return false end if // Fall back to database check return database.is_installed(name) end is_indexed fn get_indexed_package(name: string): PkgIndexEntry // Try index first let index = load_packages_index_rooted("") if index.loaded mut i = 0 while i < index.package_count if str.equals(index.entries[i].name, name) return index.entries[i] end if i = i + 1 end while return empty_pkg_entry() end if // Fall back to database mut entry = empty_pkg_entry() if not database.is_installed(name) return entry end if let pkg = database.get_installed(name) entry.name = pkg.info.name entry.version = pkg.info.version entry.release = pkg.info.release entry.description = pkg.info.description entry.installed_date = pkg.install_date entry.file_count = pkg.files_count entry.found = true return entry end get_indexed_package // ============================================================================ // File Index Operations - Uses MsgPack index with fallback // ============================================================================ fn find_file_owner(file_path: string): string return find_file_owner_rooted(file_path, "") end find_file_owner fn find_file_owner_rooted(file_path: string, root: string): string // Try index first (O(log N) binary search) let index = load_files_index_rooted(root) if index.loaded let normalized = normalize_path(file_path) let owner = binary_search_file_owner(index, normalized) if str.length(owner) > 0 return owner end if // Binary search failed - try linear search as fallback // This helps diagnose if the index isn't sorted correctly let linear_owner = linear_search_file_owner(index, normalized) if str.length(linear_owner) > 0 return linear_owner end if // File not in index - either not owned or index out of date return "" end if // Fall back to database scan (O(N*M)) if str.length(root) > 0 return database.find_owner_rooted(file_path, root) end if return database.find_owner(file_path) end find_file_owner_rooted // ============================================================================ // Index Modification (stub - no-op for now) // ============================================================================ fn index_add_package(name: string, version: string, release: int, description: string): bool // Stub - no indexing yet return true end index_add_package fn index_remove_package(name: string): bool // Stub - no indexing yet return true end index_remove_package fn index_add_files(pkg_name: string, files: [string], file_count: int): bool // Stub - no indexing yet return true end index_add_files fn index_remove_files(pkg_name: string): bool // Stub - triggers full rebuild return true end index_remove_files // ============================================================================ // MsgPack Serialization // ============================================================================ // Serialize packages index to MsgPack format // Returns buffer and sets length via out parameter simulation (length stored at buf[0]) fn serialize_packages_db_rooted(root: string, buf: [int], max_size: int): int // Get list of installed packages mut names: [string] = new [string](MAX_PACKAGES) let pkg_count = database.list_installed_rooted(names, MAX_PACKAGES, root) mut pos = 0 // Pack map header (4 keys: version, generated, package_count, packages) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // Pack "version" -> INDEX_VERSION pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(INDEX_VERSION, buf, pos) // Pack "generated" -> timestamp pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // Pack "package_count" -> count pos = pos + msgpack.msgpack_pack_string("package_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(pkg_count, buf, pos) // Pack "packages" -> array of package entries pos = pos + msgpack.msgpack_pack_string("packages", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(pkg_count, buf, pos) // Pack each package entry mut i = 0 while i < pkg_count and pos < max_size - 1000 let pkg = database.get_installed_rooted(names[i], root) // Each package is a map with 6 keys pos = pos + msgpack.msgpack_pack_map_header(6, buf, pos) // name pos = pos + msgpack.msgpack_pack_string("name", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.info.name, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.info.version, buf, pos) // release pos = pos + msgpack.msgpack_pack_string("release", buf, pos) pos = pos + msgpack.msgpack_pack_int(pkg.info.release, buf, pos) // description pos = pos + msgpack.msgpack_pack_string("description", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.info.description, buf, pos) // installed_date pos = pos + msgpack.msgpack_pack_string("installed_date", buf, pos) pos = pos + msgpack.msgpack_pack_string(pkg.install_date, buf, pos) // file_count pos = pos + msgpack.msgpack_pack_string("file_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(pkg.files_count, buf, pos) i = i + 1 end while return pos end serialize_packages_db_rooted // Count total installed files across all packages (cheap — no file content read) fn count_total_files_rooted(root: string): int mut pkg_names: [string] = new [string](MAX_PACKAGES) let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) mut total = 0 mut i = 0 while i < pkg_count mut pkg_files: [string] = new [string](8192) let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) total = total + file_count i = i + 1 end while return total end count_total_files_rooted // Serialize files index to MsgPack format // Creates a map of file_path -> package_name for fast ownership lookup fn serialize_files_db_rooted(root: string, buf: [int], max_size: int, alloc_size: int): int // Get all installed packages mut pkg_names: [string] = new [string](MAX_PACKAGES) let pkg_count = database.list_installed_rooted(pkg_names, MAX_PACKAGES, root) // Collect all files with their owners — sized to actual count + margin mut all_files: [string] = new [string](alloc_size) mut all_owners: [string] = new [string](alloc_size) mut total_files = 0 mut i = 0 while i < pkg_count and total_files < alloc_size - 256 mut pkg_files: [string] = new [string](8192) let file_count = database.get_files_rooted(pkg_names[i], pkg_files, 8192, root) mut j = 0 while j < file_count and total_files < alloc_size all_files[total_files] = pkg_files[j] all_owners[total_files] = pkg_names[i] total_files = total_files + 1 j = j + 1 end while i = i + 1 end while // Sort files alphabetically for binary search (simple bubble sort for now) sort_file_index(all_files, all_owners, total_files) mut pos = 0 // Pack map header (4 keys: version, generated, file_count, files) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // Pack "version" -> INDEX_VERSION pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(INDEX_VERSION, buf, pos) // Pack "generated" -> timestamp pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // Pack "file_count" -> count pos = pos + msgpack.msgpack_pack_string("file_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(total_files, buf, pos) // Pack "files" -> map of path -> owner pos = pos + msgpack.msgpack_pack_string("files", buf, pos) pos = pos + msgpack.msgpack_pack_map_header(total_files, buf, pos) // Pack each file -> owner pair i = 0 while i < total_files and pos < max_size - 500 pos = pos + msgpack.msgpack_pack_string(all_files[i], buf, pos) pos = pos + msgpack.msgpack_pack_string(all_owners[i], buf, pos) i = i + 1 end while return pos end serialize_files_db_rooted // Sort parallel arrays of files and owners alphabetically by file path // Uses merge sort for O(n log n) performance on large file counts proc sort_file_index(files: [string], owners: [string], count: int) if count <= 1 return end if // Allocate temporary arrays for merge operations mut tmp_files: [string] = new [string](count) mut tmp_owners: [string] = new [string](count) merge_sort(files, owners, tmp_files, tmp_owners, 0, count - 1) end sort_file_index proc merge_sort(files: [string], owners: [string], tmp_files: [string], tmp_owners: [string], left: int, right: int) if left >= right return end if let mid = left + (right - left) / 2 merge_sort(files, owners, tmp_files, tmp_owners, left, mid) merge_sort(files, owners, tmp_files, tmp_owners, mid + 1, right) merge(files, owners, tmp_files, tmp_owners, left, mid, right) end merge_sort proc merge(files: [string], owners: [string], tmp_files: [string], tmp_owners: [string], left: int, mid: int, right: int) // Copy both halves into temp arrays mut i = left while i <= right tmp_files[i] = files[i] tmp_owners[i] = owners[i] i = i + 1 end while // Merge back from temp into original mut l = left mut r = mid + 1 mut k = left while l <= mid and r <= right if str.compare(tmp_files[l], tmp_files[r]) <= 0 files[k] = tmp_files[l] owners[k] = tmp_owners[l] l = l + 1 else files[k] = tmp_files[r] owners[k] = tmp_owners[r] r = r + 1 end if k = k + 1 end while // Copy remaining left half while l <= mid files[k] = tmp_files[l] owners[k] = tmp_owners[l] l = l + 1 k = k + 1 end while // Right half remaining elements are already in place end merge // ============================================================================ // Index Rebuild // ============================================================================ fn rebuild_indexes(): bool return rebuild_indexes_rooted("") end rebuild_indexes // ============================================================================ // Index Statistics // ============================================================================ fn get_index_stats(): IndexStats mut stats = IndexStats{ package_count: 0, file_count: 0, packages_db_size: 0, files_db_size: 0, last_updated: "" } // Get package count from source of truth mut names: [string] = new [string](1024) stats.package_count = database.list_installed(names, 1024) // Count files across all packages mut i = 0 while i < stats.package_count mut files: [string] = new [string](4096) let file_count = database.get_files(names[i], files, 4096) stats.file_count = stats.file_count + file_count i = i + 1 end while return stats end get_index_stats // ============================================================================ // Root-aware versions for alternate root // ============================================================================ fn db_dir_rooted(root: string): string if str.length(root) == 0 return types.get_index_dir() end if return root + types.get_index_dir() end db_dir_rooted fn packages_db_path_rooted(root: string): string return db_dir_rooted(root) + "/packages.db" end packages_db_path_rooted fn files_db_path_rooted(root: string): string return db_dir_rooted(root) + "/files.db" end files_db_path_rooted fn init_db_rooted(root: string): bool let db = db_dir_rooted(root) if not dir.dir_exists(db) return res.is_ok(dir.create_dir_all(db)) end if return true end init_db_rooted fn indexes_exist_rooted(root: string): bool return file.fileExists(packages_db_path_rooted(root)) and file.fileExists(files_db_path_rooted(root)) end indexes_exist_rooted fn rebuild_indexes_rooted(root: string): bool if not init_db_rooted(root) return false end if // Serialize packages.db let pkg_buf_size = estimate_packages_buffer_size(MAX_PACKAGES) mut pkg_buf: [int] = new [int](pkg_buf_size) let pkg_len = serialize_packages_db_rooted(root, pkg_buf, pkg_buf_size) if pkg_len > 0 // Encode to base64 and write let pkg_encoded = base64.base64_encode_bytes(pkg_buf, pkg_len) if not res.is_ok(file.writeFile(packages_db_path_rooted(root), pkg_encoded)) return false end if end if // Serialize files.db — count actual files first to avoid massive over-allocation let actual_file_count = count_total_files_rooted(root) let alloc_size = actual_file_count + 1024 let files_buf_size = estimate_files_buffer_size(alloc_size) mut files_buf: [int] = new [int](files_buf_size) let files_len = serialize_files_db_rooted(root, files_buf, files_buf_size, alloc_size) if files_len > 0 // Encode to base64 and write let files_encoded = base64.base64_encode_bytes(files_buf, files_len) if not res.is_ok(file.writeFile(files_db_path_rooted(root), files_encoded)) return false end if end if return true end rebuild_indexes_rooted fn get_index_stats_rooted(root: string): IndexStats mut stats = IndexStats{ package_count: 0, file_count: 0, packages_db_size: 0, files_db_size: 0, last_updated: "" } // Get package count from source of truth in alternate root mut names: [string] = new [string](1024) stats.package_count = database.list_installed_rooted(names, 1024, root) // Count files across all packages mut i = 0 while i < stats.package_count mut files: [string] = new [string](4096) let file_count = database.get_files_rooted(names[i], files, 4096, root) stats.file_count = stats.file_count + file_count i = i + 1 end while return stats end get_index_stats_rooted // Debug: dump files index contents to arrays fn dump_files_index_rooted(root: string, paths: [string], owners: [string], max_count: int): int let index = load_files_index_rooted(root) if not index.loaded return 0 end if mut count = 0 while count < index.file_count and count < max_count paths[count] = index.file_paths[count] owners[count] = index.owners[count] count = count + 1 end while return count end dump_files_index_rooted end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: port.reef Authors: Chris Tusa License: Description: Port loading and parsing from package.toml ******************************************************************************/ module core.port import types import io.file import io.dir import io.path import core.str import core.config import encoding.toml import core.result as res export // Load a port from a directory path, returns PortResult fn load(port_path: string): types.PortResult // Find a port by name, searches all categories fn find(name: string): types.PortResult // List all ports in a category fn list_category(category: string, names: [string], max_count: int): int // Get all categories fn get_categories(cats: [string], max_count: int): int end export // Dynamically discover categories by scanning the ports directory // Excludes hidden dirs, infrastructure dirs (pkgs/, groups/), and config-aware paths fn get_categories(cats: [string], max_count: int): int let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) if not dir.dir_exists(ports_dir) return 0 end if let packages_path = config.get_packages_dir(cfg) let sources_path = config.get_sources_dir(cfg) let build_path = config.get_build_dir(cfg) // Extract first path component relative to ports_dir for each configured path let skip1 = get_relative_top_dir(ports_dir, packages_path) let skip2 = get_relative_top_dir(ports_dir, sources_path) let skip3 = get_relative_top_dir(ports_dir, build_path) // Also skip the groups directory let groups_dir = types.get_groups_dir() let skip_groups = get_relative_top_dir(ports_dir, groups_dir) let entries = res.unwrap_or(dir.list_dir(ports_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] let elen = str.length(entry) // Skip hidden directories if elen == 0 or entry[0] == '.' i = i + 1 continue end if // Skip infrastructure directories if str.equals(entry, skip1) or str.equals(entry, skip2) or str.equals(entry, skip3) or str.equals(entry, skip_groups) i = i + 1 continue end if // Verify it's actually a directory let entry_path = path.join_path(ports_dir, entry) if dir.dir_exists(entry_path) cats[count] = entry count = count + 1 end if i = i + 1 end while return count end get_categories // Extract the first path component of `child` relative to `parent` // e.g. parent="/usr/zports", child="/usr/zports/pkgs/packages" -> "pkgs" // Returns empty string if child is not under parent fn get_relative_top_dir(parent: string, child: string): string let parent_len = str.length(parent) let child_len = str.length(child) if child_len <= parent_len return "" end if // Check that child starts with parent let prefix = str.substring(child, 0, parent_len) if not str.equals(prefix, parent) return "" end if // Skip the separator mut start = parent_len if child[start] == '/' start = start + 1 end if // Find the next slash or end of string mut end_pos = start while end_pos < child_len if child[end_pos] == '/' break end if end_pos = end_pos + 1 end while if end_pos > start return str.substring(child, start, end_pos - start) end if return "" end get_relative_top_dir // Parse a TOML array value like ["item1", "item2"] into a string array // Returns the number of items parsed fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if // Check for array brackets if value[0] != '[' // Single value, not an array if len > 0 result[0] = value return 1 end if return 0 end if // Skip the opening bracket mut pos = 1 mut count = 0 while pos < len and count < max_items // Skip whitespace while pos < len and (value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while if pos >= len or value[pos] == ']' break end if // Check for quoted string if value[pos] == '"' pos = pos + 1 mut item_start = pos // Find closing quote while pos < len and value[pos] != '"' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if pos = pos + 1 // Skip closing quote elif value[pos] != ']' and value[pos] != ',' // Unquoted value mut item_start = pos while pos < len and value[pos] != ',' and value[pos] != ']' and value[pos] != ' ' pos = pos + 1 end while if pos > item_start result[count] = str.substring(value, item_start, pos - item_start) count = count + 1 end if end if // Skip comma and whitespace while pos < len and (value[pos] == ',' or value[pos] == ' ' or value[pos] == '\t' or value[pos] == '\n') pos = pos + 1 end while end while return count end parse_toml_array // Create an empty/default Port structure (delegate to types module) fn new_port(): types.Port return types.new_port() end new_port // Helper to create error result fn error_result(msg: string): types.PortResult return types.new_port_result(false, new_port(), msg) end error_result // Helper to create success result fn success_result(p: types.Port): types.PortResult return types.new_port_result(true, p, "") end success_result // Extract filename from URL fn extract_filename_from_url(url: string): string let len = str.length(url) if len == 0 return "" end if // Find last slash mut last_slash = -1 mut i = len - 1 while i >= 0 if url[i] == '/' last_slash = i break end if i = i - 1 end while if last_slash < 0 return url end if let filename = str.substring(url, last_slash + 1, len - last_slash - 1) // Remove query string if present let query_len = str.length(filename) mut query_pos = -1 i = 0 while i < query_len if filename[i] == '?' query_pos = i break end if i = i + 1 end while if query_pos > 0 return str.substring(filename, 0, query_pos) end if return filename end extract_filename_from_url // Parse sources from TOML - supports both new [[source]] and legacy [sources] format fn parse_sources(keys: [string], vals: [string], entry_count: int): types.SourceInfo mut info = types.new_source_info() // Try new [[source]] format first (source.0.file, source.1.file, etc.) mut source_idx = 0 mut found_new_format = false while source_idx < 16 let prefix = "source." + int_to_str(source_idx) let file_key = prefix + ".file" if not toml.toml_has_key(keys, vals, entry_count, file_key) break end if found_new_format = true mut src = types.new_source() // Get file name src.file = toml.toml_get(keys, vals, entry_count, file_key) // Get URLs array let urls_key = prefix + ".urls" if toml.toml_has_key(keys, vals, entry_count, urls_key) let urls_val = toml.toml_get(keys, vals, entry_count, urls_key) mut urls_arr: [string] = new [string](16) let urls_count = parse_toml_array(urls_val, urls_arr, 16) src.urls = urls_arr src.url_count = urls_count end if // Get checksum (single value) let checksum_key = prefix + ".checksum" if toml.toml_has_key(keys, vals, entry_count, checksum_key) src.checksum = toml.toml_get(keys, vals, entry_count, checksum_key) end if // Get extract flag (default: true) let extract_key = prefix + ".extract" if toml.toml_has_key(keys, vals, entry_count, extract_key) src.extract = toml.toml_get_bool(keys, vals, entry_count, extract_key) end if info.sources[source_idx] = src source_idx = source_idx + 1 end while if found_new_format info.count = source_idx return info end if // Fall back to legacy [sources] format if toml.toml_has_key(keys, vals, entry_count, "sources.urls") let urls_val = toml.toml_get(keys, vals, entry_count, "sources.urls") mut urls_arr: [string] = new [string](16) let urls_count = parse_toml_array(urls_val, urls_arr, 16) // Parse checksums mut checksums_arr: [string] = new [string](16) if toml.toml_has_key(keys, vals, entry_count, "sources.checksums") let cksum_val = toml.toml_get(keys, vals, entry_count, "sources.checksums") parse_toml_array(cksum_val, checksums_arr, 16) end if // Convert legacy format to new Source structs mut i = 0 while i < urls_count mut src = types.new_source() src.file = extract_filename_from_url(urls_arr[i]) src.urls[0] = urls_arr[i] src.url_count = 1 if str.length(checksums_arr[i]) > 0 src.checksum = checksums_arr[i] end if src.extract = true info.sources[i] = src i = i + 1 end while info.count = urls_count end if return info end parse_sources // Helper: convert int to string (for source index keys) fn int_to_str(n: int): string if n == 0 return "0" end if mut result = "" mut value = n while value > 0 let digit = value % 10 result = str.concat(str.substring("0123456789", digit, 1), result) value = value / 10 end while return result end int_to_str // Load a port from a directory containing package.toml fn load(port_path: string): types.PortResult // Check if directory exists if not dir.dir_exists(port_path) return error_result("Port directory does not exist: " + port_path) end if // Check for package.toml let toml_path = path.join_path(port_path, "package.toml") if not file.fileExists(toml_path) return error_result("package.toml not found in: " + port_path) end if // Read the TOML file let content = res.unwrap_or(file.readFile(toml_path), "") if str.length(content) == 0 return error_result("Failed to read package.toml or file is empty") end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return error_result("Failed to parse package.toml") end if // Build the Port structure mut port = new_port() port.port_dir = port_path // Extract category from path (e.g., /usr/zports/base/vim -> base) let parent_dir = path.dirname(port_path) port.category = path.basename(parent_dir) // Parse [package] section port.info.name = toml.toml_get(keys, vals, entry_count, "package.name") port.info.version = toml.toml_get(keys, vals, entry_count, "package.version") port.info.release = toml.toml_get_int(keys, vals, entry_count, "package.release") port.info.description = toml.toml_get(keys, vals, entry_count, "package.description") port.info.url = toml.toml_get(keys, vals, entry_count, "package.url") port.info.license = toml.toml_get(keys, vals, entry_count, "package.license") port.info.maintainer = toml.toml_get(keys, vals, entry_count, "package.maintainer") // Architecture (default to x86_64 if not specified) let arch_val = toml.toml_get(keys, vals, entry_count, "package.arch") if str.length(arch_val) > 0 port.info.arch = arch_val end if // Parse alternatives array if toml.toml_has_key(keys, vals, entry_count, "package.alternatives") let alt_val = toml.toml_get(keys, vals, entry_count, "package.alternatives") mut alt_arr: [string] = new [string](32) let alt_count = parse_toml_array(alt_val, alt_arr, 32) port.info.alternatives = alt_arr port.info.alternatives_count = alt_count end if // Parse conflicts array if toml.toml_has_key(keys, vals, entry_count, "package.conflicts") let conf_val = toml.toml_get(keys, vals, entry_count, "package.conflicts") mut conf_arr: [string] = new [string](32) let conf_count = parse_toml_array(conf_val, conf_arr, 32) port.info.conflicts = conf_arr port.info.conflicts_count = conf_count end if // Parse [dependencies] section if toml.toml_has_key(keys, vals, entry_count, "dependencies.runtime") let rt_val = toml.toml_get(keys, vals, entry_count, "dependencies.runtime") mut rt_arr: [string] = new [string](64) let rt_count = parse_toml_array(rt_val, rt_arr, 64) port.deps.runtime = rt_arr port.deps.runtime_count = rt_count end if if toml.toml_has_key(keys, vals, entry_count, "dependencies.build") let bd_val = toml.toml_get(keys, vals, entry_count, "dependencies.build") mut bd_arr: [string] = new [string](64) let bd_count = parse_toml_array(bd_val, bd_arr, 64) port.deps.build = bd_arr port.deps.build_count = bd_count end if // Parse sources - try new [[source]] format first, fall back to legacy [sources] port.sources = parse_sources(keys, vals, entry_count) // Parse [patches] section if toml.toml_has_key(keys, vals, entry_count, "patches.files") let patch_val = toml.toml_get(keys, vals, entry_count, "patches.files") mut patch_arr: [string] = new [string](32) let patch_count = parse_toml_array(patch_val, patch_arr, 32) port.patches.files = patch_arr port.patches.count = patch_count end if if toml.toml_has_key(keys, vals, entry_count, "patches.strip") port.patches.strip = toml.toml_get_int(keys, vals, entry_count, "patches.strip") end if // Parse [config] section if toml.toml_has_key(keys, vals, entry_count, "config.files") let cfg_val = toml.toml_get(keys, vals, entry_count, "config.files") mut cfg_arr: [string] = new [string](32) let cfg_count = parse_toml_array(cfg_val, cfg_arr, 32) port.config.files = cfg_arr port.config.count = cfg_count end if // Parse [scripts] section if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_install") port.scripts.pre_install = toml.toml_get(keys, vals, entry_count, "scripts.pre_install") end if if toml.toml_has_key(keys, vals, entry_count, "scripts.post_install") port.scripts.post_install = toml.toml_get(keys, vals, entry_count, "scripts.post_install") end if if toml.toml_has_key(keys, vals, entry_count, "scripts.pre_remove") port.scripts.pre_remove = toml.toml_get(keys, vals, entry_count, "scripts.pre_remove") end if if toml.toml_has_key(keys, vals, entry_count, "scripts.post_remove") port.scripts.post_remove = toml.toml_get(keys, vals, entry_count, "scripts.post_remove") end if // Parse [paths] section (per-package overrides) if toml.toml_has_key(keys, vals, entry_count, "paths.prefix") port.prefix = toml.toml_get(keys, vals, entry_count, "paths.prefix") end if if toml.toml_has_key(keys, vals, entry_count, "paths.sysconfdir") port.sysconfdir = toml.toml_get(keys, vals, entry_count, "paths.sysconfdir") end if // Parse [build] section if toml.toml_has_key(keys, vals, entry_count, "build.parallel") port.build_cfg.parallel = toml.toml_get_bool(keys, vals, entry_count, "build.parallel") end if if toml.toml_has_key(keys, vals, entry_count, "build.jobs") port.build_cfg.jobs = toml.toml_get_int(keys, vals, entry_count, "build.jobs") end if // Validate required fields if str.length(port.info.name) == 0 return types.new_port_result(false, port, "package.name is required") end if if str.length(port.info.version) == 0 return types.new_port_result(false, port, "package.version is required") end if return success_result(port) end load // Find a port by name, searching all categories // Accepts both "kernel" (searches all categories) and "hammerhead/kernel" (direct category/name) fn find(name: string): types.PortResult let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) // Check if ports directory exists if not dir.dir_exists(ports_dir) return error_result("Ports directory not found: " + ports_dir + " (check /etc/coral/coral.conf)") end if // Check if name contains a slash (category/name format) let slash_idx = str.index_of(name, "/") if slash_idx >= 0 let port_path = path.join_path(ports_dir, name) if dir.dir_exists(port_path) return load(port_path) end if return error_result("Port not found: " + name + " (searched " + ports_dir + "/" + name + ")") end if // Search all categories for the name mut cats: [string] = new [string](32) let cat_count = get_categories(cats, 32) mut i = 0 while i < cat_count let port_path = path.join_path(ports_dir, path.join_path(cats[i], name)) if dir.dir_exists(port_path) return load(port_path) end if i = i + 1 end while return error_result("Port not found: " + name + " (searched " + ports_dir + ")") end find // List all ports in a given category fn list_category(category: string, names: [string], max_count: int): int let cfg = config.load() let cat_path = path.join_path(config.get_ports_dir(cfg), category) if not dir.dir_exists(cat_path) return 0 end if let entries = res.unwrap_or(dir.list_dir(cat_path), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Skip hidden files and non-directories if str.length(entry) > 0 and entry[0] != '.' let entry_path = path.join_path(cat_path, entry) // Check if it's a valid port (has package.toml) let toml_path = path.join_path(entry_path, "package.toml") if file.fileExists(toml_path) names[count] = entry count = count + 1 end if end if i = i + 1 end while return count end list_category end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: portindex.reef Authors: Chris Tusa License: Description: Ports index for fast port lookups The ports index provides fast indexed access to port metadata without parsing individual package.toml files from disk. Index file: /var/lib/coral/db/ports.db Format: Base64-encoded MsgPack (same as packages.db, files.db) Rebuild triggers: coral sync, coral db rebuild, coral db init Manual: coral ports cache rebuild ******************************************************************************/ module core.portindex import types import io.file import io.dir import io.path import core.str import core.port import core.config import encoding.msgpack import encoding.base64 import core.result as res export type PortIndexEntry type PortsIndex // Rebuild the ports index from the ports tree fn rebuild_port_index(): bool // Check if ports.db exists fn port_index_exists(): bool // Load the ports index from disk fn load_port_index(): PortsIndex // Find a single port by name or category/name fn find_port(name: string): PortIndexEntry // Search ports by substring match on name + description fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int // List all indexed ports fn list_all_ports(entries: [PortIndexEntry], max_count: int): int // Get count of indexed ports fn get_port_count(): int // Find ports that depend on a given package name fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int end export // ============================================================================ // Types // ============================================================================ type PortIndexEntry = struct name: string category: string version: string release: int description: string port_dir: string runtime_deps: [string] runtime_dep_count: int found: bool end PortIndexEntry type PortsIndex = struct version: int generated: string port_count: int entries: [PortIndexEntry] loaded: bool end PortsIndex // ============================================================================ // Constants // ============================================================================ const PORT_INDEX_VERSION: int = 1 const MAX_PORTS: int = 4096 // ============================================================================ // Path helpers // ============================================================================ fn ports_db_path(): string return types.get_index_dir() + "/ports.db" end ports_db_path // Get current timestamp as string fn get_timestamp(): string return "2026-02-19" end get_timestamp // ============================================================================ // Empty constructors // ============================================================================ fn empty_port_index(): PortsIndex return PortsIndex{ version: 0, generated: "", port_count: 0, entries: new [PortIndexEntry](MAX_PORTS), loaded: false } end empty_port_index fn empty_port_entry(): PortIndexEntry return PortIndexEntry{ name: "", category: "", version: "", release: 0, description: "", port_dir: "", runtime_deps: new [string](64), runtime_dep_count: 0, found: false } end empty_port_entry // ============================================================================ // Buffer size estimation // ============================================================================ fn estimate_buffer_size(port_count: int): int // Header: ~100 bytes // Per port: ~300 bytes (name, category, version, description, port_dir, deps) return 100 + (port_count * 300) end estimate_buffer_size // ============================================================================ // MsgPack header size helpers (same as pkgdb.reef) // ============================================================================ fn map_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 if (b & 0xF0) == 0x80 return 1 elif b == 0xDE return 3 elif b == 0xDF return 5 end if return 1 end map_header_size fn array_header_size(buf: [int], offset: int): int let b = buf[offset] & 255 if (b & 0xF0) == 0x90 return 1 elif b == 0xDC return 3 elif b == 0xDD return 5 end if return 1 end array_header_size // ============================================================================ // Index existence check // ============================================================================ fn port_index_exists(): bool return file.fileExists(ports_db_path()) end port_index_exists // ============================================================================ // Rebuild: scan all ports, serialize to ports.db // ============================================================================ fn rebuild_port_index(): bool // Ensure db directory exists let db_dir = types.get_index_dir() if not dir.dir_exists(db_dir) dir.create_dir_all(db_dir) end if // Get all categories dynamically mut cats: [string] = new [string](32) let cat_count = port.get_categories(cats, 32) // Collect all port entries mut entries: [PortIndexEntry] = new [PortIndexEntry](MAX_PORTS) mut total = 0 mut i = 0 while i < cat_count and total < MAX_PORTS let category = cats[i] mut port_names: [string] = new [string](256) let port_count = port.list_category(category, port_names, 256) mut j = 0 while j < port_count and total < MAX_PORTS let result = port.load(get_port_path(category, port_names[j])) if result.success let p = result.port mut entry = empty_port_entry() entry.name = p.info.name entry.category = p.category entry.version = p.info.version entry.release = p.info.release entry.description = p.info.description entry.port_dir = p.port_dir entry.found = true // Copy runtime deps mut k = 0 while k < p.deps.runtime_count and k < 64 entry.runtime_deps[k] = p.deps.runtime[k] k = k + 1 end while entry.runtime_dep_count = p.deps.runtime_count entries[total] = entry total = total + 1 end if j = j + 1 end while i = i + 1 end while // Serialize to MsgPack let buf_size = estimate_buffer_size(total) mut buf: [int] = new [int](buf_size) let buf_len = serialize_port_index(entries, total, buf, buf_size) if buf_len == 0 return false end if // Encode to base64 and write let encoded = base64.base64_encode_bytes(buf, buf_len) return res.is_ok(file.writeFile(ports_db_path(), encoded)) end rebuild_port_index // Build a port path from category and name fn get_port_path(category: string, name: string): string let cfg = config.load() let ports_dir = config.get_ports_dir(cfg) return path.join_path(ports_dir, path.join_path(category, name)) end get_port_path // ============================================================================ // Serialization // ============================================================================ fn serialize_port_index(entries: [PortIndexEntry], count: int, buf: [int], max_size: int): int mut pos = 0 // Pack map header (4 keys: version, generated, port_count, ports) pos = pos + msgpack.msgpack_pack_map_header(4, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_int(PORT_INDEX_VERSION, buf, pos) // generated pos = pos + msgpack.msgpack_pack_string("generated", buf, pos) pos = pos + msgpack.msgpack_pack_string(get_timestamp(), buf, pos) // port_count pos = pos + msgpack.msgpack_pack_string("port_count", buf, pos) pos = pos + msgpack.msgpack_pack_int(count, buf, pos) // ports array pos = pos + msgpack.msgpack_pack_string("ports", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(count, buf, pos) mut i = 0 while i < count and pos < max_size - 2000 let entry = entries[i] // Each port is a map with 7 keys pos = pos + msgpack.msgpack_pack_map_header(7, buf, pos) // name pos = pos + msgpack.msgpack_pack_string("name", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.name, buf, pos) // category pos = pos + msgpack.msgpack_pack_string("category", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.category, buf, pos) // version pos = pos + msgpack.msgpack_pack_string("version", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.version, buf, pos) // release pos = pos + msgpack.msgpack_pack_string("release", buf, pos) pos = pos + msgpack.msgpack_pack_int(entry.release, buf, pos) // description pos = pos + msgpack.msgpack_pack_string("description", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.description, buf, pos) // port_dir pos = pos + msgpack.msgpack_pack_string("port_dir", buf, pos) pos = pos + msgpack.msgpack_pack_string(entry.port_dir, buf, pos) // runtime_deps (array of strings) pos = pos + msgpack.msgpack_pack_string("runtime_deps", buf, pos) pos = pos + msgpack.msgpack_pack_array_header(entry.runtime_dep_count, buf, pos) mut d = 0 while d < entry.runtime_dep_count pos = pos + msgpack.msgpack_pack_string(entry.runtime_deps[d], buf, pos) d = d + 1 end while i = i + 1 end while return pos end serialize_port_index // ============================================================================ // Deserialization // ============================================================================ fn load_port_index(): PortsIndex mut index = empty_port_index() let db_path = ports_db_path() if not file.fileExists(db_path) return index end if let encoded = res.unwrap_or(file.readFile(db_path), "") if str.length(encoded) == 0 return index end if let buf_size = str.length(encoded) mut buf: [int] = new [int](buf_size) let buf_len = res.unwrap_or(base64.base64_decode_bytes(encoded, buf, buf_size), 0) if buf_len == 0 return index end if mut offset = 0 // Expect map header if not msgpack.msgpack_is_map(buf, offset) return index end if let map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry_idx = 0 while entry_idx < map_len and offset < buf_len let key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(key, "version") index.version = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "generated") index.generated = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "port_count") index.port_count = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(key, "ports") // Parse ports array let arr_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut port_idx = 0 while port_idx < arr_len and port_idx < MAX_PORTS and offset < buf_len let port_map_len = res.unwrap_or(msgpack.msgpack_unpack_map_len(buf, offset), 0) offset = offset + map_header_size(buf, offset) mut entry = empty_port_entry() entry.found = true mut port_field = 0 while port_field < port_map_len and offset < buf_len let field_key = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) if str.equals(field_key, "name") entry.name = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "category") entry.category = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "version") entry.version = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "release") entry.release = res.unwrap_or(msgpack.msgpack_unpack_int(buf, offset), 0) offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "description") entry.description = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "port_dir") entry.port_dir = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) elif str.equals(field_key, "runtime_deps") // Parse deps array let deps_len = res.unwrap_or(msgpack.msgpack_unpack_array_len(buf, offset), 0) offset = offset + array_header_size(buf, offset) mut dep_idx = 0 while dep_idx < deps_len and dep_idx < 64 and offset < buf_len entry.runtime_deps[dep_idx] = res.unwrap_or(msgpack.msgpack_unpack_string(buf, offset), "") offset = offset + msgpack.msgpack_value_size(buf, offset) dep_idx = dep_idx + 1 end while entry.runtime_dep_count = dep_idx else offset = offset + msgpack.msgpack_value_size(buf, offset) end if port_field = port_field + 1 end while index.entries[port_idx] = entry port_idx = port_idx + 1 end while else offset = offset + msgpack.msgpack_value_size(buf, offset) end if entry_idx = entry_idx + 1 end while index.loaded = true return index end load_port_index // ============================================================================ // Query functions // ============================================================================ // Find a single port by name or category/name fn find_port(name: string): PortIndexEntry let index = load_port_index() if not index.loaded return empty_port_entry() end if // Check if name contains a slash (category/name format) let slash_idx = str.index_of(name, "/") mut i = 0 while i < index.port_count let entry = index.entries[i] if slash_idx >= 0 // Match category/name format let full_name = entry.category + "/" + entry.name // Also check directory-based name (category/dirname) let dir_name = path.basename(entry.port_dir) let full_dir_name = entry.category + "/" + dir_name if str.equals(full_name, name) or str.equals(full_dir_name, name) return entry end if else // Match just the name if str.equals(entry.name, name) return entry end if end if i = i + 1 end while return empty_port_entry() end find_port // Search ports by substring match on name + description fn search_ports(term: string, results: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if let lower_term = str.to_lower(term) mut count = 0 mut i = 0 while i < index.port_count and count < max_count let entry = index.entries[i] let lower_name = str.to_lower(entry.name) let lower_desc = str.to_lower(entry.description) if str.contains(lower_name, lower_term) or str.contains(lower_desc, lower_term) results[count] = entry count = count + 1 end if i = i + 1 end while return count end search_ports // List all indexed ports fn list_all_ports(entries: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if mut count = 0 while count < index.port_count and count < max_count entries[count] = index.entries[count] count = count + 1 end while return count end list_all_ports // Get count of indexed ports fn get_port_count(): int let index = load_port_index() if index.loaded return index.port_count end if return 0 end get_port_count // Find ports that have `name` in their runtime dependencies fn find_reverse_deps(name: string, results: [PortIndexEntry], max_count: int): int let index = load_port_index() if not index.loaded return 0 end if mut count = 0 mut i = 0 while i < index.port_count and count < max_count let entry = index.entries[i] mut j = 0 while j < entry.runtime_dep_count if str.equals(entry.runtime_deps[j], name) results[count] = entry count = count + 1 break end if j = j + 1 end while i = i + 1 end while return count end find_reverse_deps end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: repository.reef Authors: Chris Tusa License: Description: Repository manifest handling for binary package distribution ******************************************************************************/ module core.repository import core.str import io.file import io.dir import io.path import encoding.toml import util.http as uhttp import util.color import types import core.result as res export type Repository type RepoResult type RemotePackage type RemotePackageResult fn load_repo(repo_path: string): RepoResult fn list_repos(repos: [Repository], max_count: int): int // Remote package operations fn refresh_repo(repo: Repository): bool fn find_remote_package(name: string): RemotePackageResult fn download_package(pkg: RemotePackage): string fn get_cached_manifest(repo: Repository): string end export // Repository definition type Repository = struct name: string url: string priority: int enabled: bool is_signed: bool keyid: string end Repository // Result type for repository operations type RepoResult = struct success: bool repo: Repository error: string end RepoResult // Remote package from repository manifest type RemotePackage = struct name: string version: string release: int description: string license: string category: string depends: [string] depends_count: int size: int compressed: int filename: string sha256: string signature: string repo_name: string repo_url: string end RemotePackage // Result type for remote package search type RemotePackageResult = struct success: bool pkg: RemotePackage error: string end RemotePackageResult // Create empty repository fn new_repository(): Repository return Repository{ name: "", url: "", priority: 100, enabled: true, is_signed: false, keyid: "" } end new_repository // Create empty remote package fn new_remote_package(): RemotePackage return RemotePackage{ name: "", version: "", release: 0, description: "", license: "", category: "", depends: new [string](64), depends_count: 0, size: 0, compressed: 0, filename: "", sha256: "", signature: "", repo_name: "", repo_url: "" } end new_remote_package // Create error result fn error_result(msg: string): RepoResult return RepoResult{ success: false, repo: new_repository(), error: msg } end error_result // Create package error result fn pkg_error_result(msg: string): RemotePackageResult return RemotePackageResult{ success: false, pkg: new_remote_package(), error: msg } end pkg_error_result // Create package success result fn pkg_success_result(pkg: RemotePackage): RemotePackageResult return RemotePackageResult{ success: true, pkg: pkg, error: "" } end pkg_success_result // Load a repository from a .repo file fn load_repo(repo_path: string): RepoResult if not file.fileExists(repo_path) return error_result("Repository file not found: " + repo_path) end if let content = res.unwrap_or(file.readFile(repo_path), "") if str.length(content) == 0 return error_result("Empty repository file: " + repo_path) end if // Parse TOML let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return error_result("Failed to parse repository file: " + repo_path) end if mut repo = new_repository() // Read repository metadata repo.name = toml.toml_get(keys, vals, entry_count, "repository.name") repo.url = toml.toml_get(keys, vals, entry_count, "repository.url") if toml.toml_has_key(keys, vals, entry_count, "repository.priority") repo.priority = toml.toml_get_int(keys, vals, entry_count, "repository.priority") end if if toml.toml_has_key(keys, vals, entry_count, "repository.enabled") repo.enabled = toml.toml_get_bool(keys, vals, entry_count, "repository.enabled") end if if toml.toml_has_key(keys, vals, entry_count, "repository.signed") repo.is_signed = toml.toml_get_bool(keys, vals, entry_count, "repository.signed") end if if toml.toml_has_key(keys, vals, entry_count, "repository.keyid") repo.keyid = toml.toml_get(keys, vals, entry_count, "repository.keyid") end if return RepoResult{ success: true, repo: repo, error: "" } end load_repo // List all configured repositories fn list_repos(repos: [Repository], max_count: int): int let repos_dir = "/etc/coral/repos.d" if not dir.dir_exists(repos_dir) return 0 end if let entries = res.unwrap_or(dir.list_dir(repos_dir), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Check for .repo extension if str.ends_with(entry, ".repo") let repo_path = path.join_path(repos_dir, entry) let result = load_repo(repo_path) if result.success repos[count] = result.repo count = count + 1 end if end if i = i + 1 end while return count end list_repos // Get the path to cached manifest for a repository fn get_cached_manifest(repo: Repository): string let cache_dir = "/var/lib/coral/repos" return path.join_path(cache_dir, repo.name + ".toml") end get_cached_manifest // Refresh repository metadata from remote fn refresh_repo(repo: Repository): bool if not repo.enabled return false end if if str.length(repo.url) == 0 return false end if // Build URL to repo.toml mut manifest_url = repo.url if not str.ends_with(manifest_url, "/") manifest_url = manifest_url + "/" end if manifest_url = manifest_url + "repo.toml" // Ensure cache directory exists let cache_dir = "/var/lib/coral/repos" if not dir.dir_exists(cache_dir) if not res.is_ok(dir.create_dir_all(cache_dir)) return false end if end if // Download manifest let cache_path = get_cached_manifest(repo) return uhttp.download(manifest_url, cache_path) end refresh_repo // Find a package in all enabled repositories fn find_remote_package(name: string): RemotePackageResult // Get list of repositories mut repos: [Repository] = new [Repository](16) let repo_count = list_repos(repos, 16) if repo_count == 0 return pkg_error_result("No repositories configured") end if // Search in each repository (by priority) // TODO: Sort by priority mut i = 0 while i < repo_count let repo = repos[i] if not repo.enabled i = i + 1 continue end if let result = find_package_in_repo(name, repo) if result.success return result end if i = i + 1 end while return pkg_error_result("Package not found in any repository: " + name) end find_remote_package // Find a package in a specific repository fn find_package_in_repo(name: string, repo: Repository): RemotePackageResult let cache_path = get_cached_manifest(repo) // Check if we have cached manifest if not file.fileExists(cache_path) // Try to refresh if not refresh_repo(repo) return pkg_error_result("Failed to fetch repository manifest") end if end if // Read cached manifest let content = res.unwrap_or(file.readFile(cache_path), "") if str.length(content) == 0 return pkg_error_result("Empty repository manifest") end if // Parse TOML to find the package let keys = toml.toml_alloc_keys() let vals = toml.toml_alloc_values() let entry_count = res.unwrap_or(toml.toml_parse(content, keys, vals), 0) if entry_count == 0 return pkg_error_result("Failed to parse repository manifest") end if // Look for package entry: packages. let pkg_key = "packages." + name if not toml.toml_has_key(keys, vals, entry_count, pkg_key + ".version") return pkg_error_result("Package not found: " + name) end if // Build RemotePackage mut pkg = new_remote_package() pkg.name = name pkg.repo_name = repo.name pkg.repo_url = repo.url pkg.version = toml.toml_get(keys, vals, entry_count, pkg_key + ".version") pkg.release = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".release") pkg.description = toml.toml_get(keys, vals, entry_count, pkg_key + ".description") pkg.license = toml.toml_get(keys, vals, entry_count, pkg_key + ".license") pkg.category = toml.toml_get(keys, vals, entry_count, pkg_key + ".category") pkg.filename = toml.toml_get(keys, vals, entry_count, pkg_key + ".file") pkg.sha256 = toml.toml_get(keys, vals, entry_count, pkg_key + ".sha256") pkg.signature = toml.toml_get(keys, vals, entry_count, pkg_key + ".signature") if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".size") pkg.size = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".size") end if if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".compressed") pkg.compressed = toml.toml_get_int(keys, vals, entry_count, pkg_key + ".compressed") end if // Parse depends array if present if toml.toml_has_key(keys, vals, entry_count, pkg_key + ".depends") let deps_str = toml.toml_get(keys, vals, entry_count, pkg_key + ".depends") pkg.depends_count = parse_toml_array(deps_str, pkg.depends, 64) end if // If filename is empty, construct it if str.length(pkg.filename) == 0 pkg.filename = name + "-" + pkg.version + "-" + int_to_str(pkg.release) + ".x86_64.pkg.tar.xz" end if return pkg_success_result(pkg) end find_package_in_repo // Download a package to local cache fn download_package(pkg: RemotePackage): string if str.length(pkg.filename) == 0 color.print_error("Package has no filename") return "" end if // Build download URL mut base_url = pkg.repo_url if not str.ends_with(base_url, "/") base_url = base_url + "/" end if let pkg_url = base_url + "packages/" + pkg.filename // Ensure packages cache directory exists let cache_dir = types.get_packages_dir() if not dir.dir_exists(cache_dir) if not res.is_ok(dir.create_dir_all(cache_dir)) color.print_error("Failed to create packages cache directory") return "" end if end if // Download to cache let dest_path = path.join_path(cache_dir, pkg.filename) // Check if already cached if file.fileExists(dest_path) color.print_info("Using cached: " + pkg.filename) return dest_path end if // Download color.print_info("Downloading: " + pkg.filename) if not uhttp.download(pkg_url, dest_path) return "" end if return dest_path end download_package // Parse TOML array value into string array fn parse_toml_array(value: string, result: [string], max_items: int): int let len = str.length(value) if len < 2 return 0 end if if value[0] != '[' return 0 end if mut count = 0 mut i = 1 mut in_string = false mut item_start = 0 while i < len and count < max_items let c = value[i] if c == '"' and not in_string in_string = true item_start = i + 1 elif c == '"' and in_string let item_len = i - item_start if item_len > 0 result[count] = str.substring(value, item_start, item_len) count = count + 1 end if in_string = false elif c == ']' and not in_string return count end if i = i + 1 end while return count end parse_toml_array // 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 end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: resolver.reef Authors: Chris Tusa License: Description: Dependency resolution for package installation ******************************************************************************/ module core.resolver import core.str import io.file import io.path import types import core.port import core.database import util.version as ver export type ResolveResult fn resolve(name: string): ResolveResult fn get_install_order(names: [string], name_count: int, result: [string], max_count: int): int fn get_dep_tree(name: string, deps: [string], max_count: int): int // Build dependency resolver (resolves both runtime + build deps recursively from source) fn resolve_build_deps(name: string): ResolveResult // Root-aware version for alternate root installation fn get_install_order_rooted(names: [string], name_count: int, result: [string], max_count: int, root: string): int end export // Result for alternative resolution type AlternativeChoice = struct satisfied: bool // True if an alternative is already installed provider: string // The installed provider (if satisfied) options: [string] // Available options to choose from option_count: int base_dep: string // The base dependency name end AlternativeChoice // Resolution result type ResolveResult = struct success: bool packages: [string] count: int error: string end ResolveResult // Create an empty result fn new_result(): ResolveResult mut packages: [string] = new [string](256) return ResolveResult{ success: false, packages: packages, count: 0, error: "" } end new_result // Resolve dependencies for a package // Returns ordered list of packages to install (dependencies first) fn resolve(name: string): ResolveResult mut result = new_result() // Track visited to detect cycles mut visited: [string] = new [string](256) mut visited_count = 0 // Track resolution order mut order: [string] = new [string](256) mut order_count = 0 // Resolve recursively let ok = resolve_recursive(name, visited, visited_count, order, order_count) if not ok.success result.error = ok.error return result end if result.success = true result.packages = ok.packages result.count = ok.count return result end resolve // Internal result for recursive resolution type RecurseResult = struct success: bool packages: [string] count: int error: string end RecurseResult fn new_recurse_result(): RecurseResult mut packages: [string] = new [string](256) return RecurseResult{ success: true, packages: packages, count: 0, error: "" } end new_recurse_result fn resolve_recursive(name: string, visited: [string], visited_count: int, order: [string], order_count: int): RecurseResult mut result = new_recurse_result() result.packages = order result.count = order_count // Check for cycle if array_contains(visited, visited_count, name) // Already visited - skip result.success = true return result end if // Mark as visited visited[visited_count] = name let new_visited_count = visited_count + 1 // Find the port let port_result = port.find(name) if not port_result.success // Check if already installed if database.is_installed(name) result.success = true return result end if // Check if an alternative provider exists let provider = database.find_provider(name) if str.length(provider) > 0 // Alternative already installed result.success = true return result end if result.success = false result.error = "Port not found: " + name return result end if let p = port_result.port let pkg_name = p.info.name // Resolve runtime dependencies first mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] // Check if this is an alternative dependency if is_alternative_dep(dep) let alt_choice = resolve_alternative(dep) if alt_choice.satisfied // An alternative is already installed, skip i = i + 1 continue end if // No alternative installed - pick the first option // In a full implementation, we'd prompt the user here if alt_choice.option_count > 0 let chosen = alt_choice.options[0] if not array_contains(result.packages, result.count, chosen) let dep_result = resolve_recursive(chosen, visited, new_visited_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if else // Regular dependency — may include version constraint (e.g., "libfoo>=1.0") let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) if database.is_installed(dep_name) // Installed — check version constraint if present if ver.has_constraint(constraint) let installed_pkg = database.get_installed(dep_name) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) // Installed version doesn't satisfy constraint — needs upgrade if not array_contains(result.packages, result.count, dep_name) let dep_result = resolve_recursive(dep_name, visited, new_visited_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if end if // No constraint or constraint satisfied — skip else // Not installed — resolve it if not array_contains(result.packages, result.count, dep_name) let dep_result = resolve_recursive(dep_name, visited, new_visited_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if end if i = i + 1 end while // Add this package to order (after its dependencies) // Use canonical name from package.toml, not the raw search argument if not array_contains(result.packages, result.count, pkg_name) // Skip if already installed if not database.is_installed(pkg_name) result.packages[result.count] = pkg_name result.count = result.count + 1 end if end if result.success = true return result end resolve_recursive // Get install order for multiple packages fn get_install_order(names: [string], name_count: int, result: [string], max_count: int): int mut order_count = 0 mut i = 0 while i < name_count let name = names[i] let resolve_result = resolve(name) if resolve_result.success // Add resolved packages to result mut j = 0 while j < resolve_result.count and order_count < max_count let pkg = resolve_result.packages[j] // Avoid duplicates if not array_contains(result, order_count, pkg) result[order_count] = pkg order_count = order_count + 1 end if j = j + 1 end while end if i = i + 1 end while return order_count end get_install_order // Get dependency tree for display (with indentation info) fn get_dep_tree(name: string, deps: [string], max_count: int): int mut count = 0 collect_deps(name, deps, count, 0, max_count) return count end get_dep_tree // Collect dependencies recursively fn collect_deps(name: string, deps: [string], count: int, depth: int, max_count: int): int if depth > 20 or count >= max_count return count end if let result = port.find(name) if not result.success return count end if let p = result.port mut current_count = count mut i = 0 while i < p.deps.runtime_count and current_count < max_count let dep = p.deps.runtime[i] // Add with depth prefix (number of spaces = depth * 2) let indent = make_indent(depth) deps[current_count] = indent + dep current_count = current_count + 1 // Recurse current_count = collect_deps(dep, deps, current_count, depth + 1, max_count) i = i + 1 end while return current_count end collect_deps // Create indentation string fn make_indent(depth: int): string if depth == 0 return "" end if mut result = "" mut i = 0 while i < depth result = result + " " i = i + 1 end while return result end make_indent // Check if array contains a string fn array_contains(arr: [string], count: int, s: string): bool mut i = 0 while i < count if arr[i] == s return true end if i = i + 1 end while return false end array_contains // Check if a dependency string represents alternatives (format: dep:opt1,opt2,opt3) fn is_alternative_dep(dep: string): bool return str.contains(dep, ":") end is_alternative_dep // Parse alternative options from a dependency string // Format: base_dep:alt1,alt2,alt3 // Returns number of alternatives parsed, fills alternatives array fn parse_alternatives(dep: string, alternatives: [string], max_count: int): int let colon_pos = str.index_of_char(dep, ':') if colon_pos < 0 // Not an alternative, return the single dependency alternatives[0] = dep return 1 end if // Get the alternatives part after the colon let alts_part = str.substring(dep, colon_pos + 1, str.length(dep) - colon_pos - 1) // Split by comma return str.split(alts_part, ',', alternatives, max_count) end parse_alternatives // Get the base dependency name from an alternative string fn get_base_dep(dep: string): string let colon_pos = str.index_of_char(dep, ':') if colon_pos < 0 return dep end if return str.substring(dep, 0, colon_pos) end get_base_dep // Create empty AlternativeChoice fn new_alternative_choice(): AlternativeChoice return AlternativeChoice{ satisfied: false, provider: "", options: new [string](16), option_count: 0, base_dep: "" } end new_alternative_choice // Resolve an alternative dependency // Returns satisfied=true if any option is already installed // Otherwise returns the list of available options fn resolve_alternative(dep: string): AlternativeChoice mut choice = new_alternative_choice() if not is_alternative_dep(dep) // Regular dependency choice.base_dep = dep choice.options[0] = dep choice.option_count = 1 // Check if installed if database.is_installed(dep) choice.satisfied = true choice.provider = dep end if return choice end if // Parse the alternatives choice.base_dep = get_base_dep(dep) choice.option_count = parse_alternatives(dep, choice.options, 16) // Check if any alternative is already installed mut i = 0 while i < choice.option_count let opt = choice.options[i] if database.is_installed(opt) choice.satisfied = true choice.provider = opt return choice end if i = i + 1 end while // Check if any provides this via alternatives let provider = database.find_provider(choice.base_dep) if str.length(provider) > 0 choice.satisfied = true choice.provider = provider return choice end if return choice end resolve_alternative // ============================================================================ // Build dependency resolver (resolves both runtime + build deps from source) // ============================================================================ // Resolve all dependencies needed to build a package from source. // Returns topological order: dependencies first, target package last. // Resolves both runtime and build deps recursively. // Uses proper cycle detection with separate visited/in_stack arrays. fn resolve_build_deps(name: string): ResolveResult mut result = new_result() // Permanent marks: fully resolved packages mut visited: [string] = new [string](256) mut visited_count = 0 // Temporary marks: packages on the current DFS stack (for cycle detection) mut in_stack: [string] = new [string](256) mut stack_count = 0 // Topological build order mut order: [string] = new [string](256) mut order_count = 0 let ok = resolve_build_recursive(name, visited, visited_count, in_stack, stack_count, order, order_count) if not ok.success result.error = ok.error return result end if result.success = true result.packages = ok.packages result.count = ok.count return result end resolve_build_deps fn resolve_build_recursive(name: string, visited: [string], visited_count: int, in_stack: [string], stack_count: int, order: [string], order_count: int): RecurseResult mut result = new_recurse_result() result.packages = order result.count = order_count // Permanent mark: already fully resolved, skip if array_contains(visited, visited_count, name) result.success = true return result end if // Temporary mark: cycle detected if array_contains(in_stack, stack_count, name) result.success = false result.error = "Circular dependency detected: " + name return result end if // If already installed, mark visited and skip (don't need to build) if database.is_installed(name) visited[visited_count] = name result.success = true return result end if // Find the port (must exist to build from source) let port_result = port.find(name) if not port_result.success // Check if a provider is already installed let provider = database.find_provider(name) if str.length(provider) > 0 visited[visited_count] = name result.success = true return result end if result.success = false result.error = "Port not found: " + name return result end if let p = port_result.port let pkg_name = p.info.name // Push onto DFS stack (temporary mark) in_stack[stack_count] = name let new_stack_count = stack_count + 1 // Mark as visited (permanent mark) visited[visited_count] = name let new_visited_count = visited_count + 1 // Recurse into RUNTIME deps mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] if is_alternative_dep(dep) let alt_choice = resolve_alternative(dep) if not alt_choice.satisfied and alt_choice.option_count > 0 let chosen = alt_choice.options[0] let dep_result = resolve_build_recursive(chosen, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if else let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) mut needs_build = false if database.is_installed(dep_name) // Check version constraint if ver.has_constraint(constraint) let installed_pkg = database.get_installed(dep_name) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) needs_build = true end if end if else needs_build = true end if if needs_build let dep_result = resolve_build_recursive(dep_name, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if i = i + 1 end while // Recurse into BUILD deps i = 0 while i < p.deps.build_count let dep = p.deps.build[i] if is_alternative_dep(dep) let alt_choice = resolve_alternative(dep) if not alt_choice.satisfied and alt_choice.option_count > 0 let chosen = alt_choice.options[0] let dep_result = resolve_build_recursive(chosen, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if else let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) mut needs_build = false if database.is_installed(dep_name) if ver.has_constraint(constraint) let installed_pkg = database.get_installed(dep_name) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) needs_build = true end if end if else needs_build = true end if if needs_build let dep_result = resolve_build_recursive(dep_name, visited, new_visited_count, in_stack, new_stack_count, result.packages, result.count) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if i = i + 1 end while // Post-order: add this package AFTER all its deps (topological sort) if not array_contains(result.packages, result.count, pkg_name) result.packages[result.count] = pkg_name result.count = result.count + 1 end if result.success = true return result end resolve_build_recursive // ============================================================================ // Root-aware resolver functions for alternate root installation // ============================================================================ // Internal result for rooted recursive resolution type RootedRecurseResult = struct success: bool packages: [string] count: int error: string end RootedRecurseResult fn new_rooted_recurse_result(): RootedRecurseResult mut packages: [string] = new [string](256) return RootedRecurseResult{ success: true, packages: packages, count: 0, error: "" } end new_rooted_recurse_result // Resolve dependencies checking against a specific root fn resolve_recursive_rooted(name: string, visited: [string], visited_count: int, order: [string], order_count: int, root: string): RootedRecurseResult mut result = new_rooted_recurse_result() result.packages = order result.count = order_count // Check for cycle if array_contains(visited, visited_count, name) result.success = true return result end if // Mark as visited visited[visited_count] = name let new_visited_count = visited_count + 1 // Find the port let port_result = port.find(name) if not port_result.success // Check if already installed in target root if database.is_installed_rooted(name, root) result.success = true return result end if // Check if an alternative provider exists in target root // Note: find_provider doesn't have rooted version yet, skip for now result.success = false result.error = "Port not found: " + name return result end if let p = port_result.port let pkg_name = p.info.name // Resolve runtime dependencies first mut i = 0 while i < p.deps.runtime_count let dep = p.deps.runtime[i] // Skip alternative deps for now (simplification) if not is_alternative_dep(dep) // Parse version constraint if present let constraint = ver.parse_dep_constraint(dep) let dep_name = ver.constraint_name(constraint) if database.is_installed_rooted(dep_name, root) // Check version constraint if present if ver.has_constraint(constraint) let installed_pkg = database.get_installed_rooted(dep_name, root) if not ver.check_version_constraint(installed_pkg.info.version, ver.constraint_op(constraint), ver.constraint_version(constraint)) // Needs upgrade let dep_result = resolve_recursive_rooted(dep_name, visited, new_visited_count, result.packages, result.count, root) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if else let dep_result = resolve_recursive_rooted(dep_name, visited, new_visited_count, result.packages, result.count, root) if not dep_result.success return dep_result end if result.packages = dep_result.packages result.count = dep_result.count end if end if i = i + 1 end while // Add this package to order (after its dependencies) // Use canonical name from package.toml, not the raw search argument if not array_contains(result.packages, result.count, pkg_name) // Skip if already installed in target root if not database.is_installed_rooted(pkg_name, root) result.packages[result.count] = pkg_name result.count = result.count + 1 end if end if result.success = true return result end resolve_recursive_rooted // Resolve for a single package with root support fn resolve_rooted(name: string, root: string): ResolveResult mut result = new_result() mut visited: [string] = new [string](256) mut visited_count = 0 mut order: [string] = new [string](256) mut order_count = 0 let ok = resolve_recursive_rooted(name, visited, visited_count, order, order_count, root) if not ok.success result.error = ok.error return result end if result.success = true result.packages = ok.packages result.count = ok.count return result end resolve_rooted // Get install order for multiple packages, checking against target root fn get_install_order_rooted(names: [string], name_count: int, result: [string], max_count: int, root: string): int mut order_count = 0 mut i = 0 while i < name_count let name = names[i] let resolve_result = resolve_rooted(name, root) if resolve_result.success mut j = 0 while j < resolve_result.count and order_count < max_count let pkg = resolve_result.packages[j] if not array_contains(result, order_count, pkg) result[order_count] = pkg order_count = order_count + 1 end if j = j + 1 end while end if i = i + 1 end while return order_count end get_install_order_rooted end module /****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: signing.reef Authors: Chris Tusa License: Description: Ed25519 signing and verification for packages and repositories ******************************************************************************/ module core.signing import core.str import io.file import io.dir import io.path import sys.process import core.result as res export type KeyPair type SignResult type VerifyResult fn generate_keypair(name: string): SignResult fn sign_file(file_path: string, key_path: string): SignResult fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult fn import_key(key_data: string, name: string): SignResult fn list_keys(keys: [KeyPair], max_count: int): int fn get_keyring_dir(): string fn get_trusted_keys_dir(): string // High-level verification functions fn verify_package(pkg_path: string, sig_path: string): VerifyResult fn verify_manifest(manifest_path: string): VerifyResult fn is_key_trusted(keyid: string): bool end export // Key pair information type KeyPair = struct name: string keyid: string public_key: string private_key: string created: string trusted: bool end KeyPair // Result for signing operations type SignResult = struct success: bool signature: string error: string end SignResult // Result for verification type VerifyResult = struct success: bool valid: bool keyid: string error: string end VerifyResult // Get the keyring directory fn get_keyring_dir(): string return "/etc/coral/keys" end get_keyring_dir // Create empty keypair fn new_keypair(): KeyPair return KeyPair{ name: "", keyid: "", public_key: "", private_key: "", created: "", trusted: false } end new_keypair // Create error result fn error_sign_result(msg: string): SignResult return SignResult{ success: false, signature: "", error: msg } end error_sign_result fn error_verify_result(msg: string): VerifyResult return VerifyResult{ success: false, valid: false, keyid: "", error: msg } end error_verify_result // Generate a new Ed25519 keypair // Note: This requires an external tool (openssl or signify) fn generate_keypair(name: string): SignResult let keyring = get_keyring_dir() // Ensure keyring directory exists if not dir.dir_exists(keyring) if not res.is_ok(dir.create_dir_all(keyring)) return error_sign_result("Failed to create keyring directory") end if end if let private_path = path.join_path(keyring, name + ".key") let public_path = path.join_path(keyring, name + ".pub") // Check if key already exists if file.fileExists(private_path) return error_sign_result("Key already exists: " + name) end if // Generate key using openssl let cmd = "openssl genpkey -algorithm ed25519 -out \"" + private_path + "\" && " + "openssl pkey -in \"" + private_path + "\" -pubout -out \"" + public_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return error_sign_result("Failed to start key generation") end if let exit_code = process.process_wait(pid) if exit_code != 0 return error_sign_result("Key generation failed") end if return SignResult{ success: true, signature: "", error: "" } end generate_keypair // Sign a file with Ed25519 fn sign_file(file_path: string, key_path: string): SignResult if not file.fileExists(file_path) return error_sign_result("File not found: " + file_path) end if if not file.fileExists(key_path) return error_sign_result("Key not found: " + key_path) end if let sig_path = file_path + ".sig" // Sign using openssl let cmd = "openssl pkeyutl -sign -inkey \"" + key_path + "\" -in \"" + file_path + "\" -out \"" + sig_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return error_sign_result("Failed to start signing") end if let exit_code = process.process_wait(pid) if exit_code != 0 return error_sign_result("Signing failed") end if return SignResult{ success: true, signature: sig_path, error: "" } end sign_file // Verify a file signature fn verify_file(file_path: string, sig_path: string, key_path: string): VerifyResult if not file.fileExists(file_path) return error_verify_result("File not found: " + file_path) end if if not file.fileExists(sig_path) return error_verify_result("Signature not found: " + sig_path) end if if not file.fileExists(key_path) return error_verify_result("Key not found: " + key_path) end if // Verify using openssl let cmd = "openssl pkeyutl -verify -pubin -inkey \"" + key_path + "\" -in \"" + file_path + "\" -sigfile \"" + sig_path + "\"" let pid = process.process_spawn_shell(cmd) if pid < 0 return error_verify_result("Failed to start verification") end if let exit_code = process.process_wait(pid) if exit_code == 0 return VerifyResult{ success: true, valid: true, keyid: "", error: "" } else return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature verification failed" } end if end verify_file // Import a public key fn import_key(key_data: string, name: string): SignResult let keyring = get_keyring_dir() if not dir.dir_exists(keyring) if not res.is_ok(dir.create_dir_all(keyring)) return error_sign_result("Failed to create keyring directory") end if end if let key_path = path.join_path(keyring, name + ".pub") if file.fileExists(key_path) return error_sign_result("Key already exists: " + name) end if if not res.is_ok(file.writeFile(key_path, key_data)) return error_sign_result("Failed to write key file") end if return SignResult{ success: true, signature: "", error: "" } end import_key // List all keys in keyring fn list_keys(keys: [KeyPair], max_count: int): int let keyring = get_keyring_dir() if not dir.dir_exists(keyring) return 0 end if let entries = res.unwrap_or(dir.list_dir(keyring), new [string](0)) let entry_count = entries.length() mut count = 0 mut i = 0 while i < entry_count and count < max_count let entry = entries[i] // Look for .pub files if str.ends_with(entry, ".pub") let name_len = str.length(entry) - 4 let name = str.substring(entry, 0, name_len) mut kp = new_keypair() kp.name = name kp.trusted = true // Local keys are trusted // Read public key let pub_path = path.join_path(keyring, entry) kp.public_key = res.unwrap_or(file.readFile(pub_path), "") // Check for private key let priv_path = path.join_path(keyring, name + ".key") if file.fileExists(priv_path) kp.private_key = "(present)" end if keys[count] = kp count = count + 1 end if i = i + 1 end while return count end list_keys // Get the trusted keys directory fn get_trusted_keys_dir(): string return "/var/lib/coral/trusted-keys" end get_trusted_keys_dir // Check if a key is in the trusted keys list fn is_key_trusted(keyid: string): bool let trusted_dir = get_trusted_keys_dir() if not dir.dir_exists(trusted_dir) return false end if // Look for key file by keyid let key_path = path.join_path(trusted_dir, keyid + ".pub") if file.fileExists(key_path) return true end if // Also check the main keyring let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub") return file.fileExists(keyring_path) end is_key_trusted // Get path to a trusted key by keyid fn get_trusted_key_path(keyid: string): string // Try trusted-keys directory first let trusted_dir = get_trusted_keys_dir() let trusted_path = path.join_path(trusted_dir, keyid + ".pub") if file.fileExists(trusted_path) return trusted_path end if // Fall back to main keyring let keyring_path = path.join_path(get_keyring_dir(), keyid + ".pub") if file.fileExists(keyring_path) return keyring_path end if return "" end get_trusted_key_path // Verify a package with any trusted key fn verify_package(pkg_path: string, sig_path: string): VerifyResult if not file.fileExists(pkg_path) return error_verify_result("Package not found: " + pkg_path) end if if not file.fileExists(sig_path) return error_verify_result("Signature not found: " + sig_path) end if // Try to verify with each trusted key let trusted_dir = get_trusted_keys_dir() let keyring_dir = get_keyring_dir() // Collect all public keys from both directories mut all_keys: [string] = new [string](64) mut key_count = 0 // Get keys from trusted-keys directory if dir.dir_exists(trusted_dir) let entries = res.unwrap_or(dir.list_dir(trusted_dir), new [string](0)) let entry_count = entries.length() mut i = 0 while i < entry_count and key_count < 64 if str.ends_with(entries[i], ".pub") all_keys[key_count] = path.join_path(trusted_dir, entries[i]) key_count = key_count + 1 end if i = i + 1 end while end if // Get keys from keyring directory if dir.dir_exists(keyring_dir) let entries = res.unwrap_or(dir.list_dir(keyring_dir), new [string](0)) let entry_count = entries.length() mut i = 0 while i < entry_count and key_count < 64 if str.ends_with(entries[i], ".pub") let key_path = path.join_path(keyring_dir, entries[i]) // Avoid duplicates mut is_dup = false mut j = 0 while j < key_count if str.equals(all_keys[j], key_path) is_dup = true end if j = j + 1 end while if not is_dup all_keys[key_count] = key_path key_count = key_count + 1 end if end if i = i + 1 end while end if if key_count == 0 return error_verify_result("No trusted keys found") end if // Try each key mut i = 0 while i < key_count let key_path = all_keys[i] let result = verify_file(pkg_path, sig_path, key_path) if result.success and result.valid // Extract keyid from path let filename = path.basename(key_path) let keyid = str.substring(filename, 0, str.length(filename) - 4) return VerifyResult{ success: true, valid: true, keyid: keyid, error: "" } end if i = i + 1 end while return VerifyResult{ success: true, valid: false, keyid: "", error: "Signature does not match any trusted key" } end verify_package // Verify a repository manifest (repo.toml and repo.toml.sig) fn verify_manifest(manifest_path: string): VerifyResult if not file.fileExists(manifest_path) return error_verify_result("Manifest not found: " + manifest_path) end if let sig_path = manifest_path + ".sig" if not file.fileExists(sig_path) return error_verify_result("Manifest signature not found: " + sig_path) end if return verify_package(manifest_path, sig_path) end verify_manifest end module