/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025, Leafscale, LLC - https://www.leafscale.com Project: Zygaena Filename: verify.reef Authors: Chris Tusa License: Description: Verify command - check installed package integrity ******************************************************************************/ module commands.verify import sys.args import io.file import io.dir import io.path import core.str import core.config import core.database import util.mtree import util.checksum import types import util.color import exitcodes as ec import fs.stat import fs.link export fn execute(opts: types.GlobalOptions): int end export fn execute(opts: types.GlobalOptions): int let argc = args.count() if argc <= opts.cmd_index + 1 print_usage() return ec.EXIT_USAGE() end if // Load configuration and apply command-line overrides let base_cfg = config.load() let cfg = config.apply_overrides(base_cfg, opts.root, opts.prefix, opts.no_chroot) let root_path = config.get_root(cfg) if opts.verbose and str.length(root_path) > 0 color.print_info("Using root: " + root_path) end if let pkg_name = args.get(opts.cmd_index + 1) // Check if installed if not check_installed(pkg_name, root_path) color.print_error(pkg_name + " is not installed") return ec.EXIT_PKG_NOT_FOUND() end if color.print_action("Verifying " + pkg_name + "...") // Load manifest from database let manifest_path = database.get_manifest_path(pkg_name) if not file.fileExists(manifest_path) color.print_error("No manifest found for " + pkg_name + " (legacy package without .MANIFEST)") return ec.EXIT_PKG_INVALID() end if let manifest_content = file.readFile(manifest_path) if str.length(manifest_content) == 0 color.print_error("Empty manifest for " + pkg_name) return ec.EXIT_PKG_INVALID() end if mut entries: [mtree.ManifestEntry] = new [mtree.ManifestEntry](8192) let entry_count = mtree.parse_manifest(manifest_content, entries, 8192) if entry_count == 0 color.print_error("Failed to parse manifest for " + pkg_name) return ec.EXIT_PKG_INVALID() end if // Determine actual root let actual_root = get_actual_root(root_path) // Verify each entry mut missing_count = 0 mut checksum_fail_count = 0 mut mode_fail_count = 0 mut link_fail_count = 0 mut ok_count = 0 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 full_path = path.join_path(actual_root, rel_path) if str.equals(etype, "dir") if not dir.dir_exists(full_path) color.print_warning("MISSING dir: " + rel_path) missing_count = missing_count + 1 else // Check mode let expected_mode = mtree.entry_mode(entry) let actual_mode = stat.file_mode(full_path) // Compare lower 12 bits (permissions + setuid/setgid/sticky) if (actual_mode % 4096) != (expected_mode % 4096) if opts.verbose color.print_warning("MODE " + rel_path + " (expected " + int_to_octal(expected_mode) + ", got " + int_to_octal(actual_mode) + ")") else color.print_warning("MODE " + rel_path) end if mode_fail_count = mode_fail_count + 1 else ok_count = ok_count + 1 end if end if elif str.equals(etype, "file") if not stat.exists(full_path) color.print_warning("MISSING file: " + rel_path) missing_count = missing_count + 1 else mut file_ok = true // Check sha256 let expected_sha = mtree.entry_sha256(entry) if str.length(expected_sha) > 0 let actual_sha = checksum.sha256_file(full_path) if not str.equals(actual_sha, expected_sha) color.print_warning("CHECKSUM " + rel_path) checksum_fail_count = checksum_fail_count + 1 file_ok = false end if end if // Check mode let expected_mode = mtree.entry_mode(entry) let actual_mode = stat.file_mode(full_path) if (actual_mode % 4096) != (expected_mode % 4096) if opts.verbose color.print_warning("MODE " + rel_path + " (expected " + int_to_octal(expected_mode) + ", got " + int_to_octal(actual_mode) + ")") else color.print_warning("MODE " + rel_path) end if mode_fail_count = mode_fail_count + 1 file_ok = false end if // Check ownership let expected_uid = mtree.name_to_uid(mtree.entry_uname(entry)) let expected_gid = mtree.name_to_gid(mtree.entry_gname(entry)) let actual_uid = stat.file_uid(full_path) let actual_gid = stat.file_gid(full_path) if actual_uid != expected_uid or actual_gid != expected_gid if opts.verbose color.print_warning("OWNER " + rel_path + " (expected " + mtree.entry_uname(entry) + ":" + mtree.entry_gname(entry) + ")") else color.print_warning("OWNER " + rel_path) end if mode_fail_count = mode_fail_count + 1 file_ok = false end if if file_ok ok_count = ok_count + 1 end if end if elif str.equals(etype, "link") if not stat.is_symlink(full_path) if not stat.exists(full_path) color.print_warning("MISSING link: " + rel_path) missing_count = missing_count + 1 else color.print_warning("NOT A LINK " + rel_path) link_fail_count = link_fail_count + 1 end if else let expected_target = mtree.entry_link(entry) let actual_target = link.readlink(full_path) if not str.equals(actual_target, expected_target) if opts.verbose color.print_warning("LINK " + rel_path + " -> " + actual_target + " (expected " + expected_target + ")") else color.print_warning("LINK TARGET " + rel_path) end if link_fail_count = link_fail_count + 1 else ok_count = ok_count + 1 end if end if end if i = i + 1 end while // Summary println("") let total_issues = missing_count + checksum_fail_count + mode_fail_count + link_fail_count if total_issues == 0 color.print_success(pkg_name + ": " + int_to_str(ok_count) + " entries verified, all OK") return ec.EXIT_SUCCESS() else color.print_error(pkg_name + ": " + int_to_str(total_issues) + " issue(s) found") if missing_count > 0 println(" Missing: " + int_to_str(missing_count)) end if if checksum_fail_count > 0 println(" Checksum: " + int_to_str(checksum_fail_count)) end if if mode_fail_count > 0 println(" Mode/Owner: " + int_to_str(mode_fail_count)) end if if link_fail_count > 0 println(" Symlinks: " + int_to_str(link_fail_count)) end if println(" OK: " + int_to_str(ok_count)) return ec.EXIT_PKG_CHECKSUM_MISMATCH() end if end execute fn check_installed(name: string, root_path: string): bool if str.length(root_path) > 0 return database.is_installed_rooted(name, root_path) end if return database.is_installed(name) end check_installed fn get_actual_root(root_path: string): string if str.length(root_path) == 0 return "/" end if return root_path end get_actual_root proc print_usage() println("Usage: coral verify ") println("") println("Verify integrity of an installed package against its manifest.") println("") println("Checks:") println(" - Missing files, directories, and symlinks") println(" - File checksum (sha256) mismatches") println(" - Permission and ownership changes") println(" - Broken or incorrect symlink targets") println("") println("Arguments:") println(" Name of installed package to verify") println("") println("Options:") println(" -v, --verbose Show expected vs actual values for failures") println("") println("Examples:") println(" coral verify vim") println(" coral verify -v openssl") end print_usage // 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 // Helper: convert int to octal string (for mode display) fn int_to_octal(n: int): string if n == 0 return "0" end if mut value = n % 4096 mut result = "" while value > 0 let digit = value % 8 result = str.concat(str.substring("01234567", digit, 1), result) value = value / 8 end while return result end int_to_octal end module