/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (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