From d2d1c297ccd2f7fb086f233f10cd9dbda80dcdb8 Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Mon, 22 Dec 2025 20:10:17 +0100 Subject: [PATCH] Refactor to align with Rust formatting guidelines and enhance code readability. - Adjusted indentation and line breaks for structs, functions, and method calls to comply with Rust formatting standards. - Improved error message formatting and consistency across `PkgTreeError` instances. - Restructured long function arguments and chained calls for clarity and maintainability. - Simplified conditional statements and loops for better readability. - No functional changes introduced. --- libips/src/actions/executors.rs | 159 ++- libips/src/actions/mod.rs | 22 +- libips/src/api.rs | 274 +++- libips/src/depend/mod.rs | 160 ++- libips/src/digest/mod.rs | 6 +- libips/src/image/action_plan.rs | 14 +- libips/src/image/catalog.rs | 694 ++++++---- libips/src/image/installed.rs | 262 ++-- libips/src/image/installed_tests.rs | 61 +- libips/src/image/mod.rs | 367 +++--- libips/src/image/tests.rs | 121 +- libips/src/lib.rs | 64 +- libips/src/publisher.rs | 24 +- libips/src/publisher_tests.rs | 57 +- libips/src/repository/catalog_writer.rs | 78 +- libips/src/repository/file_backend.rs | 402 +++--- libips/src/repository/mod.rs | 16 +- libips/src/repository/obsoleted.rs | 1396 ++++++++++++--------- libips/src/repository/progress.rs | 14 +- libips/src/repository/rest_backend.rs | 283 +++-- libips/src/repository/tests.rs | 188 +-- libips/src/solver/advice.rs | 262 +++- libips/src/solver/mod.rs | 694 +++++++--- libips/src/test_json_manifest.rs | 21 +- libips/src/transformer.rs | 2 +- libips/tests/e2e_openindiana.rs | 15 +- pkg6/src/error.rs | 14 +- pkg6/src/main.rs | 568 ++++++--- pkg6depotd/src/cli.rs | 2 +- pkg6depotd/src/config.rs | 11 +- pkg6depotd/src/errors.rs | 20 +- pkg6depotd/src/http/admin.rs | 21 +- pkg6depotd/src/http/handlers/catalog.rs | 19 +- pkg6depotd/src/http/handlers/file.rs | 57 +- pkg6depotd/src/http/handlers/info.rs | 114 +- pkg6depotd/src/http/handlers/manifest.rs | 20 +- pkg6depotd/src/http/handlers/mod.rs | 4 +- pkg6depotd/src/http/handlers/publisher.rs | 31 +- pkg6depotd/src/http/handlers/versions.rs | 32 +- pkg6depotd/src/http/mod.rs | 6 +- pkg6depotd/src/http/routes.rs | 35 +- pkg6depotd/src/http/server.rs | 10 +- pkg6depotd/src/lib.rs | 37 +- pkg6depotd/src/main.rs | 2 +- pkg6depotd/src/repo.rs | 96 +- pkg6depotd/src/telemetry/mod.rs | 4 +- pkg6depotd/tests/integration_tests.rs | 165 ++- pkg6repo/src/e2e_tests.rs | 107 +- pkg6repo/src/main.rs | 376 +++--- pkg6repo/src/pkg5_import.rs | 42 +- pkgtree/src/main.rs | 579 +++++++-- ports/src/main.rs | 4 +- ports/src/workspace.rs | 4 +- specfile/src/macros.rs | 22 +- userland/src/component.rs | 2 +- userland/src/lib.rs | 44 +- 56 files changed, 5356 insertions(+), 2748 deletions(-) diff --git a/libips/src/actions/executors.rs b/libips/src/actions/executors.rs index c45c40b..b4d3589 100644 --- a/libips/src/actions/executors.rs +++ b/libips/src/actions/executors.rs @@ -9,8 +9,8 @@ use miette::Diagnostic; use thiserror::Error; use tracing::info; -use crate::actions::{Link as LinkAction, Manifest}; use crate::actions::{Dir as DirAction, File as FileAction}; +use crate::actions::{Link as LinkAction, Manifest}; #[derive(Error, Debug, Diagnostic)] pub enum InstallerError { @@ -23,16 +23,25 @@ pub enum InstallerError { }, #[error("Absolute paths are forbidden in actions: {path}")] - #[diagnostic(code(ips::installer_error::absolute_path_forbidden), help("Provide paths relative to the image root"))] + #[diagnostic( + code(ips::installer_error::absolute_path_forbidden), + help("Provide paths relative to the image root") + )] AbsolutePathForbidden { path: String }, #[error("Path escapes image root via traversal: {rel}")] - #[diagnostic(code(ips::installer_error::path_outside_image), help("Remove '..' components that escape the image root"))] + #[diagnostic( + code(ips::installer_error::path_outside_image), + help("Remove '..' components that escape the image root") + )] PathTraversalOutsideImage { rel: String }, #[error("Unsupported or not yet implemented action: {action} ({reason})")] #[diagnostic(code(ips::installer_error::unsupported_action))] - UnsupportedAction { action: &'static str, reason: String }, + UnsupportedAction { + action: &'static str, + reason: String, + }, } fn parse_mode(mode: &str, default: u32) -> u32 { @@ -74,7 +83,7 @@ pub fn safe_join(image_root: &Path, rel: &str) -> Result { return Err(InstallerError::AbsolutePathForbidden { path: rel.to_string(), - }) + }); } } } @@ -107,7 +116,10 @@ impl std::fmt::Debug for ApplyOptions { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ApplyOptions") .field("dry_run", &self.dry_run) - .field("progress", &self.progress.as_ref().map(|_| "Some(callback)")) + .field( + "progress", + &self.progress.as_ref().map(|_| "Some(callback)"), + ) .field("progress_interval", &self.progress_interval) .finish() } @@ -115,65 +127,154 @@ impl std::fmt::Debug for ApplyOptions { impl Default for ApplyOptions { fn default() -> Self { - Self { dry_run: false, progress: None, progress_interval: 0 } + Self { + dry_run: false, + progress: None, + progress_interval: 0, + } } } /// Progress event emitted by apply_manifest when a callback is provided. #[derive(Debug, Clone, Copy)] pub enum ProgressEvent { - StartingPhase { phase: &'static str, total: usize }, - Progress { phase: &'static str, current: usize, total: usize }, - FinishedPhase { phase: &'static str, total: usize }, + StartingPhase { + phase: &'static str, + total: usize, + }, + Progress { + phase: &'static str, + current: usize, + total: usize, + }, + FinishedPhase { + phase: &'static str, + total: usize, + }, } pub type ProgressCallback = Arc; /// Apply a manifest to the filesystem rooted at image_root. /// This function enforces ordering: directories, then files, then links, then others (no-ops for now). -pub fn apply_manifest(image_root: &Path, manifest: &Manifest, opts: &ApplyOptions) -> Result<(), InstallerError> { +pub fn apply_manifest( + image_root: &Path, + manifest: &Manifest, + opts: &ApplyOptions, +) -> Result<(), InstallerError> { let emit = |evt: ProgressEvent, cb: &Option| { - if let Some(cb) = cb.as_ref() { (cb)(evt); } + if let Some(cb) = cb.as_ref() { + (cb)(evt); + } }; // Directories first let total_dirs = manifest.directories.len(); - if total_dirs > 0 { emit(ProgressEvent::StartingPhase { phase: "directories", total: total_dirs }, &opts.progress); } + if total_dirs > 0 { + emit( + ProgressEvent::StartingPhase { + phase: "directories", + total: total_dirs, + }, + &opts.progress, + ); + } let mut i = 0usize; for d in &manifest.directories { apply_dir(image_root, d, opts)?; i += 1; if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_dirs) { - emit(ProgressEvent::Progress { phase: "directories", current: i, total: total_dirs }, &opts.progress); + emit( + ProgressEvent::Progress { + phase: "directories", + current: i, + total: total_dirs, + }, + &opts.progress, + ); } } - if total_dirs > 0 { emit(ProgressEvent::FinishedPhase { phase: "directories", total: total_dirs }, &opts.progress); } + if total_dirs > 0 { + emit( + ProgressEvent::FinishedPhase { + phase: "directories", + total: total_dirs, + }, + &opts.progress, + ); + } // Files next let total_files = manifest.files.len(); - if total_files > 0 { emit(ProgressEvent::StartingPhase { phase: "files", total: total_files }, &opts.progress); } + if total_files > 0 { + emit( + ProgressEvent::StartingPhase { + phase: "files", + total: total_files, + }, + &opts.progress, + ); + } i = 0; for f_action in &manifest.files { apply_file(image_root, f_action, opts)?; i += 1; if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_files) { - emit(ProgressEvent::Progress { phase: "files", current: i, total: total_files }, &opts.progress); + emit( + ProgressEvent::Progress { + phase: "files", + current: i, + total: total_files, + }, + &opts.progress, + ); } } - if total_files > 0 { emit(ProgressEvent::FinishedPhase { phase: "files", total: total_files }, &opts.progress); } + if total_files > 0 { + emit( + ProgressEvent::FinishedPhase { + phase: "files", + total: total_files, + }, + &opts.progress, + ); + } // Links let total_links = manifest.links.len(); - if total_links > 0 { emit(ProgressEvent::StartingPhase { phase: "links", total: total_links }, &opts.progress); } + if total_links > 0 { + emit( + ProgressEvent::StartingPhase { + phase: "links", + total: total_links, + }, + &opts.progress, + ); + } i = 0; for l in &manifest.links { apply_link(image_root, l, opts)?; i += 1; if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_links) { - emit(ProgressEvent::Progress { phase: "links", current: i, total: total_links }, &opts.progress); + emit( + ProgressEvent::Progress { + phase: "links", + current: i, + total: total_links, + }, + &opts.progress, + ); } } - if total_links > 0 { emit(ProgressEvent::FinishedPhase { phase: "links", total: total_links }, &opts.progress); } + if total_links > 0 { + emit( + ProgressEvent::FinishedPhase { + phase: "links", + total: total_links, + }, + &opts.progress, + ); + } // Other action kinds are ignored for now and left for future extension. Ok(()) @@ -216,7 +317,11 @@ fn ensure_parent(image_root: &Path, p: &str, opts: &ApplyOptions) -> Result<(), Ok(()) } -fn apply_file(image_root: &Path, f: &FileAction, opts: &ApplyOptions) -> Result<(), InstallerError> { +fn apply_file( + image_root: &Path, + f: &FileAction, + opts: &ApplyOptions, +) -> Result<(), InstallerError> { let full = safe_join(image_root, &f.path)?; // Ensure parent exists (directories should already be applied, but be robust) @@ -248,7 +353,11 @@ fn apply_file(image_root: &Path, f: &FileAction, opts: &ApplyOptions) -> Result< Ok(()) } -fn apply_link(image_root: &Path, l: &LinkAction, opts: &ApplyOptions) -> Result<(), InstallerError> { +fn apply_link( + image_root: &Path, + l: &LinkAction, + opts: &ApplyOptions, +) -> Result<(), InstallerError> { let link_path = safe_join(image_root, &l.path)?; // Determine link type (default to symlink). If properties contain type=hard, create hard link. @@ -275,7 +384,9 @@ fn apply_link(image_root: &Path, l: &LinkAction, opts: &ApplyOptions) -> Result< } else { // Symlink: require non-absolute target to avoid embedding full host paths if Path::new(&l.target).is_absolute() { - return Err(InstallerError::AbsolutePathForbidden { path: l.target.clone() }); + return Err(InstallerError::AbsolutePathForbidden { + path: l.target.clone(), + }); } // Create relative symlink as provided (do not convert to absolute to avoid embedding full paths) #[cfg(target_family = "unix")] diff --git a/libips/src/actions/mod.rs b/libips/src/actions/mod.rs index be8164e..4d85b34 100644 --- a/libips/src/actions/mod.rs +++ b/libips/src/actions/mod.rs @@ -898,7 +898,10 @@ impl Manifest { match serde_json::from_str::(&content) { Ok(manifest) => Ok(manifest), Err(err) => { - debug!("Manifest::parse_file: Error in JSON deserialization: {}. Continuing with mtree like format parsing", err); + debug!( + "Manifest::parse_file: Error in JSON deserialization: {}. Continuing with mtree like format parsing", + err + ); // If JSON parsing fails, fall back to string format Manifest::parse_string(content) } @@ -933,17 +936,24 @@ impl Manifest { property.key = prop.as_str().to_owned(); } Rule::property_value => { - let str_val: String = prop.as_str().to_owned(); - property.value = str_val - .replace(['\"', '\\'], ""); + let str_val: String = + prop.as_str().to_owned(); + property.value = + str_val.replace(['\"', '\\'], ""); } - _ => panic!("unexpected rule {:?} inside action expected property_name or property_value", prop.as_rule()) + _ => panic!( + "unexpected rule {:?} inside action expected property_name or property_value", + prop.as_rule() + ), } } act.properties.push(property); } Rule::EOI => (), - _ => panic!("unexpected rule {:?} inside action expected payload, property, action_name", action.as_rule()), + _ => panic!( + "unexpected rule {:?} inside action expected payload, property, action_name", + action.as_rule() + ), } } m.add_action(act); diff --git a/libips/src/api.rs b/libips/src/api.rs index fe31e09..c420406 100644 --- a/libips/src/api.rs +++ b/libips/src/api.rs @@ -58,12 +58,17 @@ use walkdir::WalkDir; pub use crate::actions::Manifest; // Core typed manifest -use crate::actions::{Attr, Dependency as DependAction, File as FileAction, License as LicenseAction, Link as LinkAction, Property}; +use crate::actions::{ + Attr, Dependency as DependAction, File as FileAction, License as LicenseAction, + Link as LinkAction, Property, +}; pub use crate::depend::{FileDep, GenerateOptions as DependGenerateOptions}; pub use crate::fmri::Fmri; // For BaseMeta use crate::repository::file_backend::{FileBackend, Transaction}; -use crate::repository::{ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository}; +use crate::repository::{ + ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository, +}; use crate::transformer; pub use crate::transformer::TransformRule; @@ -87,7 +92,10 @@ pub enum IpsError { Io(String), #[error("Unimplemented feature: {feature}")] - #[diagnostic(code(ips::api_error::unimplemented), help("See doc/forge_docs/ips_integration.md for roadmap."))] + #[diagnostic( + code(ips::api_error::unimplemented), + help("See doc/forge_docs/ips_integration.md for roadmap.") + )] Unimplemented { feature: &'static str }, } @@ -183,19 +191,32 @@ impl ManifestBuilder { let mut props = std::collections::HashMap::new(); props.insert( "path".to_string(), - Property { key: "path".to_string(), value: path.to_string() }, + Property { + key: "path".to_string(), + value: path.to_string(), + }, ); props.insert( "license".to_string(), - Property { key: "license".to_string(), value: license_name.to_string() }, + Property { + key: "license".to_string(), + value: license_name.to_string(), + }, ); - self.manifest.licenses.push(LicenseAction { payload: String::new(), properties: props }); + self.manifest.licenses.push(LicenseAction { + payload: String::new(), + properties: props, + }); self } /// Add a link action pub fn add_link(&mut self, path: &str, target: &str) -> &mut Self { - self.manifest.links.push(LinkAction { path: path.to_string(), target: target.to_string(), properties: Default::default() }); + self.manifest.links.push(LinkAction { + path: path.to_string(), + target: target.to_string(), + properties: Default::default(), + }); self } @@ -211,7 +232,9 @@ impl ManifestBuilder { } /// Start a new empty builder pub fn new() -> Self { - Self { manifest: Manifest::new() } + Self { + manifest: Manifest::new(), + } } /// Convenience: construct a Manifest directly by scanning a prototype directory. @@ -223,9 +246,9 @@ impl ManifestBuilder { proto.display() ))); } - let root = proto - .canonicalize() - .map_err(|e| IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e)))?; + let root = proto.canonicalize().map_err(|e| { + IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e)) + })?; let mut m = Manifest::new(); for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) { @@ -260,10 +283,18 @@ impl ManifestBuilder { if let Some(fmri) = meta.fmri { push_attr("pkg.fmri", fmri.to_string()); } - if let Some(s) = meta.summary { push_attr("pkg.summary", s); } - if let Some(c) = meta.classification { push_attr("info.classification", c); } - if let Some(u) = meta.upstream_url { push_attr("info.upstream-url", u); } - if let Some(su) = meta.source_url { push_attr("info.source-url", su); } + if let Some(s) = meta.summary { + push_attr("pkg.summary", s); + } + if let Some(c) = meta.classification { + push_attr("info.classification", c); + } + if let Some(u) = meta.upstream_url { + push_attr("info.upstream-url", u); + } + if let Some(su) = meta.source_url { + push_attr("info.source-url", su); + } if let Some(l) = meta.license { // Represent base license via an attribute named 'license'; callers may add dedicated license actions separately self.manifest.attributes.push(Attr { @@ -310,12 +341,16 @@ impl Repository { pub fn open(path: &Path) -> Result { // Validate by opening backend let _ = FileBackend::open(path)?; - Ok(Self { path: path.to_path_buf() }) + Ok(Self { + path: path.to_path_buf(), + }) } pub fn create(path: &Path) -> Result { let _ = FileBackend::create(path, RepositoryVersion::default())?; - Ok(Self { path: path.to_path_buf() }) + Ok(Self { + path: path.to_path_buf(), + }) } pub fn has_publisher(&self, name: &str) -> Result { @@ -330,7 +365,9 @@ impl Repository { Ok(()) } - pub fn path(&self) -> &Path { &self.path } + pub fn path(&self) -> &Path { + &self.path + } } /// High-level publishing client for starting repository transactions. @@ -352,14 +389,21 @@ pub struct PublisherClient { impl PublisherClient { pub fn new(repo: Repository, publisher: impl Into) -> Self { - Self { repo, publisher: publisher.into() } + Self { + repo, + publisher: publisher.into(), + } } /// Begin a new transaction pub fn begin(&self) -> Result { let backend = FileBackend::open(self.repo.path())?; let tx = backend.begin_transaction()?; // returns Transaction bound to repo path - Ok(Txn { backend_path: self.repo.path().to_path_buf(), tx, publisher: self.publisher.clone() }) + Ok(Txn { + backend_path: self.repo.path().to_path_buf(), + tx, + publisher: self.publisher.clone(), + }) } } @@ -389,9 +433,9 @@ pub struct Txn { impl Txn { /// Add all files from the given payload/prototype directory pub fn add_payload_dir(&mut self, dir: &Path) -> Result<(), IpsError> { - let root = dir - .canonicalize() - .map_err(|e| IpsError::Io(format!("failed to canonicalize {}: {}", dir.display(), e)))?; + let root = dir.canonicalize().map_err(|e| { + IpsError::Io(format!("failed to canonicalize {}: {}", dir.display(), e)) + })?; for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) { let p = entry.path(); if p.is_file() { @@ -451,7 +495,11 @@ pub struct DependencyGenerator; impl DependencyGenerator { /// Compute file-level dependencies for the given manifest, using `proto` as base for local file resolution. /// This is a helper for callers that want to inspect raw file deps before mapping them to package FMRIs. - pub fn file_deps(proto: &Path, manifest: &Manifest, mut opts: DependGenerateOptions) -> Result, IpsError> { + pub fn file_deps( + proto: &Path, + manifest: &Manifest, + mut opts: DependGenerateOptions, + ) -> Result, IpsError> { if opts.proto_dir.is_none() { opts.proto_dir = Some(proto.to_path_buf()); } @@ -463,7 +511,9 @@ impl DependencyGenerator { /// Intentionally not implemented in this facade: mapping raw file dependencies to package FMRIs /// requires repository/catalog context. Call `generate_with_repo` instead. pub fn generate(_proto: &Path, _manifest: &Manifest) -> Result { - Err(IpsError::Unimplemented { feature: "DependencyGenerator::generate (use generate_with_repo)" }) + Err(IpsError::Unimplemented { + feature: "DependencyGenerator::generate (use generate_with_repo)", + }) } /// Generate dependencies using a repository to resolve file-level deps into package FMRIs. @@ -562,10 +612,8 @@ impl Resolver { if f.version.is_none() { // Query repository for this package name let pkgs = repo.list_packages(publisher, Some(&f.name))?; - let matches: Vec<&crate::repository::PackageInfo> = pkgs - .iter() - .filter(|pi| pi.fmri.name == f.name) - .collect(); + let matches: Vec<&crate::repository::PackageInfo> = + pkgs.iter().filter(|pi| pi.fmri.name == f.name).collect(); if matches.len() == 1 { let fmri = &matches[0].fmri; if f.publisher.is_none() { @@ -597,7 +645,6 @@ fn manifest_fmri(manifest: &Manifest) -> Option { None } - /// Lint facade providing a typed, extensible rule engine with enable/disable controls. /// /// Configure which rules to run, override severities, and pass rule-specific parameters. @@ -618,8 +665,8 @@ pub struct LintConfig { pub reference_repos: Vec, pub rulesets: Vec, // Rule configurability - pub disabled_rules: Vec, // rule IDs to disable - pub enabled_only: Option>, // if Some, only these rule IDs run + pub disabled_rules: Vec, // rule IDs to disable + pub enabled_only: Option>, // if Some, only these rule IDs run pub severity_overrides: std::collections::HashMap, pub rule_params: std::collections::HashMap>, // rule_id -> (key->val) } @@ -638,31 +685,48 @@ pub mod lint { #[derive(Debug, Error, Diagnostic)] pub enum LintIssue { - #[error("Manifest is missing pkg.fmri or it is invalid")] - #[diagnostic(code(ips::lint_error::missing_fmri), help("Add a valid set name=pkg.fmri value=... attribute"))] + #[error("Manifest is missing pkg.fmri or it is invalid")] + #[diagnostic( + code(ips::lint_error::missing_fmri), + help("Add a valid set name=pkg.fmri value=... attribute") + )] MissingOrInvalidFmri, #[error("Manifest has multiple pkg.fmri attributes")] - #[diagnostic(code(ips::lint_error::duplicate_fmri), help("Ensure only one pkg.fmri set action is present"))] + #[diagnostic( + code(ips::lint_error::duplicate_fmri), + help("Ensure only one pkg.fmri set action is present") + )] DuplicateFmri, #[error("Manifest is missing pkg.summary")] - #[diagnostic(code(ips::lint_error::missing_summary), help("Add a set name=pkg.summary value=... attribute"))] + #[diagnostic( + code(ips::lint_error::missing_summary), + help("Add a set name=pkg.summary value=... attribute") + )] MissingSummary, - #[error("Dependency is missing FMRI or name")] - #[diagnostic(code(ips::lint_error::dependency_missing_fmri), help("Each depend action should include a valid fmri (name or full fmri)"))] + #[error("Dependency is missing FMRI or name")] + #[diagnostic( + code(ips::lint_error::dependency_missing_fmri), + help("Each depend action should include a valid fmri (name or full fmri)") + )] DependencyMissingFmri, #[error("Dependency type is missing")] - #[diagnostic(code(ips::lint_error::dependency_missing_type), help("Set depend type (e.g., require, incorporate, optional)"))] + #[diagnostic( + code(ips::lint_error::dependency_missing_type), + help("Set depend type (e.g., require, incorporate, optional)") + )] DependencyMissingType, } pub trait LintRule { fn id(&self) -> &'static str; fn description(&self) -> &'static str; - fn default_severity(&self) -> LintSeverity { LintSeverity::Error } + fn default_severity(&self) -> LintSeverity { + LintSeverity::Error + } /// Run this rule against the manifest. Implementors may ignore `config` (prefix with `_`) if not needed. /// The config carries enable/disable lists, severity overrides and rule-specific parameters for extensibility. fn check(&self, manifest: &Manifest, config: &LintConfig) -> Vec; @@ -670,8 +734,12 @@ pub mod lint { struct RuleManifestFmri; impl LintRule for RuleManifestFmri { - fn id(&self) -> &'static str { "manifest.fmri" } - fn description(&self) -> &'static str { "Validate pkg.fmri presence/uniqueness/parse" } + fn id(&self) -> &'static str { + "manifest.fmri" + } + fn description(&self) -> &'static str { + "Validate pkg.fmri presence/uniqueness/parse" + } fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec { let mut diags = Vec::new(); let mut fmri_attr_count = 0usize; @@ -679,13 +747,21 @@ pub mod lint { for attr in &manifest.attributes { if attr.key == "pkg.fmri" { fmri_attr_count += 1; - if let Some(v) = attr.values.get(0) { fmri_text = Some(v.clone()); } + if let Some(v) = attr.values.get(0) { + fmri_text = Some(v.clone()); + } } } - if fmri_attr_count > 1 { diags.push(miette::Report::new(LintIssue::DuplicateFmri)); } + if fmri_attr_count > 1 { + diags.push(miette::Report::new(LintIssue::DuplicateFmri)); + } match (fmri_attr_count, fmri_text) { (0, _) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)), - (_, Some(txt)) => { if crate::fmri::Fmri::parse(&txt).is_err() { diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)); } }, + (_, Some(txt)) => { + if crate::fmri::Fmri::parse(&txt).is_err() { + diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)); + } + } (_, None) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)), } diags @@ -694,29 +770,47 @@ pub mod lint { struct RuleManifestSummary; impl LintRule for RuleManifestSummary { - fn id(&self) -> &'static str { "manifest.summary" } - fn description(&self) -> &'static str { "Validate pkg.summary presence" } + fn id(&self) -> &'static str { + "manifest.summary" + } + fn description(&self) -> &'static str { + "Validate pkg.summary presence" + } fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec { let mut diags = Vec::new(); let has_summary = manifest .attributes .iter() .any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| !v.trim().is_empty())); - if !has_summary { diags.push(miette::Report::new(LintIssue::MissingSummary)); } + if !has_summary { + diags.push(miette::Report::new(LintIssue::MissingSummary)); + } diags } } struct RuleDependencyFields; impl LintRule for RuleDependencyFields { - fn id(&self) -> &'static str { "depend.fields" } - fn description(&self) -> &'static str { "Validate basic dependency fields" } + fn id(&self) -> &'static str { + "depend.fields" + } + fn description(&self) -> &'static str { + "Validate basic dependency fields" + } fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec { let mut diags = Vec::new(); for dep in &manifest.dependencies { - let fmri_ok = dep.fmri.as_ref().map(|f| !f.name.trim().is_empty()).unwrap_or(false); - if !fmri_ok { diags.push(miette::Report::new(LintIssue::DependencyMissingFmri)); } - if dep.dependency_type.trim().is_empty() { diags.push(miette::Report::new(LintIssue::DependencyMissingType)); } + let fmri_ok = dep + .fmri + .as_ref() + .map(|f| !f.name.trim().is_empty()) + .unwrap_or(false); + if !fmri_ok { + diags.push(miette::Report::new(LintIssue::DependencyMissingFmri)); + } + if dep.dependency_type.trim().is_empty() { + diags.push(miette::Report::new(LintIssue::DependencyMissingType)); + } } diags } @@ -735,7 +829,8 @@ pub mod lint { let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect(); return set.contains(rule_id); } - let disabled: std::collections::HashSet<&str> = cfg.disabled_rules.iter().map(|s| s.as_str()).collect(); + let disabled: std::collections::HashSet<&str> = + cfg.disabled_rules.iter().map(|s| s.as_str()).collect(); !disabled.contains(rule_id) } @@ -751,7 +846,10 @@ pub mod lint { /// assert!(diags.is_empty()); /// # Ok::<(), ips::IpsError>(()) /// ``` - pub fn lint_manifest(manifest: &Manifest, config: &LintConfig) -> Result, IpsError> { + pub fn lint_manifest( + manifest: &Manifest, + config: &LintConfig, + ) -> Result, IpsError> { let mut diags: Vec = Vec::new(); for rule in default_rules().into_iter() { if rule_enabled(rule.id(), config) { @@ -769,7 +867,11 @@ mod tests { fn make_manifest_with_fmri(fmri_str: &str) -> Manifest { let mut m = Manifest::new(); - m.attributes.push(Attr { key: "pkg.fmri".into(), values: vec![fmri_str.to_string()], properties: Default::default() }); + m.attributes.push(Attr { + key: "pkg.fmri".into(), + values: vec![fmri_str.to_string()], + properties: Default::default(), + }); m } @@ -799,14 +901,17 @@ mod tests { let fmri = dep.fmri.as_ref().unwrap(); assert_eq!(fmri.name, "pkgA"); assert_eq!(fmri.publisher.as_deref(), Some("pub")); - assert!(fmri.version.is_some(), "expected version to be filled from provider"); + assert!( + fmri.version.is_some(), + "expected version to be filled from provider" + ); assert_eq!(fmri.version.as_ref().unwrap().to_string(), "1.0"); } #[test] fn resolver_uses_repository_for_provider() { - use crate::repository::file_backend::FileBackend; use crate::repository::RepositoryVersion; + use crate::repository::file_backend::FileBackend; // Create a temporary repository and add a publisher let tmp = tempfile::tempdir().unwrap(); @@ -816,7 +921,11 @@ mod tests { // Publish provider package pkgA@1.0 let mut provider = Manifest::new(); - provider.attributes.push(Attr { key: "pkg.fmri".into(), values: vec!["pkg://pub/pkgA@1.0".to_string()], properties: Default::default() }); + provider.attributes.push(Attr { + key: "pkg.fmri".into(), + values: vec!["pkg://pub/pkgA@1.0".to_string()], + properties: Default::default(), + }); let mut tx = backend.begin_transaction().unwrap(); tx.update_manifest(provider); tx.set_publisher("pub"); @@ -854,8 +963,16 @@ mod tests { #[test] fn lint_accepts_valid_manifest() { let mut m = Manifest::new(); - m.attributes.push(Attr { key: "pkg.fmri".into(), values: vec!["pkg://pub/name@1.0".to_string()], properties: Default::default() }); - m.attributes.push(Attr { key: "pkg.summary".into(), values: vec!["A package".to_string()], properties: Default::default() }); + m.attributes.push(Attr { + key: "pkg.fmri".into(), + values: vec!["pkg://pub/name@1.0".to_string()], + properties: Default::default(), + }); + m.attributes.push(Attr { + key: "pkg.summary".into(), + values: vec!["A package".to_string()], + properties: Default::default(), + }); let cfg = LintConfig::default(); let diags = lint::lint_manifest(&m, &cfg).unwrap(); assert!(diags.is_empty(), "unexpected diags: {:?}", diags); @@ -865,14 +982,22 @@ mod tests { fn lint_disable_summary_rule() { // Manifest with valid fmri but missing summary let mut m = Manifest::new(); - m.attributes.push(Attr { key: "pkg.fmri".into(), values: vec!["pkg://pub/name@1.0".to_string()], properties: Default::default() }); + m.attributes.push(Attr { + key: "pkg.fmri".into(), + values: vec!["pkg://pub/name@1.0".to_string()], + properties: Default::default(), + }); // Disable the summary rule; expect no diagnostics let mut cfg = LintConfig::default(); cfg.disabled_rules = vec!["manifest.summary".to_string()]; let diags = lint::lint_manifest(&m, &cfg).unwrap(); // fmri is valid, dependencies empty, summary rule disabled => no diags - assert!(diags.is_empty(), "expected no diagnostics when summary rule disabled, got: {:?}", diags); + assert!( + diags.is_empty(), + "expected no diagnostics when summary rule disabled, got: {:?}", + diags + ); } #[test] @@ -889,14 +1014,29 @@ mod tests { let m = b.build(); // Validate attributes include fmri and summary - assert!(m.attributes.iter().any(|a| a.key == "pkg.fmri" && a.values.get(0).map(|v| v == &fmri.to_string()).unwrap_or(false))); - assert!(m.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.get(0).map(|v| v == "Summary").unwrap_or(false))); + assert!(m.attributes.iter().any(|a| { + a.key == "pkg.fmri" + && a.values + .get(0) + .map(|v| v == &fmri.to_string()) + .unwrap_or(false) + })); + assert!( + m.attributes.iter().any(|a| a.key == "pkg.summary" + && a.values.get(0).map(|v| v == "Summary").unwrap_or(false)) + ); // Validate license assert_eq!(m.licenses.len(), 1); let lic = &m.licenses[0]; - assert_eq!(lic.properties.get("path").map(|p| p.value.as_str()), Some("LICENSE")); - assert_eq!(lic.properties.get("license").map(|p| p.value.as_str()), Some("MIT")); + assert_eq!( + lic.properties.get("path").map(|p| p.value.as_str()), + Some("LICENSE") + ); + assert_eq!( + lic.properties.get("license").map(|p| p.value.as_str()), + Some("MIT") + ); // Validate link assert_eq!(m.links.len(), 1); diff --git a/libips/src/depend/mod.rs b/libips/src/depend/mod.rs index 0f5b1bd..1be0822 100644 --- a/libips/src/depend/mod.rs +++ b/libips/src/depend/mod.rs @@ -8,9 +8,9 @@ use miette::Diagnostic; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::error::Error as StdError; use std::fs; use std::path::{Path, PathBuf}; -use std::error::Error as StdError; use thiserror::Error; use tracing::{debug, warn}; @@ -27,10 +27,16 @@ pub struct DependError { impl DependError { fn new(message: impl Into) -> Self { - Self { message: message.into(), source: None } + Self { + message: message.into(), + source: None, + } } fn with_source(message: impl Into, source: Box) -> Self { - Self { message: message.into(), source: Some(source) } + Self { + message: message.into(), + source: Some(source), + } } } @@ -84,16 +90,26 @@ pub struct FileDep { } /// Convert manifest file actions into FileDep entries (ELF only for now). -pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &GenerateOptions) -> Result> { +pub fn generate_file_dependencies_from_manifest( + manifest: &Manifest, + opts: &GenerateOptions, +) -> Result> { let mut out = Vec::new(); let bypass = compile_bypass(&opts.bypass_patterns)?; for f in &manifest.files { // Determine installed path (manifests typically do not start with '/'). - let installed_path = if f.path.starts_with('/') { f.path.clone() } else { format!("/{}", f.path) }; + let installed_path = if f.path.starts_with('/') { + f.path.clone() + } else { + format!("/{}", f.path) + }; if should_bypass(&installed_path, &bypass) { - debug!("bypassing dependency generation for {} per patterns", installed_path); + debug!( + "bypassing dependency generation for {} per patterns", + installed_path + ); continue; } @@ -142,16 +158,28 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene // Normalize /bin -> /usr/bin let interp_path = normalize_bin_path(&interp); if !interp_path.starts_with('/') { - warn!("Script shebang for {} specifies non-absolute interpreter: {}", installed_path, interp_path); + warn!( + "Script shebang for {} specifies non-absolute interpreter: {}", + installed_path, interp_path + ); } else { // Derive dir and base name let (dir, base) = split_dir_base(&interp_path); if let Some(dir) = dir { - out.push(FileDep { kind: FileDepKind::Script { base_name: base.to_string(), run_paths: vec![dir.to_string()], installed_path: installed_path.clone() } }); + out.push(FileDep { + kind: FileDepKind::Script { + base_name: base.to_string(), + run_paths: vec![dir.to_string()], + installed_path: installed_path.clone(), + }, + }); // If Python interpreter, perform Python analysis if interp_path.contains("python") { - if let Some((maj, min)) = infer_python_version_from_paths(&installed_path, Some(&interp_path)) { - let mut pydeps = process_python(&bytes, &installed_path, (maj, min), opts); + if let Some((maj, min)) = + infer_python_version_from_paths(&installed_path, Some(&interp_path)) + { + let mut pydeps = + process_python(&bytes, &installed_path, (maj, min), opts); out.append(&mut pydeps); } } @@ -171,7 +199,13 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene if exec_path.starts_with('/') { let (dir, base) = split_dir_base(&exec_path); if let Some(dir) = dir { - out.push(FileDep { kind: FileDepKind::Script { base_name: base.to_string(), run_paths: vec![dir.to_string()], installed_path: installed_path.clone() } }); + out.push(FileDep { + kind: FileDepKind::Script { + base_name: base.to_string(), + run_paths: vec![dir.to_string()], + installed_path: installed_path.clone(), + }, + }); } } } @@ -183,14 +217,19 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene } /// Insert default runpaths into provided runpaths based on PD_DEFAULT_RUNPATH token -fn insert_default_runpath(defaults: &[String], provided: &[String]) -> std::result::Result, DependError> { +fn insert_default_runpath( + defaults: &[String], + provided: &[String], +) -> std::result::Result, DependError> { let mut out = Vec::new(); let mut token_count = 0; for p in provided { if p == PD_DEFAULT_RUNPATH { token_count += 1; if token_count > 1 { - return Err(DependError::new("Multiple PD_DEFAULT_RUNPATH tokens in runpath override")); + return Err(DependError::new( + "Multiple PD_DEFAULT_RUNPATH tokens in runpath override", + )); } out.extend_from_slice(defaults); } else { @@ -208,7 +247,9 @@ fn insert_default_runpath(defaults: &[String], provided: &[String]) -> std::resu fn compile_bypass(patterns: &[String]) -> Result> { let mut out = Vec::new(); for p in patterns { - out.push(Regex::new(p).map_err(|e| DependError::with_source(format!("invalid bypass pattern: {}", p), Box::new(e)))?); + out.push(Regex::new(p).map_err(|e| { + DependError::with_source(format!("invalid bypass pattern: {}", p), Box::new(e)) + })?); } Ok(out) } @@ -259,11 +300,18 @@ fn process_elf(bytes: &[u8], installed_path: &str, opts: &GenerateOptions) -> Ve } } else { // If no override, prefer DT_RUNPATH if present else defaults - if runpaths.is_empty() { defaults.clone() } else { runpaths.clone() } + if runpaths.is_empty() { + defaults.clone() + } else { + runpaths.clone() + } }; // Expand $ORIGIN - let origin = Path::new(installed_path).parent().map(|p| p.display().to_string()).unwrap_or_else(|| "/".to_string()); + let origin = Path::new(installed_path) + .parent() + .map(|p| p.display().to_string()) + .unwrap_or_else(|| "/".to_string()); let expanded: Vec = effective .into_iter() .map(|p| p.replace("$ORIGIN", &origin)) @@ -271,7 +319,13 @@ fn process_elf(bytes: &[u8], installed_path: &str, opts: &GenerateOptions) -> Ve // Emit FileDep for each DT_NEEDED base name for bn in needed.drain(..) { - out.push(FileDep { kind: FileDepKind::Elf { base_name: bn, run_paths: expanded.clone(), installed_path: installed_path.to_string() } }); + out.push(FileDep { + kind: FileDepKind::Elf { + base_name: bn, + run_paths: expanded.clone(), + installed_path: installed_path.to_string(), + }, + }); } } Err(err) => warn!("ELF parse error for {}: {}", installed_path, err), @@ -292,7 +346,11 @@ pub fn resolve_dependencies( for fd in file_deps { match &fd.kind { - FileDepKind::Elf { base_name, run_paths, .. } => { + FileDepKind::Elf { + base_name, + run_paths, + .. + } => { let mut providers: Vec = Vec::new(); for dir in run_paths { let full = normalize_join(dir, base_name); @@ -330,7 +388,11 @@ pub fn resolve_dependencies( // unresolved -> skip for now; future: emit analysis warnings } } - FileDepKind::Script { base_name, run_paths, .. } => { + FileDepKind::Script { + base_name, + run_paths, + .. + } => { let mut providers: Vec = Vec::new(); for dir in run_paths { let full = normalize_join(dir, base_name); @@ -366,7 +428,11 @@ pub fn resolve_dependencies( } else { } } - FileDepKind::Python { base_names, run_paths, .. } => { + FileDepKind::Python { + base_names, + run_paths, + .. + } => { let mut providers: Vec = Vec::new(); for dir in run_paths { for base in base_names { @@ -418,7 +484,10 @@ fn normalize_join(dir: &str, base: &str) -> String { } } -fn build_path_provider_map(repo: &R, publisher: Option<&str>) -> Result>> { +fn build_path_provider_map( + repo: &R, + publisher: Option<&str>, +) -> Result>> { // Ask repo to show contents for all packages (files only) let contents = repo .show_contents(publisher, None, Some(&["file".to_string()])) @@ -429,14 +498,21 @@ fn build_path_provider_map(repo: &R, publisher: Option<&s let fmri = match pc.package_id.parse::() { Ok(f) => f, Err(e) => { - warn!("Skipping package with invalid FMRI {}: {}", pc.package_id, e); + warn!( + "Skipping package with invalid FMRI {}: {}", + pc.package_id, e + ); continue; } }; if let Some(files) = pc.files { for p in files { // Ensure leading slash - let key = if p.starts_with('/') { p } else { format!("/{}", p) }; + let key = if p.starts_with('/') { + p + } else { + format!("/{}", p) + }; map.entry(key).or_default().push(fmri.clone()); } } @@ -444,7 +520,6 @@ fn build_path_provider_map(repo: &R, publisher: Option<&s Ok(map) } - // --- Helpers for script processing --- fn parse_shebang(bytes: &[u8]) -> Option { if bytes.len() < 2 || bytes[0] != b'#' || bytes[1] != b'!' { @@ -501,7 +576,6 @@ fn split_dir_base(path: &str) -> (Option<&str>, &str) { } } - fn looks_like_smf_manifest(bytes: &[u8]) -> bool { // Very lightweight detection: SMF manifests are XML files with a root // We do a lossy UTF-8 conversion and look for the tag to avoid a full XML parser. @@ -510,7 +584,10 @@ fn looks_like_smf_manifest(bytes: &[u8]) -> bool { } // --- Python helpers --- -fn infer_python_version_from_paths(installed_path: &str, shebang_path: Option<&str>) -> Option<(u8, u8)> { +fn infer_python_version_from_paths( + installed_path: &str, + shebang_path: Option<&str>, +) -> Option<(u8, u8)> { // Prefer version implied by installed path under /usr/lib/pythonX.Y if let Ok(re) = Regex::new(r"^/usr/lib/python(\d+)\.(\d+)(/|$)") { if let Some(c) = re.captures(installed_path) { @@ -526,7 +603,9 @@ fn infer_python_version_from_paths(installed_path: &str, shebang_path: Option<&s if let Ok(re) = Regex::new(r"python(\d+)\.(\d+)") { if let Some(c) = re.captures(sb) { if let (Some(ma), Some(mi)) = (c.get(1), c.get(2)) { - if let (Ok(maj), Ok(min)) = (ma.as_str().parse::(), mi.as_str().parse::()) { + if let (Ok(maj), Ok(min)) = + (ma.as_str().parse::(), mi.as_str().parse::()) + { return Some((maj, min)); } } @@ -580,7 +659,12 @@ fn collect_python_imports(src: &str) -> Vec { mods } -fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: &GenerateOptions) -> Vec { +fn process_python( + bytes: &[u8], + installed_path: &str, + version: (u8, u8), + opts: &GenerateOptions, +) -> Vec { let text = String::from_utf8_lossy(bytes); let imports = collect_python_imports(&text); if imports.is_empty() { @@ -591,11 +675,21 @@ fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: & for m in imports { let py = format!("{}.py", m); let so = format!("{}.so", m); - if !base_names.contains(&py) { base_names.push(py); } - if !base_names.contains(&so) { base_names.push(so); } + if !base_names.contains(&py) { + base_names.push(py); + } + if !base_names.contains(&so) { + base_names.push(so); + } } let run_paths = compute_python_runpaths(version, opts); - vec![FileDep { kind: FileDepKind::Python { base_names, run_paths, installed_path: installed_path.to_string() } }] + vec![FileDep { + kind: FileDepKind::Python { + base_names, + run_paths, + installed_path: installed_path.to_string(), + }, + }] } // --- SMF helpers --- @@ -608,7 +702,9 @@ fn extract_smf_execs(bytes: &[u8]) -> Vec { let m = cap.get(1).or_else(|| cap.get(2)); if let Some(v) = m { let val = v.as_str().to_string(); - if !out.contains(&val) { out.push(val); } + if !out.contains(&val) { + out.push(val); + } } } } diff --git a/libips/src/digest/mod.rs b/libips/src/digest/mod.rs index cf8c4bb..bd61cde 100644 --- a/libips/src/digest/mod.rs +++ b/libips/src/digest/mod.rs @@ -129,7 +129,7 @@ impl Digest { x => { return Err(DigestError::UnknownAlgorithm { algorithm: x.to_string(), - }) + }); } }; @@ -152,7 +152,9 @@ pub enum DigestError { #[error("hashing algorithm {algorithm:?} is not known by this library")] #[diagnostic( code(ips::digest_error::unknown_algorithm), - help("Use one of the supported algorithms: sha1, sha256t, sha512t, sha512t_256, sha3256t, sha3512t_256, sha3512t") + help( + "Use one of the supported algorithms: sha1, sha256t, sha512t, sha512t_256, sha3256t, sha3512t_256, sha3512t" + ) )] UnknownAlgorithm { algorithm: String }, diff --git a/libips/src/image/action_plan.rs b/libips/src/image/action_plan.rs index 5dc4ea0..dcd5d4d 100644 --- a/libips/src/image/action_plan.rs +++ b/libips/src/image/action_plan.rs @@ -1,7 +1,7 @@ use std::path::Path; -use crate::actions::executors::{apply_manifest, ApplyOptions, InstallerError}; use crate::actions::Manifest; +use crate::actions::executors::{ApplyOptions, InstallerError, apply_manifest}; use crate::solver::InstallPlan; /// ActionPlan represents a merged list of actions across all manifests @@ -50,12 +50,20 @@ mod tests { #[test] fn build_and_apply_empty_plan_dry_run() { // Empty install plan should produce empty action plan and apply should be no-op. - let plan = SInstallPlan { add: vec![], remove: vec![], update: vec![], reasons: vec![] }; + let plan = SInstallPlan { + add: vec![], + remove: vec![], + update: vec![], + reasons: vec![], + }; let ap = ActionPlan::from_install_plan(&plan); assert!(ap.manifest.directories.is_empty()); assert!(ap.manifest.files.is_empty()); assert!(ap.manifest.links.is_empty()); - let opts = ApplyOptions { dry_run: true, ..Default::default() }; + let opts = ApplyOptions { + dry_run: true, + ..Default::default() + }; let root = Path::new("/tmp/ips_image_test_nonexistent_root"); // Even if root doesn't exist, dry_run should not perform any IO and succeed. let res = ap.apply(root, &opts); diff --git a/libips/src/image/catalog.rs b/libips/src/image/catalog.rs index 988dc6d..3ba992c 100644 --- a/libips/src/image/catalog.rs +++ b/libips/src/image/catalog.rs @@ -1,16 +1,16 @@ -use crate::actions::{Manifest}; +use crate::actions::Manifest; use crate::fmri::Fmri; use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry}; +use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder}; use miette::Diagnostic; use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::fs; +use std::io::{Cursor, Read, Write}; use std::path::{Path, PathBuf}; use thiserror::Error; -use tracing::{info, warn, trace}; -use std::io::{Cursor, Read, Write}; -use lz4::{Decoder as Lz4Decoder, EncoderBuilder as Lz4EncoderBuilder}; -use std::collections::HashMap; +use tracing::{info, trace, warn}; /// Table definition for the catalog database /// Key: stem@version @@ -27,7 +27,6 @@ pub const OBSOLETED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new(" /// Value: version string as bytes (same format as Fmri::version()) pub const INCORPORATE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("incorporate"); - /// Errors that can occur when working with the image catalog #[derive(Error, Debug, Diagnostic)] pub enum CatalogError { @@ -66,8 +65,12 @@ pub type Result = std::result::Result; // Internal helpers for (de)compressing manifest JSON payloads stored in redb fn is_likely_json(bytes: &[u8]) -> bool { let mut i = 0; - while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { i += 1; } - if i >= bytes.len() { return false; } + while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { + i += 1; + } + if i >= bytes.len() { + return false; + } matches!(bytes[i], b'{' | b'[') } @@ -110,10 +113,10 @@ fn decode_manifest_bytes(bytes: &[u8]) -> Result { pub struct PackageInfo { /// The FMRI of the package pub fmri: Fmri, - + /// Whether the package is obsolete pub obsolete: bool, - + /// The publisher of the package pub publisher: String, } @@ -124,7 +127,7 @@ pub struct ImageCatalog { db_path: PathBuf, /// Path to the separate obsoleted database obsoleted_db_path: PathBuf, - + /// Path to the catalog directory catalog_dir: PathBuf, } @@ -138,92 +141,126 @@ impl ImageCatalog { catalog_dir: catalog_dir.as_ref().to_path_buf(), } } - + /// Dump the contents of a specific table to stdout for debugging pub fn dump_table(&self, table_name: &str) -> Result<()> { // Determine which table to dump and open the appropriate database match table_name { "catalog" => { - let db = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; - let tx = db.begin_read() - .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; + let db = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; + let tx = db.begin_read().map_err(|e| { + CatalogError::Database(format!("Failed to begin transaction: {}", e)) + })?; self.dump_catalog_table(&tx)?; } "obsoleted" => { - let db = Database::open(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?; - let tx = db.begin_read() - .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; + let db = Database::open(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted database: {}", e)) + })?; + let tx = db.begin_read().map_err(|e| { + CatalogError::Database(format!("Failed to begin transaction: {}", e)) + })?; self.dump_obsoleted_table(&tx)?; } "incorporate" => { - let db = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; - let tx = db.begin_read() - .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; + let db = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; + let tx = db.begin_read().map_err(|e| { + CatalogError::Database(format!("Failed to begin transaction: {}", e)) + })?; // Simple dump of incorporate locks if let Ok(table) = tx.open_table(INCORPORATE_TABLE) { - for entry in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate incorporate table: {}", e)))? { - let (k, v) = entry.map_err(|e| CatalogError::Database(format!("Failed to read incorporate table entry: {}", e)))?; + for entry in table.iter().map_err(|e| { + CatalogError::Database(format!( + "Failed to iterate incorporate table: {}", + e + )) + })? { + let (k, v) = entry.map_err(|e| { + CatalogError::Database(format!( + "Failed to read incorporate table entry: {}", + e + )) + })?; let stem = k.value(); let ver = String::from_utf8_lossy(v.value()); println!("{} -> {}", stem, ver); } } } - _ => return Err(CatalogError::Database(format!("Unknown table: {}", table_name))), + _ => { + return Err(CatalogError::Database(format!( + "Unknown table: {}", + table_name + ))); + } } - + Ok(()) } - + /// Dump the contents of all tables to stdout for debugging pub fn dump_all_tables(&self) -> Result<()> { // Catalog DB - let db_cat = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; - let tx_cat = db_cat.begin_read() + let db_cat = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; + let tx_cat = db_cat + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; println!("=== CATALOG TABLE ==="); let _ = self.dump_catalog_table(&tx_cat); - + // Obsoleted DB - let db_obs = Database::open(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?; - let tx_obs = db_obs.begin_read() + let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted database: {}", e)) + })?; + let tx_obs = db_obs + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; println!("\n=== OBSOLETED TABLE ==="); let _ = self.dump_obsoleted_table(&tx_obs); - + Ok(()) } - + /// Dump the contents of the catalog table fn dump_catalog_table(&self, tx: &redb::ReadTransaction) -> Result<()> { match tx.open_table(CATALOG_TABLE) { Ok(table) => { let mut count = 0; - for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? { - let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?; + for entry_result in table.iter().map_err(|e| { + CatalogError::Database(format!("Failed to iterate catalog table: {}", e)) + })? { + let (key, value) = entry_result.map_err(|e| { + CatalogError::Database(format!( + "Failed to get entry from catalog table: {}", + e + )) + })?; let key_str = key.value(); - + // Try to deserialize the manifest (supports JSON or LZ4-compressed JSON) match decode_manifest_bytes(value.value()) { Ok(manifest) => { // Extract the publisher from the FMRI attribute - let publisher = manifest.attributes.iter() + let publisher = manifest + .attributes + .iter() .find(|attr| attr.key == "pkg.fmri") .and_then(|attr| attr.values.get(0).cloned()) .unwrap_or_else(|| "unknown".to_string()); - + println!("Key: {}", key_str); println!(" FMRI: {}", publisher); println!(" Attributes: {}", manifest.attributes.len()); println!(" Files: {}", manifest.files.len()); println!(" Directories: {}", manifest.directories.len()); println!(" Dependencies: {}", manifest.dependencies.len()); - }, + } Err(e) => { println!("Key: {}", key_str); println!(" Error deserializing manifest: {}", e); @@ -233,191 +270,265 @@ impl ImageCatalog { } println!("Total entries in catalog table: {}", count); Ok(()) - }, + } Err(e) => { println!("Error opening catalog table: {}", e); - Err(CatalogError::Database(format!("Failed to open catalog table: {}", e))) + Err(CatalogError::Database(format!( + "Failed to open catalog table: {}", + e + ))) } } } - + /// Dump the contents of the obsoleted table fn dump_obsoleted_table(&self, tx: &redb::ReadTransaction) -> Result<()> { match tx.open_table(OBSOLETED_TABLE) { Ok(table) => { let mut count = 0; - for entry_result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? { - let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?; + for entry_result in table.iter().map_err(|e| { + CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)) + })? { + let (key, _) = entry_result.map_err(|e| { + CatalogError::Database(format!( + "Failed to get entry from obsoleted table: {}", + e + )) + })?; let key_str = key.value(); - + println!("Key: {}", key_str); count += 1; } println!("Total entries in obsoleted table: {}", count); Ok(()) - }, + } Err(e) => { println!("Error opening obsoleted table: {}", e); - Err(CatalogError::Database(format!("Failed to open obsoleted table: {}", e))) + Err(CatalogError::Database(format!( + "Failed to open obsoleted table: {}", + e + ))) } } } - - + /// Get database statistics pub fn get_db_stats(&self) -> Result<()> { // Open the catalog database - let db_cat = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; - let tx_cat = db_cat.begin_read() + let db_cat = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; + let tx_cat = db_cat + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Open the obsoleted database - let db_obs = Database::open(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?; - let tx_obs = db_obs.begin_read() + let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted database: {}", e)) + })?; + let tx_obs = db_obs + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Get table statistics let mut catalog_count = 0; let mut obsoleted_count = 0; - + // Count catalog entries if let Ok(table) = tx_cat.open_table(CATALOG_TABLE) { - for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? { - let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?; + for result in table.iter().map_err(|e| { + CatalogError::Database(format!("Failed to iterate catalog table: {}", e)) + })? { + let _ = result.map_err(|e| { + CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)) + })?; catalog_count += 1; } } - + // Count obsoleted entries (separate DB) if let Ok(table) = tx_obs.open_table(OBSOLETED_TABLE) { - for result in table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? { - let _ = result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?; + for result in table.iter().map_err(|e| { + CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)) + })? { + let _ = result.map_err(|e| { + CatalogError::Database(format!( + "Failed to get entry from obsoleted table: {}", + e + )) + })?; obsoleted_count += 1; } } - + // Print statistics println!("Catalog database path: {}", self.db_path.display()); - println!("Obsoleted database path: {}", self.obsoleted_db_path.display()); + println!( + "Obsoleted database path: {}", + self.obsoleted_db_path.display() + ); println!("Catalog directory: {}", self.catalog_dir.display()); println!("Table statistics:"); println!(" Catalog table: {} entries", catalog_count); println!(" Obsoleted table: {} entries", obsoleted_count); println!("Total entries: {}", catalog_count + obsoleted_count); - + Ok(()) } - + /// Initialize the catalog database pub fn init_db(&self) -> Result<()> { // Ensure parent directories exist - if let Some(parent) = self.db_path.parent() { fs::create_dir_all(parent)?; } - if let Some(parent) = self.obsoleted_db_path.parent() { fs::create_dir_all(parent)?; } - + if let Some(parent) = self.db_path.parent() { + fs::create_dir_all(parent)?; + } + if let Some(parent) = self.obsoleted_db_path.parent() { + fs::create_dir_all(parent)?; + } + // Create/open catalog database and tables - let db_cat = Database::create(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to create catalog database: {}", e)))?; - let tx_cat = db_cat.begin_write() + let db_cat = Database::create(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to create catalog database: {}", e)) + })?; + let tx_cat = db_cat + .begin_write() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - tx_cat.open_table(CATALOG_TABLE) - .map_err(|e| CatalogError::Database(format!("Failed to create catalog table: {}", e)))?; - tx_cat.open_table(INCORPORATE_TABLE) - .map_err(|e| CatalogError::Database(format!("Failed to create incorporate table: {}", e)))?; - tx_cat.commit() - .map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?; - + tx_cat.open_table(CATALOG_TABLE).map_err(|e| { + CatalogError::Database(format!("Failed to create catalog table: {}", e)) + })?; + tx_cat.open_table(INCORPORATE_TABLE).map_err(|e| { + CatalogError::Database(format!("Failed to create incorporate table: {}", e)) + })?; + tx_cat.commit().map_err(|e| { + CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)) + })?; + // Create/open obsoleted database and table - let db_obs = Database::create(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to create obsoleted database: {}", e)))?; - let tx_obs = db_obs.begin_write() + let db_obs = Database::create(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to create obsoleted database: {}", e)) + })?; + let tx_obs = db_obs + .begin_write() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - tx_obs.open_table(OBSOLETED_TABLE) - .map_err(|e| CatalogError::Database(format!("Failed to create obsoleted table: {}", e)))?; - tx_obs.commit() - .map_err(|e| CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)))?; - + tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| { + CatalogError::Database(format!("Failed to create obsoleted table: {}", e)) + })?; + tx_obs.commit().map_err(|e| { + CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)) + })?; + Ok(()) } - + /// Build the catalog from downloaded catalogs pub fn build_catalog(&self, publishers: &[String]) -> Result<()> { info!("Building catalog (publishers: {})", publishers.len()); trace!("Catalog directory: {:?}", self.catalog_dir); trace!("Catalog database path: {:?}", self.db_path); - + if publishers.is_empty() { return Err(CatalogError::NoPublishers); } - + // Open the databases - trace!("Opening databases at {:?} and {:?}", self.db_path, self.obsoleted_db_path); - let db_cat = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; - let db_obs = Database::open(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?; - + trace!( + "Opening databases at {:?} and {:?}", + self.db_path, self.obsoleted_db_path + ); + let db_cat = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; + let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted database: {}", e)) + })?; + // Begin writing transactions trace!("Beginning write transactions"); - let tx_cat = db_cat.begin_write() - .map_err(|e| CatalogError::Database(format!("Failed to begin catalog transaction: {}", e)))?; - let tx_obs = db_obs.begin_write() - .map_err(|e| CatalogError::Database(format!("Failed to begin obsoleted transaction: {}", e)))?; - + let tx_cat = db_cat.begin_write().map_err(|e| { + CatalogError::Database(format!("Failed to begin catalog transaction: {}", e)) + })?; + let tx_obs = db_obs.begin_write().map_err(|e| { + CatalogError::Database(format!("Failed to begin obsoleted transaction: {}", e)) + })?; + // Open the catalog table trace!("Opening catalog table"); - let mut catalog_table = tx_cat.open_table(CATALOG_TABLE) + let mut catalog_table = tx_cat + .open_table(CATALOG_TABLE) .map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?; - + // Open the obsoleted table trace!("Opening obsoleted table"); - let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?; - + let mut obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted table: {}", e)) + })?; + // Process each publisher for publisher in publishers { trace!("Processing publisher: {}", publisher); let publisher_catalog_dir = self.catalog_dir.join(publisher); trace!("Publisher catalog directory: {:?}", publisher_catalog_dir); - + // Skip if the publisher catalog directory doesn't exist if !publisher_catalog_dir.exists() { - warn!("Publisher catalog directory not found: {}", publisher_catalog_dir.display()); + warn!( + "Publisher catalog directory not found: {}", + publisher_catalog_dir.display() + ); continue; } - + // Determine where catalog parts live. Support both legacy nested layout // (publisher//catalog) and flat layout (directly under publisher dir). - let nested_dir = publisher_catalog_dir.join("publisher").join(publisher).join("catalog"); + let nested_dir = publisher_catalog_dir + .join("publisher") + .join(publisher) + .join("catalog"); let flat_dir = publisher_catalog_dir.clone(); - let catalog_parts_dir = if nested_dir.exists() { &nested_dir } else { &flat_dir }; + let catalog_parts_dir = if nested_dir.exists() { + &nested_dir + } else { + &flat_dir + }; trace!("Creating catalog manager for publisher: {}", publisher); trace!("Catalog parts directory: {:?}", catalog_parts_dir); // Check if the catalog parts directory exists (either layout) if !catalog_parts_dir.exists() { - warn!("Catalog parts directory not found: {}", catalog_parts_dir.display()); + warn!( + "Catalog parts directory not found: {}", + catalog_parts_dir.display() + ); continue; } - let mut catalog_manager = CatalogManager::new(catalog_parts_dir, publisher) - .map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to create catalog manager: {}", e))))?; - + let mut catalog_manager = + CatalogManager::new(catalog_parts_dir, publisher).map_err(|e| { + CatalogError::Repository(crate::repository::RepositoryError::Other(format!( + "Failed to create catalog manager: {}", + e + ))) + })?; + // Get all catalog parts trace!("Getting catalog parts for publisher: {}", publisher); let parts = catalog_manager.attrs().parts.clone(); trace!("Catalog parts: {:?}", parts.keys().collect::>()); - + // Load all catalog parts for part_name in parts.keys() { trace!("Loading catalog part: {}", part_name); - catalog_manager.load_part(part_name) - .map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to load catalog part: {}", e))))?; + catalog_manager.load_part(part_name).map_err(|e| { + CatalogError::Repository(crate::repository::RepositoryError::Other(format!( + "Failed to load catalog part: {}", + e + ))) + })?; } - + // New approach: Merge information across all catalog parts per stem@version, then process once let mut loaded_parts: Vec<&CatalogPart> = Vec::new(); for part_name in parts.keys() { @@ -425,23 +536,30 @@ impl ImageCatalog { loaded_parts.push(part); } } - self.process_publisher_merged(&mut catalog_table, &mut obsoleted_table, publisher, &loaded_parts)?; + self.process_publisher_merged( + &mut catalog_table, + &mut obsoleted_table, + publisher, + &loaded_parts, + )?; } - + // Drop the tables to release the borrow on tx drop(catalog_table); drop(obsoleted_table); - + // Commit the transactions - tx_cat.commit() - .map_err(|e| CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)))?; - tx_obs.commit() - .map_err(|e| CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)))?; - + tx_cat.commit().map_err(|e| { + CatalogError::Database(format!("Failed to commit catalog transaction: {}", e)) + })?; + tx_obs.commit().map_err(|e| { + CatalogError::Database(format!("Failed to commit obsoleted transaction: {}", e)) + })?; + info!("Catalog built successfully"); Ok(()) } - + /// Process a catalog part and add its packages to the catalog #[allow(dead_code)] fn process_catalog_part( @@ -453,7 +571,7 @@ impl ImageCatalog { publisher: &str, ) -> Result<()> { trace!("Processing catalog part for publisher: {}", publisher); - + // Get packages for this publisher if let Some(publisher_packages) = part.packages.get(publisher) { let total_versions: usize = publisher_packages.values().map(|v| v.len()).sum(); @@ -469,15 +587,22 @@ impl ImageCatalog { total_versions, publisher ); - + // Process each package stem for (stem, versions) in publisher_packages { - trace!("Processing package stem: {} ({} versions)", stem, versions.len()); - + trace!( + "Processing package stem: {} ({} versions)", + stem, + versions.len() + ); + // Process each package version for version_entry in versions { - trace!("Processing version: {} | actions: {:?}", version_entry.version, version_entry.actions); - + trace!( + "Processing version: {} | actions: {:?}", + version_entry.version, version_entry.actions + ); + // Create the FMRI let version = if !version_entry.version.is_empty() { match crate::fmri::Version::parse(&version_entry.version) { @@ -490,7 +615,7 @@ impl ImageCatalog { } else { None }; - + let fmri = Fmri::with_publisher(publisher, stem, version); let catalog_key = format!("{}@{}", stem, version_entry.version); let obsoleted_key = fmri.to_string(); @@ -499,52 +624,70 @@ impl ImageCatalog { // obsolete in an earlier part (present in obsoleted_table) and is NOT present // in the catalog_table, skip importing it from this part. if !part_name.contains(".base") { - let has_catalog = matches!(catalog_table.get(catalog_key.as_str()), Ok(Some(_))); + let has_catalog = + matches!(catalog_table.get(catalog_key.as_str()), Ok(Some(_))); if !has_catalog { - let was_obsoleted = matches!(obsoleted_table.get(obsoleted_key.as_str()), Ok(Some(_))); + let was_obsoleted = + matches!(obsoleted_table.get(obsoleted_key.as_str()), Ok(Some(_))); if was_obsoleted { // Count as obsolete for progress accounting, even though we skip processing obsolete_count_incl_skipped += 1; skipped_obsolete += 1; trace!( "Skipping {} from part {} because it is marked obsolete and not present in catalog", - obsoleted_key, - part_name + obsoleted_key, part_name ); continue; } } } - + // Check if we already have this package in the catalog let existing_manifest = match catalog_table.get(catalog_key.as_str()) { Ok(Some(bytes)) => Some(decode_manifest_bytes(bytes.value())?), _ => None, }; - + // Create or update the manifest - let manifest = self.create_or_update_manifest(existing_manifest, version_entry, stem, publisher)?; - + let manifest = self.create_or_update_manifest( + existing_manifest, + version_entry, + stem, + publisher, + )?; + // Check if the package is obsolete let is_obsolete = self.is_package_obsolete(&manifest); - if is_obsolete { obsolete_count_incl_skipped += 1; } - + if is_obsolete { + obsolete_count_incl_skipped += 1; + } + // Serialize the manifest let manifest_bytes = serde_json::to_vec(&manifest)?; - + // Store the package in the appropriate table if is_obsolete { // Store obsolete packages in the obsoleted table with the full FMRI as key let empty_bytes: &[u8] = &[0u8; 0]; obsoleted_table .insert(obsoleted_key.as_str(), empty_bytes) - .map_err(|e| CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)))?; + .map_err(|e| { + CatalogError::Database(format!( + "Failed to insert into obsoleted table: {}", + e + )) + })?; } else { // Store non-obsolete packages in the catalog table with stem@version as a key let compressed = compress_json_lz4(&manifest_bytes)?; catalog_table .insert(catalog_key.as_str(), compressed.as_slice()) - .map_err(|e| CatalogError::Database(format!("Failed to insert into catalog table: {}", e)))?; + .map_err(|e| { + CatalogError::Database(format!( + "Failed to insert into catalog table: {}", + e + )) + })?; } processed += 1; @@ -565,19 +708,15 @@ impl ImageCatalog { // Final summary for this part/publisher info!( "Finished import for publisher {}, part {}: {} versions processed ({} obsolete incl. skipped, {} skipped)", - publisher, - part_name, - processed, - obsolete_count_incl_skipped, - skipped_obsolete + publisher, part_name, processed, obsolete_count_incl_skipped, skipped_obsolete ); } else { trace!("No packages found for publisher: {}", publisher); } - + Ok(()) } - + /// Process all catalog parts by merging entries per stem@version and deciding once per package fn process_publisher_merged( &self, @@ -596,16 +735,19 @@ impl ImageCatalog { for (stem, versions) in publisher_packages { let stem_map = merged.entry(stem.clone()).or_default(); for v in versions { - let entry = stem_map - .entry(v.version.clone()) - .or_insert(PackageVersionEntry { - version: v.version.clone(), - actions: None, - signature_sha1: None, - }); + let entry = + stem_map + .entry(v.version.clone()) + .or_insert(PackageVersionEntry { + version: v.version.clone(), + actions: None, + signature_sha1: None, + }); // Merge signature if not yet set if entry.signature_sha1.is_none() { - if let Some(sig) = &v.signature_sha1 { entry.signature_sha1 = Some(sig.clone()); } + if let Some(sig) = &v.signature_sha1 { + entry.signature_sha1 = Some(sig.clone()); + } } // Merge actions, de-duplicating if let Some(actions) = &v.actions { @@ -649,40 +791,55 @@ impl ImageCatalog { }; // Build/update manifest with merged actions - let manifest = self.create_or_update_manifest(existing_manifest, entry, stem, publisher)?; + let manifest = + self.create_or_update_manifest(existing_manifest, entry, stem, publisher)?; // Obsolete decision based on merged actions in manifest let is_obsolete = self.is_package_obsolete(&manifest); - if is_obsolete { obsolete_count += 1; } + if is_obsolete { + obsolete_count += 1; + } // Serialize and write if is_obsolete { // Compute full FMRI for obsoleted key let version_obj = if !entry.version.is_empty() { - match crate::fmri::Version::parse(&entry.version) { Ok(v) => Some(v), Err(_) => None } - } else { None }; + match crate::fmri::Version::parse(&entry.version) { + Ok(v) => Some(v), + Err(_) => None, + } + } else { + None + }; let fmri = Fmri::with_publisher(publisher, stem, version_obj); let obsoleted_key = fmri.to_string(); let empty_bytes: &[u8] = &[0u8; 0]; obsoleted_table .insert(obsoleted_key.as_str(), empty_bytes) - .map_err(|e| CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)))?; + .map_err(|e| { + CatalogError::Database(format!( + "Failed to insert into obsoleted table: {}", + e + )) + })?; } else { let manifest_bytes = serde_json::to_vec(&manifest)?; let compressed = compress_json_lz4(&manifest_bytes)?; catalog_table .insert(catalog_key.as_str(), compressed.as_slice()) - .map_err(|e| CatalogError::Database(format!("Failed to insert into catalog table: {}", e)))?; + .map_err(|e| { + CatalogError::Database(format!( + "Failed to insert into catalog table: {}", + e + )) + })?; } processed += 1; if processed % progress_step == 0 { info!( "Import progress (publisher {}, merged): {}/{} versions processed ({} obsolete)", - publisher, - processed, - total_versions, - obsolete_count + publisher, processed, total_versions, obsolete_count ); } } @@ -691,14 +848,12 @@ impl ImageCatalog { info!( "Finished merged import for publisher {}: {} versions processed ({} obsolete)", - publisher, - processed, - obsolete_count + publisher, processed, obsolete_count ); Ok(()) } - + /// Create or update a manifest from a package version entry fn create_or_update_manifest( &self, @@ -709,7 +864,7 @@ impl ImageCatalog { ) -> Result { // Start with the existing manifest or create a new one let mut manifest = existing_manifest.unwrap_or_else(Manifest::new); - + // Parse and add actions from the version entry if let Some(actions) = &version_entry.actions { for action_str in actions { @@ -720,19 +875,20 @@ impl ImageCatalog { if name_part.starts_with("name=") { // Extract the key (after "name=") let key = &name_part[5..]; - + // Extract the value (after "value=") if let Some(value_part) = action_str.split_whitespace().nth(2) { if value_part.starts_with("value=") { let mut value = &value_part[6..]; - + // Remove quotes if present if value.starts_with('"') && value.ends_with('"') { - value = &value[1..value.len()-1]; + value = &value[1..value.len() - 1]; } - + // Add or update the attribute in the manifest - let attr_index = manifest.attributes.iter().position(|attr| attr.key == key); + let attr_index = + manifest.attributes.iter().position(|attr| attr.key == key); if let Some(index) = attr_index { manifest.attributes[index].values = vec![value.to_string()]; } else { @@ -758,10 +914,14 @@ impl ImageCatalog { match k { "type" => dep_type = v.to_string(), "predicate" => { - if let Ok(f) = crate::fmri::Fmri::parse(v) { dep_predicate = Some(f); } + if let Ok(f) = crate::fmri::Fmri::parse(v) { + dep_predicate = Some(f); + } } "fmri" => { - if let Ok(f) = crate::fmri::Fmri::parse(v) { dep_fmris.push(f); } + if let Ok(f) = crate::fmri::Fmri::parse(v) { + dep_fmris.push(f); + } } "root-image" => { root_image = v.to_string(); @@ -783,7 +943,7 @@ impl ImageCatalog { } } } - + // Ensure the manifest has the correct FMRI attribute // Create a Version object from the version string let version = if !version_entry.version.is_empty() { @@ -792,28 +952,32 @@ impl ImageCatalog { Err(e) => { // Map the FmriError to a CatalogError return Err(CatalogError::Repository( - crate::repository::RepositoryError::Other( - format!("Invalid version format: {}", e) - ) + crate::repository::RepositoryError::Other(format!( + "Invalid version format: {}", + e + )), )); } } } else { None }; - + // Create the FMRI with publisher, stem, and version let fmri = Fmri::with_publisher(publisher, stem, version); self.ensure_fmri_attribute(&mut manifest, &fmri); - + Ok(manifest) } - + /// Ensure the manifest has the correct FMRI attribute fn ensure_fmri_attribute(&self, manifest: &mut Manifest, fmri: &Fmri) { // Check if the manifest already has an FMRI attribute - let has_fmri = manifest.attributes.iter().any(|attr| attr.key == "pkg.fmri"); - + let has_fmri = manifest + .attributes + .iter() + .any(|attr| attr.key == "pkg.fmri"); + // If not, add it if !has_fmri { let mut attr = crate::actions::Attr::default(); @@ -822,65 +986,77 @@ impl ImageCatalog { manifest.attributes.push(attr); } } - + /// Check if a package is obsolete fn is_package_obsolete(&self, manifest: &Manifest) -> bool { manifest.attributes.iter().any(|attr| { attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true") }) } - + /// Query the catalog for packages matching a pattern pub fn query_packages(&self, pattern: Option<&str>) -> Result> { // Open the catalog database - let db_cat = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; + let db_cat = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; // Begin a read transaction - let tx_cat = db_cat.begin_read() + let tx_cat = db_cat + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Open the catalog table - let catalog_table = tx_cat.open_table(CATALOG_TABLE) + let catalog_table = tx_cat + .open_table(CATALOG_TABLE) .map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?; - + // Open the obsoleted database - let db_obs = Database::open(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?; - let tx_obs = db_obs.begin_read() + let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted database: {}", e)) + })?; + let tx_obs = db_obs + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?; - + let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted table: {}", e)) + })?; + let mut results = Vec::new(); - + // Process the catalog table (non-obsolete packages) // Iterate through all entries in the table - for entry_result in catalog_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? { - let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?; + for entry_result in catalog_table.iter().map_err(|e| { + CatalogError::Database(format!("Failed to iterate catalog table: {}", e)) + })? { + let (key, value) = entry_result.map_err(|e| { + CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)) + })?; let key_str = key.value(); - + // Skip if the key doesn't match the pattern if let Some(pattern) = pattern { if !key_str.contains(pattern) { continue; } } - + // Parse the key to get stem and version let parts: Vec<&str> = key_str.split('@').collect(); if parts.len() != 2 { warn!("Invalid key format: {}", key_str); continue; } - + let stem = parts[0]; let version = parts[1]; - + // Deserialize the manifest let manifest: Manifest = decode_manifest_bytes(value.value())?; - + // Extract the publisher from the FMRI attribute - let publisher = manifest.attributes.iter() + let publisher = manifest + .attributes + .iter() .find(|attr| attr.key == "pkg.fmri") .map(|attr| { if let Some(fmri_str) = attr.values.get(0) { @@ -894,7 +1070,7 @@ impl ImageCatalog { } }) .unwrap_or_else(|| "unknown".to_string()); - + // Create a Version object from the version string let version_obj = if !version.is_empty() { match crate::fmri::Version::parse(version) { @@ -904,10 +1080,10 @@ impl ImageCatalog { } else { None }; - + // Create the FMRI with publisher, stem, and version let fmri = Fmri::with_publisher(&publisher, stem, version_obj); - + // Add to results (non-obsolete) results.push(PackageInfo { fmri, @@ -915,71 +1091,87 @@ impl ImageCatalog { publisher, }); } - + // Process the obsoleted table (obsolete packages) // Iterate through all entries in the table - for entry_result in obsoleted_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? { - let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?; + for entry_result in obsoleted_table.iter().map_err(|e| { + CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)) + })? { + let (key, _) = entry_result.map_err(|e| { + CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)) + })?; let key_str = key.value(); - + // Skip if the key doesn't match the pattern if let Some(pattern) = pattern { if !key_str.contains(pattern) { continue; } } - + // Parse the key to get the FMRI match Fmri::parse(key_str) { Ok(fmri) => { // Extract the publisher - let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string()); - + let publisher = fmri + .publisher + .clone() + .unwrap_or_else(|| "unknown".to_string()); + // Add to results (obsolete) results.push(PackageInfo { fmri, obsolete: true, publisher, }); - }, + } Err(e) => { - warn!("Failed to parse FMRI from obsoleted table key: {}: {}", key_str, e); + warn!( + "Failed to parse FMRI from obsoleted table key: {}: {}", + key_str, e + ); continue; } } } - + Ok(results) } - + /// Get a manifest from the catalog pub fn get_manifest(&self, fmri: &Fmri) -> Result> { // Open the catalog database - let db_cat = Database::open(&self.db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open catalog database: {}", e)))?; + let db_cat = Database::open(&self.db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open catalog database: {}", e)) + })?; // Begin a read transaction - let tx_cat = db_cat.begin_read() + let tx_cat = db_cat + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Open the catalog table - let catalog_table = tx_cat.open_table(CATALOG_TABLE) + let catalog_table = tx_cat + .open_table(CATALOG_TABLE) .map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?; - + // Create the key for the catalog table (stem@version) let catalog_key = format!("{}@{}", fmri.stem(), fmri.version()); - + // Try to get the manifest from the catalog table if let Ok(Some(bytes)) = catalog_table.get(catalog_key.as_str()) { return Ok(Some(decode_manifest_bytes(bytes.value())?)); } - + // If not found in catalog DB, check obsoleted DB - let db_obs = Database::open(&self.obsoleted_db_path) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted database: {}", e)))?; - let tx_obs = db_obs.begin_read() + let db_obs = Database::open(&self.obsoleted_db_path).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted database: {}", e)) + })?; + let tx_obs = db_obs + .begin_read() .map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?; - let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE) - .map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?; + let obsoleted_table = tx_obs.open_table(OBSOLETED_TABLE).map_err(|e| { + CatalogError::Database(format!("Failed to open obsoleted table: {}", e)) + })?; let obsoleted_key = fmri.to_string(); if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) { let mut manifest = Manifest::new(); @@ -995,4 +1187,4 @@ impl ImageCatalog { } Ok(None) } -} \ No newline at end of file +} diff --git a/libips/src/image/installed.rs b/libips/src/image/installed.rs index cab432d..ba49866 100644 --- a/libips/src/image/installed.rs +++ b/libips/src/image/installed.rs @@ -46,7 +46,7 @@ pub type Result = std::result::Result; pub struct InstalledPackageInfo { /// The FMRI of the package pub fmri: Fmri, - + /// The publisher of the package pub publisher: String, } @@ -68,48 +68,58 @@ impl InstalledPackages { // To fix this issue, we use block scopes {} around table operations to ensure that the table // objects are dropped (and their borrows released) before committing the transaction. // This pattern is used in all methods that commit transactions after table operations. - + /// Create a new installed packages database pub fn new>(db_path: P) -> Self { InstalledPackages { db_path: db_path.as_ref().to_path_buf(), } } - + /// Dump the contents of the installed table to stdout for debugging pub fn dump_installed_table(&self) -> Result<()> { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a read transaction - let tx = db.begin_read() + let tx = db + .begin_read() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Open the installed table match tx.open_table(INSTALLED_TABLE) { Ok(table) => { let mut count = 0; - for entry_result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? { - let (key, value) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?; + for entry_result in table.iter().map_err(|e| { + InstalledError::Database(format!("Failed to iterate installed table: {}", e)) + })? { + let (key, value) = entry_result.map_err(|e| { + InstalledError::Database(format!( + "Failed to get entry from installed table: {}", + e + )) + })?; let key_str = key.value(); - + // Try to deserialize the manifest match serde_json::from_slice::(value.value()) { Ok(manifest) => { // Extract the publisher from the FMRI attribute - let publisher = manifest.attributes.iter() + let publisher = manifest + .attributes + .iter() .find(|attr| attr.key == "pkg.fmri") .and_then(|attr| attr.values.get(0).cloned()) .unwrap_or_else(|| "unknown".to_string()); - + println!("Key: {}", key_str); println!(" FMRI: {}", publisher); println!(" Attributes: {}", manifest.attributes.len()); println!(" Files: {}", manifest.files.len()); println!(" Directories: {}", manifest.directories.len()); println!(" Dependencies: {}", manifest.dependencies.len()); - }, + } Err(e) => { println!("Key: {}", key_str); println!(" Error deserializing manifest: {}", e); @@ -119,214 +129,252 @@ impl InstalledPackages { } println!("Total entries in installed table: {}", count); Ok(()) - }, + } Err(e) => { println!("Error opening installed table: {}", e); - Err(InstalledError::Database(format!("Failed to open installed table: {}", e))) + Err(InstalledError::Database(format!( + "Failed to open installed table: {}", + e + ))) } } } - + /// Get database statistics pub fn get_db_stats(&self) -> Result<()> { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a read transaction - let tx = db.begin_read() + let tx = db + .begin_read() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Get table statistics let mut installed_count = 0; - + // Count installed entries if let Ok(table) = tx.open_table(INSTALLED_TABLE) { - for result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? { - let _ = result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?; + for result in table.iter().map_err(|e| { + InstalledError::Database(format!("Failed to iterate installed table: {}", e)) + })? { + let _ = result.map_err(|e| { + InstalledError::Database(format!( + "Failed to get entry from installed table: {}", + e + )) + })?; installed_count += 1; } } - + // Print statistics println!("Database path: {}", self.db_path.display()); println!("Table statistics:"); println!(" Installed table: {} entries", installed_count); println!("Total entries: {}", installed_count); - + Ok(()) } - + /// Initialize the installed packages database pub fn init_db(&self) -> Result<()> { // Create a parent directory if it doesn't exist if let Some(parent) = self.db_path.parent() { fs::create_dir_all(parent)?; } - + // Open or create the database let db = Database::create(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?; - + // Create tables - let tx = db.begin_write() + let tx = db + .begin_write() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - - tx.open_table(INSTALLED_TABLE) - .map_err(|e| InstalledError::Database(format!("Failed to create installed table: {}", e)))?; - - tx.commit() - .map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?; - + + tx.open_table(INSTALLED_TABLE).map_err(|e| { + InstalledError::Database(format!("Failed to create installed table: {}", e)) + })?; + + tx.commit().map_err(|e| { + InstalledError::Database(format!("Failed to commit transaction: {}", e)) + })?; + Ok(()) } - + /// Add a package to the installed packages database pub fn add_package(&self, fmri: &Fmri, manifest: &Manifest) -> Result<()> { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a writing transaction - let tx = db.begin_write() + let tx = db + .begin_write() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Create the key (full FMRI including publisher) let key = fmri.to_string(); - + // Serialize the manifest let manifest_bytes = serde_json::to_vec(manifest)?; - + // Use a block scope to ensure the table is dropped before committing the transaction { // Open the installed table - let mut installed_table = tx.open_table(INSTALLED_TABLE) - .map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; - + let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| { + InstalledError::Database(format!("Failed to open installed table: {}", e)) + })?; + // Insert the package into the installed table - installed_table.insert(key.as_str(), manifest_bytes.as_slice()) - .map_err(|e| InstalledError::Database(format!("Failed to insert into installed table: {}", e)))?; - + installed_table + .insert(key.as_str(), manifest_bytes.as_slice()) + .map_err(|e| { + InstalledError::Database(format!( + "Failed to insert into installed table: {}", + e + )) + })?; + // The table is dropped at the end of this block, releasing its borrow of tx } - + // Commit the transaction - tx.commit() - .map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?; - + tx.commit().map_err(|e| { + InstalledError::Database(format!("Failed to commit transaction: {}", e)) + })?; + info!("Added package to installed database: {}", key); Ok(()) } - + /// Remove a package from the installed packages database pub fn remove_package(&self, fmri: &Fmri) -> Result<()> { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a writing transaction - let tx = db.begin_write() + let tx = db + .begin_write() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Create the key (full FMRI including publisher) let key = fmri.to_string(); - + // Use a block scope to ensure the table is dropped before committing the transaction { // Open the installed table - let mut installed_table = tx.open_table(INSTALLED_TABLE) - .map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; - + let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| { + InstalledError::Database(format!("Failed to open installed table: {}", e)) + })?; + // Check if the package exists if let Ok(None) = installed_table.get(key.as_str()) { return Err(InstalledError::PackageNotFound(key)); } - + // Remove the package from the installed table - installed_table.remove(key.as_str()) - .map_err(|e| InstalledError::Database(format!("Failed to remove from installed table: {}", e)))?; - + installed_table.remove(key.as_str()).map_err(|e| { + InstalledError::Database(format!("Failed to remove from installed table: {}", e)) + })?; + // The table is dropped at the end of this block, releasing its borrow of tx } - + // Commit the transaction - tx.commit() - .map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?; - + tx.commit().map_err(|e| { + InstalledError::Database(format!("Failed to commit transaction: {}", e)) + })?; + info!("Removed package from installed database: {}", key); Ok(()) } - + /// Query the installed packages database for packages matching a pattern pub fn query_packages(&self, pattern: Option<&str>) -> Result> { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a read transaction - let tx = db.begin_read() + let tx = db + .begin_read() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Use a block scope to ensure the table is dropped when no longer needed let results = { // Open the installed table - let installed_table = tx.open_table(INSTALLED_TABLE) - .map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; - + let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| { + InstalledError::Database(format!("Failed to open installed table: {}", e)) + })?; + let mut results = Vec::new(); - + // Process the installed table // Iterate through all entries in the table - for entry_result in installed_table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? { - let (key, _) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", e)))?; + for entry_result in installed_table.iter().map_err(|e| { + InstalledError::Database(format!("Failed to iterate installed table: {}", e)) + })? { + let (key, _) = entry_result.map_err(|e| { + InstalledError::Database(format!( + "Failed to get entry from installed table: {}", + e + )) + })?; let key_str = key.value(); - + // Skip if the key doesn't match the pattern if let Some(pattern) = pattern { if !key_str.contains(pattern) { continue; } } - + // Parse the key to get the FMRI let fmri = Fmri::from_str(key_str)?; - + // Get the publisher (handling the Option) - let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string()); - + let publisher = fmri + .publisher + .clone() + .unwrap_or_else(|| "unknown".to_string()); + // Add to results - results.push(InstalledPackageInfo { - fmri, - publisher, - }); + results.push(InstalledPackageInfo { fmri, publisher }); } - + results // The table is dropped at the end of this block }; - + Ok(results) } - + /// Get a manifest from the installed packages database pub fn get_manifest(&self, fmri: &Fmri) -> Result> { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a read transaction - let tx = db.begin_read() + let tx = db + .begin_read() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Create the key (full FMRI including publisher) let key = fmri.to_string(); - + // Use a block scope to ensure the table is dropped when no longer needed let manifest_option = { // Open the installed table - let installed_table = tx.open_table(INSTALLED_TABLE) - .map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; - + let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| { + InstalledError::Database(format!("Failed to open installed table: {}", e)) + })?; + // Try to get the manifest from the installed table if let Ok(Some(bytes)) = installed_table.get(key.as_str()) { Some(serde_json::from_slice(bytes.value())?) @@ -335,29 +383,31 @@ impl InstalledPackages { } // The table is dropped at the end of this block }; - + Ok(manifest_option) } - + /// Check if a package is installed pub fn is_installed(&self, fmri: &Fmri) -> Result { // Open the database let db = Database::open(&self.db_path) .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; - + // Begin a read transaction - let tx = db.begin_read() + let tx = db + .begin_read() .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; - + // Create the key (full FMRI including publisher) let key = fmri.to_string(); - + // Use a block scope to ensure the table is dropped when no longer needed let is_installed = { // Open the installed table - let installed_table = tx.open_table(INSTALLED_TABLE) - .map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; - + let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| { + InstalledError::Database(format!("Failed to open installed table: {}", e)) + })?; + // Check if the package exists if let Ok(Some(_)) = installed_table.get(key.as_str()) { true @@ -366,7 +416,7 @@ impl InstalledPackages { } // The table is dropped at the end of this block }; - + Ok(is_installed) } -} \ No newline at end of file +} diff --git a/libips/src/image/installed_tests.rs b/libips/src/image/installed_tests.rs index ad7ef69..05dd84d 100644 --- a/libips/src/image/installed_tests.rs +++ b/libips/src/image/installed_tests.rs @@ -10,67 +10,70 @@ fn test_installed_packages() { // Create a temporary directory for the test let temp_dir = tempdir().unwrap(); let image_path = temp_dir.path().join("image"); - + // Create the image let image = Image::create_image(&image_path, ImageType::Full).unwrap(); - + // Verify that the installed packages database was initialized assert!(image.installed_db_path().exists()); - + // Create a test manifest let mut manifest = Manifest::new(); - + // Add some attributes to the manifest let mut attr = Attr::default(); attr.key = "pkg.fmri".to_string(); attr.values = vec!["pkg://test/example/package@1.0".to_string()]; manifest.attributes.push(attr); - + let mut attr = Attr::default(); attr.key = "pkg.summary".to_string(); attr.values = vec!["Example package".to_string()]; manifest.attributes.push(attr); - + let mut attr = Attr::default(); attr.key = "pkg.description".to_string(); attr.values = vec!["An example package for testing".to_string()]; manifest.attributes.push(attr); - + // Create an FMRI for the package let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap(); - + // Install the package image.install_package(&fmri, &manifest).unwrap(); - + // Verify that the package is installed assert!(image.is_package_installed(&fmri).unwrap()); - + // Query the installed packages let packages = image.query_installed_packages(None).unwrap(); - + // Verify that the package is in the results assert_eq!(packages.len(), 1); - assert_eq!(packages[0].fmri.to_string(), "pkg://test/example/package@1.0"); + assert_eq!( + packages[0].fmri.to_string(), + "pkg://test/example/package@1.0" + ); assert_eq!(packages[0].publisher, "test"); - + // Get the manifest from the installed packages database let installed_manifest = image.get_manifest_from_installed(&fmri).unwrap().unwrap(); - + // Verify that the manifest is correct assert_eq!(installed_manifest.attributes.len(), 3); - + // Uninstall the package image.uninstall_package(&fmri).unwrap(); - + // Verify that the package is no longer installed assert!(!image.is_package_installed(&fmri).unwrap()); - + // Query the installed packages again let packages = image.query_installed_packages(None).unwrap(); - + // Verify that there are no packages assert_eq!(packages.len(), 0); - + // Clean up temp_dir.close().unwrap(); } @@ -80,42 +83,42 @@ fn test_installed_packages_key_format() { // Create a temporary directory for the test let temp_dir = tempdir().unwrap(); let db_path = temp_dir.path().join("installed.redb"); - + // Create the installed packages database let installed = InstalledPackages::new(&db_path); installed.init_db().unwrap(); - + // Create a test manifest let mut manifest = Manifest::new(); - + // Add some attributes to the manifest let mut attr = Attr::default(); attr.key = "pkg.fmri".to_string(); attr.values = vec!["pkg://test/example/package@1.0".to_string()]; manifest.attributes.push(attr); - + // Create an FMRI for the package let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap(); - + // Add the package to the database installed.add_package(&fmri, &manifest).unwrap(); - + // Open the database directly to check the key format let db = Database::open(&db_path).unwrap(); let tx = db.begin_read().unwrap(); let table = tx.open_table(installed::INSTALLED_TABLE).unwrap(); - + // Iterate through the keys let mut keys = Vec::new(); for entry in table.iter().unwrap() { let (key, _) = entry.unwrap(); keys.push(key.value().to_string()); } - + // Verify that there is one key and it has the correct format assert_eq!(keys.len(), 1); assert_eq!(keys[0], "pkg://test/example/package@1.0"); - + // Clean up temp_dir.close().unwrap(); -} \ No newline at end of file +} diff --git a/libips/src/image/mod.rs b/libips/src/image/mod.rs index d8738fd..8ebeccf 100644 --- a/libips/src/image/mod.rs +++ b/libips/src/image/mod.rs @@ -4,18 +4,18 @@ mod tests; use miette::Diagnostic; use properties::*; +use redb::{Database, ReadableDatabase, ReadableTable}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs::{self, File}; use std::path::{Path, PathBuf}; use thiserror::Error; -use redb::{Database, ReadableDatabase, ReadableTable}; -use crate::repository::{ReadableRepository, RepositoryError, RestBackend, FileBackend}; +use crate::repository::{FileBackend, ReadableRepository, RepositoryError, RestBackend}; // Export the catalog module pub mod catalog; -use catalog::{ImageCatalog, PackageInfo, INCORPORATE_TABLE}; +use catalog::{INCORPORATE_TABLE, ImageCatalog, PackageInfo}; // Export the installed packages module pub mod installed; @@ -49,28 +49,28 @@ pub enum ImageError { help("Provide a valid path for the image") )] InvalidPath(String), - + #[error("Repository error: {0}")] #[diagnostic( code(ips::image_error::repository), help("Check the repository configuration and try again") )] Repository(#[from] RepositoryError), - + #[error("Database error: {0}")] #[diagnostic( code(ips::image_error::database), help("Check the database configuration and try again") )] Database(String), - + #[error("Publisher not found: {0}")] #[diagnostic( code(ips::image_error::publisher_not_found), help("Check the publisher name and try again") )] PublisherNotFound(String), - + #[error("No publishers configured")] #[diagnostic( code(ips::image_error::no_publishers), @@ -148,9 +148,15 @@ impl Image { publishers: vec![], } } - + /// Add a publisher to the image - pub fn add_publisher(&mut self, name: &str, origin: &str, mirrors: Vec, is_default: bool) -> Result<()> { + pub fn add_publisher( + &mut self, + name: &str, + origin: &str, + mirrors: Vec, + is_default: bool, + ) -> Result<()> { // Check if publisher already exists if self.publishers.iter().any(|p| p.name == name) { // Update existing publisher @@ -159,7 +165,7 @@ impl Image { publisher.origin = origin.to_string(); publisher.mirrors = mirrors; publisher.is_default = is_default; - + // If this publisher is now the default, make sure no other publisher is default if is_default { for other_publisher in &mut self.publishers { @@ -168,7 +174,7 @@ impl Image { } } } - + break; } } @@ -180,43 +186,43 @@ impl Image { mirrors, is_default, }; - + // If this publisher is the default, make sure no other publisher is default if is_default { for publisher in &mut self.publishers { publisher.is_default = false; } } - + self.publishers.push(publisher); } - + // Save the image to persist the changes self.save()?; - + Ok(()) } - + /// Remove a publisher from the image pub fn remove_publisher(&mut self, name: &str) -> Result<()> { let initial_len = self.publishers.len(); self.publishers.retain(|p| p.name != name); - + if self.publishers.len() == initial_len { return Err(ImageError::PublisherNotFound(name.to_string())); } - + // If we removed the default publisher, set the first remaining publisher as default if self.publishers.iter().all(|p| !p.is_default) && !self.publishers.is_empty() { self.publishers[0].is_default = true; } - + // Save the image to persist the changes self.save()?; - + Ok(()) } - + /// Get the default publisher pub fn default_publisher(&self) -> Result<&Publisher> { // Find the default publisher @@ -225,15 +231,15 @@ impl Image { return Ok(publisher); } } - + // If no publisher is marked as default, return the first one if !self.publishers.is_empty() { return Ok(&self.publishers[0]); } - + Err(ImageError::NoPublishers) } - + /// Get a publisher by name pub fn get_publisher(&self, name: &str) -> Result<&Publisher> { for publisher in &self.publishers { @@ -241,10 +247,10 @@ impl Image { return Ok(publisher); } } - + Err(ImageError::PublisherNotFound(name.to_string())) } - + /// Get all publishers pub fn publishers(&self) -> &[Publisher] { &self.publishers @@ -272,27 +278,27 @@ impl Image { pub fn image_json_path(&self) -> PathBuf { self.metadata_dir().join("pkg6.image.json") } - + /// Returns the path to the installed packages database pub fn installed_db_path(&self) -> PathBuf { self.metadata_dir().join("installed.redb") } - + /// Returns the path to the manifest directory pub fn manifest_dir(&self) -> PathBuf { self.metadata_dir().join("manifests") } - + /// Returns the path to the catalog directory pub fn catalog_dir(&self) -> PathBuf { self.metadata_dir().join("catalog") } - + /// Returns the path to the catalog database pub fn catalog_db_path(&self) -> PathBuf { self.metadata_dir().join("catalog.redb") } - + /// Returns the path to the obsoleted packages database (separate DB) pub fn obsoleted_db_path(&self) -> PathBuf { self.metadata_dir().join("obsoleted.redb") @@ -304,46 +310,62 @@ impl Image { fs::create_dir_all(&metadata_dir).map_err(|e| { ImageError::IO(std::io::Error::new( std::io::ErrorKind::Other, - format!("Failed to create metadata directory at {:?}: {}", metadata_dir, e), + format!( + "Failed to create metadata directory at {:?}: {}", + metadata_dir, e + ), )) }) } - + /// Creates the manifest directory if it doesn't exist pub fn create_manifest_dir(&self) -> Result<()> { let manifest_dir = self.manifest_dir(); fs::create_dir_all(&manifest_dir).map_err(|e| { ImageError::IO(std::io::Error::new( std::io::ErrorKind::Other, - format!("Failed to create manifest directory at {:?}: {}", manifest_dir, e), + format!( + "Failed to create manifest directory at {:?}: {}", + manifest_dir, e + ), )) }) } - + /// Creates the catalog directory if it doesn't exist pub fn create_catalog_dir(&self) -> Result<()> { let catalog_dir = self.catalog_dir(); fs::create_dir_all(&catalog_dir).map_err(|e| { ImageError::IO(std::io::Error::new( std::io::ErrorKind::Other, - format!("Failed to create catalog directory at {:?}: {}", catalog_dir, e), + format!( + "Failed to create catalog directory at {:?}: {}", + catalog_dir, e + ), )) }) } - + /// Initialize the installed packages database pub fn init_installed_db(&self) -> Result<()> { let db_path = self.installed_db_path(); - + // Create the installed packages database let installed = InstalledPackages::new(&db_path); installed.init_db().map_err(|e| { - ImageError::Database(format!("Failed to initialize installed packages database: {}", e)) + ImageError::Database(format!( + "Failed to initialize installed packages database: {}", + e + )) }) } - + /// Add a package to the installed packages database - pub fn install_package(&self, fmri: &crate::fmri::Fmri, manifest: &crate::actions::Manifest) -> Result<()> { + pub fn install_package( + &self, + fmri: &crate::fmri::Fmri, + manifest: &crate::actions::Manifest, + ) -> Result<()> { // Precheck incorporation dependencies: fail if any stem already has a lock for d in &manifest.dependencies { if d.dependency_type == "incorporate" { @@ -351,7 +373,8 @@ impl Image { let stem = df.stem(); if let Some(_) = self.get_incorporated_release(stem)? { return Err(ImageError::Database(format!( - "Incorporation lock already exists for stem {}", stem + "Incorporation lock already exists for stem {}", + stem ))); } } @@ -361,7 +384,10 @@ impl Image { // Add to installed database let installed = InstalledPackages::new(self.installed_db_path()); installed.add_package(fmri, manifest).map_err(|e| { - ImageError::Database(format!("Failed to add package to installed database: {}", e)) + ImageError::Database(format!( + "Failed to add package to installed database: {}", + e + )) })?; // Write incorporation locks for any incorporate dependencies @@ -380,31 +406,43 @@ impl Image { } Ok(()) } - + /// Remove a package from the installed packages database pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> { let installed = InstalledPackages::new(self.installed_db_path()); installed.remove_package(fmri).map_err(|e| { - ImageError::Database(format!("Failed to remove package from installed database: {}", e)) + ImageError::Database(format!( + "Failed to remove package from installed database: {}", + e + )) }) } - + /// Query the installed packages database for packages matching a pattern - pub fn query_installed_packages(&self, pattern: Option<&str>) -> Result> { + pub fn query_installed_packages( + &self, + pattern: Option<&str>, + ) -> Result> { let installed = InstalledPackages::new(self.installed_db_path()); - installed.query_packages(pattern).map_err(|e| { - ImageError::Database(format!("Failed to query installed packages: {}", e)) - }) + installed + .query_packages(pattern) + .map_err(|e| ImageError::Database(format!("Failed to query installed packages: {}", e))) } - + /// Get a manifest from the installed packages database - pub fn get_manifest_from_installed(&self, fmri: &crate::fmri::Fmri) -> Result> { + pub fn get_manifest_from_installed( + &self, + fmri: &crate::fmri::Fmri, + ) -> Result> { let installed = InstalledPackages::new(self.installed_db_path()); installed.get_manifest(fmri).map_err(|e| { - ImageError::Database(format!("Failed to get manifest from installed database: {}", e)) + ImageError::Database(format!( + "Failed to get manifest from installed database: {}", + e + )) }) } - + /// Check if a package is installed pub fn is_package_installed(&self, fmri: &crate::fmri::Fmri) -> Result { let installed = InstalledPackages::new(self.installed_db_path()); @@ -412,14 +450,18 @@ impl Image { ImageError::Database(format!("Failed to check if package is installed: {}", e)) }) } - + /// Save a manifest into the metadata manifests directory for this image. /// /// The original, unprocessed manifest text is downloaded from the repository /// and stored under a flattened path: /// manifests//@.p5m /// Missing publisher will fall back to the image default publisher, then "unknown". - pub fn save_manifest(&self, fmri: &crate::fmri::Fmri, _manifest: &crate::actions::Manifest) -> Result { + pub fn save_manifest( + &self, + fmri: &crate::fmri::Fmri, + _manifest: &crate::actions::Manifest, + ) -> Result { // Determine publisher name let pub_name = if let Some(p) = &fmri.publisher { p.clone() @@ -438,7 +480,9 @@ impl Image { let mut out = String::new(); for b in s.bytes() { match b { - b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => out.push(b as char), + b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => { + out.push(b as char) + } b' ' => out.push('+'), _ => { out.push('%'); @@ -481,31 +525,35 @@ impl Image { Ok(file_path) } - + /// Initialize the catalog database pub fn init_catalog_db(&self) -> Result<()> { - let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path()); + let catalog = ImageCatalog::new( + self.catalog_dir(), + self.catalog_db_path(), + self.obsoleted_db_path(), + ); catalog.init_db().map_err(|e| { ImageError::Database(format!("Failed to initialize catalog database: {}", e)) }) } - + /// Download catalogs from all configured publishers and build the merged catalog pub fn download_catalogs(&self) -> Result<()> { // Create catalog directory if it doesn't exist self.create_catalog_dir()?; - + // Download catalogs for each publisher for publisher in &self.publishers { self.download_publisher_catalog(&publisher.name)?; } - + // Build the merged catalog self.build_catalog()?; - + Ok(()) } - + /// Refresh catalogs for specified publishers or all publishers if none specified /// /// # Arguments @@ -519,78 +567,91 @@ impl Image { pub fn refresh_catalogs(&self, publishers: &[String], full: bool) -> Result<()> { // Create catalog directory if it doesn't exist self.create_catalog_dir()?; - + // Determine which publishers to refresh let publishers_to_refresh: Vec<&Publisher> = if publishers.is_empty() { // If no publishers specified, refresh all self.publishers.iter().collect() } else { // Otherwise, filter publishers by name - self.publishers.iter() + self.publishers + .iter() .filter(|p| publishers.contains(&p.name)) .collect() }; - + // Check if we have any publishers to refresh if publishers_to_refresh.is_empty() { return Err(ImageError::NoPublishers); } - + // If full refresh is requested, clear the catalog directory for each publisher if full { for publisher in &publishers_to_refresh { let publisher_catalog_dir = self.catalog_dir().join(&publisher.name); if publisher_catalog_dir.exists() { - fs::remove_dir_all(&publisher_catalog_dir) - .map_err(|e| ImageError::IO(std::io::Error::new( + fs::remove_dir_all(&publisher_catalog_dir).map_err(|e| { + ImageError::IO(std::io::Error::new( std::io::ErrorKind::Other, - format!("Failed to remove catalog directory for publisher {}: {}", - publisher.name, e) - )))?; + format!( + "Failed to remove catalog directory for publisher {}: {}", + publisher.name, e + ), + )) + })?; } - fs::create_dir_all(&publisher_catalog_dir) - .map_err(|e| ImageError::IO(std::io::Error::new( + fs::create_dir_all(&publisher_catalog_dir).map_err(|e| { + ImageError::IO(std::io::Error::new( std::io::ErrorKind::Other, - format!("Failed to create catalog directory for publisher {}: {}", - publisher.name, e) - )))?; + format!( + "Failed to create catalog directory for publisher {}: {}", + publisher.name, e + ), + )) + })?; } } - + // Download catalogs for each publisher for publisher in publishers_to_refresh { self.download_publisher_catalog(&publisher.name)?; } - + // Build the merged catalog self.build_catalog()?; - + Ok(()) } - + /// Build the merged catalog from downloaded catalogs pub fn build_catalog(&self) -> Result<()> { // Initialize the catalog database if it doesn't exist self.init_catalog_db()?; - + // Get publisher names - let publisher_names: Vec = self.publishers.iter() - .map(|p| p.name.clone()) - .collect(); - + let publisher_names: Vec = self.publishers.iter().map(|p| p.name.clone()).collect(); + // Create the catalog and build it - let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path()); - catalog.build_catalog(&publisher_names).map_err(|e| { - ImageError::Database(format!("Failed to build catalog: {}", e)) - }) + let catalog = ImageCatalog::new( + self.catalog_dir(), + self.catalog_db_path(), + self.obsoleted_db_path(), + ); + catalog + .build_catalog(&publisher_names) + .map_err(|e| ImageError::Database(format!("Failed to build catalog: {}", e))) } - + /// Query the catalog for packages matching a pattern pub fn query_catalog(&self, pattern: Option<&str>) -> Result> { - let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path()); - catalog.query_packages(pattern).map_err(|e| { - ImageError::Database(format!("Failed to query catalog: {}", e)) - }) + let catalog = ImageCatalog::new( + self.catalog_dir(), + self.catalog_db_path(), + self.obsoleted_db_path(), + ); + catalog + .query_packages(pattern) + .map_err(|e| ImageError::Database(format!("Failed to query catalog: {}", e))) } /// Look up an incorporation lock for a given stem. @@ -598,16 +659,18 @@ impl Image { pub fn get_incorporated_release(&self, stem: &str) -> Result> { let db = Database::open(self.catalog_db_path()) .map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?; - let tx = db.begin_read() - .map_err(|e| ImageError::Database(format!("Failed to begin read transaction: {}", e)))?; + let tx = db.begin_read().map_err(|e| { + ImageError::Database(format!("Failed to begin read transaction: {}", e)) + })?; match tx.open_table(INCORPORATE_TABLE) { - Ok(table) => { - match table.get(stem) { - Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())), - Ok(None) => Ok(None), - Err(e) => Err(ImageError::Database(format!("Failed to read incorporate lock: {}", e))), - } - } + Ok(table) => match table.get(stem) { + Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())), + Ok(None) => Ok(None), + Err(e) => Err(ImageError::Database(format!( + "Failed to read incorporate lock: {}", + e + ))), + }, Err(_) => Ok(None), } } @@ -617,91 +680,109 @@ impl Image { pub fn add_incorporation_lock(&self, stem: &str, release: &str) -> Result<()> { let db = Database::open(self.catalog_db_path()) .map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?; - let tx = db.begin_write() - .map_err(|e| ImageError::Database(format!("Failed to begin write transaction: {}", e)))?; + let tx = db.begin_write().map_err(|e| { + ImageError::Database(format!("Failed to begin write transaction: {}", e)) + })?; { - let mut table = tx.open_table(INCORPORATE_TABLE) - .map_err(|e| ImageError::Database(format!("Failed to open incorporate table: {}", e)))?; + let mut table = tx.open_table(INCORPORATE_TABLE).map_err(|e| { + ImageError::Database(format!("Failed to open incorporate table: {}", e)) + })?; if let Ok(Some(_)) = table.get(stem) { - return Err(ImageError::Database(format!("Incorporation lock already exists for stem {}", stem))); + return Err(ImageError::Database(format!( + "Incorporation lock already exists for stem {}", + stem + ))); } - table.insert(stem, release.as_bytes()) - .map_err(|e| ImageError::Database(format!("Failed to insert incorporate lock: {}", e)))?; + table.insert(stem, release.as_bytes()).map_err(|e| { + ImageError::Database(format!("Failed to insert incorporate lock: {}", e)) + })?; } - tx.commit() - .map_err(|e| ImageError::Database(format!("Failed to commit incorporate lock: {}", e)))? - ; + tx.commit().map_err(|e| { + ImageError::Database(format!("Failed to commit incorporate lock: {}", e)) + })?; Ok(()) } - + /// Get a manifest from the catalog - pub fn get_manifest_from_catalog(&self, fmri: &crate::fmri::Fmri) -> Result> { - let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path()); + pub fn get_manifest_from_catalog( + &self, + fmri: &crate::fmri::Fmri, + ) -> Result> { + let catalog = ImageCatalog::new( + self.catalog_dir(), + self.catalog_db_path(), + self.obsoleted_db_path(), + ); catalog.get_manifest(fmri).map_err(|e| { ImageError::Database(format!("Failed to get manifest from catalog: {}", e)) }) } - + /// Fetch a full manifest for the given FMRI directly from its repository origin. /// /// This bypasses the local catalog database and retrieves the full manifest from /// the configured publisher origin (REST for http/https origins; File backend for /// file:// origins). A versioned FMRI is required. - pub fn get_manifest_from_repository(&self, fmri: &crate::fmri::Fmri) -> Result { + pub fn get_manifest_from_repository( + &self, + fmri: &crate::fmri::Fmri, + ) -> Result { // Determine publisher: use FMRI's publisher if present, otherwise default publisher let publisher_name = if let Some(p) = &fmri.publisher { p.clone() } else { self.default_publisher()?.name.clone() }; - + // Look up publisher configuration let publisher = self.get_publisher(&publisher_name)?; let origin = &publisher.origin; - + // Require a concrete version in the FMRI if fmri.version().is_empty() { return Err(ImageError::Repository(RepositoryError::Other( "FMRI must include a version to fetch manifest".to_string(), ))); } - + // Choose backend based on origin scheme if origin.starts_with("file://") { let path_str = origin.trim_start_matches("file://"); let path = PathBuf::from(path_str); let mut repo = FileBackend::open(&path)?; - repo.fetch_manifest(&publisher_name, fmri).map_err(Into::into) + repo.fetch_manifest(&publisher_name, fmri) + .map_err(Into::into) } else { let mut repo = RestBackend::open(origin)?; // Optionally set a per-publisher cache directory (used by other REST ops) let publisher_catalog_dir = self.catalog_dir().join(&publisher.name); repo.set_local_cache_path(&publisher_catalog_dir)?; - repo.fetch_manifest(&publisher_name, fmri).map_err(Into::into) + repo.fetch_manifest(&publisher_name, fmri) + .map_err(Into::into) } } - + /// Download catalog for a specific publisher pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> { // Get the publisher let publisher = self.get_publisher(publisher_name)?; - + // Create a REST backend for the publisher let mut repo = RestBackend::open(&publisher.origin)?; - + // Set local cache path to the catalog directory for this publisher let publisher_catalog_dir = self.catalog_dir().join(&publisher.name); fs::create_dir_all(&publisher_catalog_dir)?; repo.set_local_cache_path(&publisher_catalog_dir)?; - + // Download the catalog repo.download_catalog(&publisher.name, None)?; - + Ok(()) } - + /// Create a new image with the basic directory structure - /// + /// /// This method only creates the image structure without adding publishers or downloading catalogs. /// Publisher addition and catalog downloading should be handled separately. /// @@ -715,21 +796,21 @@ impl Image { ImageType::Full => Image::new_full(path.as_ref().to_path_buf()), ImageType::Partial => Image::new_partial(path.as_ref().to_path_buf()), }; - + // Create the directory structure image.create_metadata_dir()?; image.create_manifest_dir()?; image.create_catalog_dir()?; - + // Initialize the installed packages database image.init_installed_db()?; - + // Initialize the catalog database image.init_catalog_db()?; - + // Save the image image.save()?; - + Ok(image) } @@ -749,14 +830,14 @@ impl Image { /// Loads an image from the specified path pub fn load>(path: P) -> Result { let path = path.as_ref(); - + // Check for both full and partial image JSON files let full_image = Image::new_full(path); let partial_image = Image::new_partial(path); - + let full_json_path = full_image.image_json_path(); let partial_json_path = partial_image.image_json_path(); - + // Determine which JSON file exists let json_path = if full_json_path.exists() { full_json_path @@ -764,18 +845,18 @@ impl Image { partial_json_path } else { return Err(ImageError::InvalidPath(format!( - "Image JSON file not found at either {:?} or {:?}", + "Image JSON file not found at either {:?} or {:?}", full_json_path, partial_json_path ))); }; - + let file = File::open(&json_path).map_err(|e| { ImageError::IO(std::io::Error::new( std::io::ErrorKind::Other, format!("Failed to open image JSON file at {:?}: {}", json_path, e), )) })?; - + serde_json::from_reader(file).map_err(ImageError::Json) } } diff --git a/libips/src/image/tests.rs b/libips/src/image/tests.rs index 892bbeb..c5d3dc8 100644 --- a/libips/src/image/tests.rs +++ b/libips/src/image/tests.rs @@ -7,13 +7,13 @@ fn test_image_catalog() { // Create a temporary directory for the test let temp_dir = tempdir().unwrap(); let image_path = temp_dir.path().join("image"); - + // Create the image let image = Image::create_image(&image_path, ImageType::Full).unwrap(); - + // Verify that the catalog database was initialized assert!(image.catalog_db_path().exists()); - + // Clean up temp_dir.close().unwrap(); } @@ -23,28 +23,30 @@ fn test_catalog_methods() { // Create a temporary directory for the test let temp_dir = tempdir().unwrap(); let image_path = temp_dir.path().join("image"); - + // Create the image let mut image = Image::create_image(&image_path, ImageType::Full).unwrap(); - + // Print the image type and paths println!("Image type: {:?}", image.image_type()); println!("Image path: {:?}", image.path()); println!("Metadata dir: {:?}", image.metadata_dir()); println!("Catalog dir: {:?}", image.catalog_dir()); - + // Add a publisher - image.add_publisher("test", "http://example.com/repo", vec![], true).unwrap(); - + image + .add_publisher("test", "http://example.com/repo", vec![], true) + .unwrap(); + // Print the publishers println!("Publishers: {:?}", image.publishers()); - + // Create the catalog directory structure let catalog_dir = image.catalog_dir(); let publisher_dir = catalog_dir.join("test"); println!("Publisher dir: {:?}", publisher_dir); fs::create_dir_all(&publisher_dir).unwrap(); - + // Create a simple catalog.attrs file let attrs_content = r#"{ "created": "2025-08-04T23:01:00Z", @@ -59,10 +61,13 @@ fn test_catalog_methods() { "updates": {}, "version": 1 }"#; - println!("Writing catalog.attrs to {:?}", publisher_dir.join("catalog.attrs")); + println!( + "Writing catalog.attrs to {:?}", + publisher_dir.join("catalog.attrs") + ); println!("catalog.attrs content: {}", attrs_content); fs::write(publisher_dir.join("catalog.attrs"), attrs_content).unwrap(); - + // Create a simple base catalog part let base_content = r#"{ "test": { @@ -88,68 +93,80 @@ fn test_catalog_methods() { ] } }"#; - println!("Writing base catalog part to {:?}", publisher_dir.join("base")); + println!( + "Writing base catalog part to {:?}", + publisher_dir.join("base") + ); println!("base catalog part content: {}", base_content); fs::write(publisher_dir.join("base"), base_content).unwrap(); - + // Verify that the files were written correctly - println!("Checking if catalog.attrs exists: {}", publisher_dir.join("catalog.attrs").exists()); - println!("Checking if base catalog part exists: {}", publisher_dir.join("base").exists()); - + println!( + "Checking if catalog.attrs exists: {}", + publisher_dir.join("catalog.attrs").exists() + ); + println!( + "Checking if base catalog part exists: {}", + publisher_dir.join("base").exists() + ); + // Build the catalog println!("Building catalog..."); match image.build_catalog() { Ok(_) => println!("Catalog built successfully"), Err(e) => println!("Failed to build catalog: {:?}", e), } - + // Query the catalog println!("Querying catalog..."); let packages = match image.query_catalog(None) { Ok(pkgs) => { println!("Found {} packages", pkgs.len()); pkgs - }, + } Err(e) => { println!("Failed to query catalog: {:?}", e); panic!("Failed to query catalog: {:?}", e); } }; - + // Verify that both non-obsolete and obsolete packages are in the results assert_eq!(packages.len(), 2); - + // Verify that one package is marked as obsolete let obsolete_packages: Vec<_> = packages.iter().filter(|p| p.obsolete).collect(); assert_eq!(obsolete_packages.len(), 1); assert_eq!(obsolete_packages[0].fmri.stem(), "example/obsolete"); - + // Verify that the obsolete package has the full FMRI as key // This is indirectly verified by checking that the publisher is included in the FMRI - assert_eq!(obsolete_packages[0].fmri.publisher, Some("test".to_string())); - + assert_eq!( + obsolete_packages[0].fmri.publisher, + Some("test".to_string()) + ); + // Verify that one package is not marked as obsolete let non_obsolete_packages: Vec<_> = packages.iter().filter(|p| !p.obsolete).collect(); assert_eq!(non_obsolete_packages.len(), 1); assert_eq!(non_obsolete_packages[0].fmri.stem(), "example/package"); - + // Get the manifest for the non-obsolete package let fmri = &non_obsolete_packages[0].fmri; let manifest = image.get_manifest_from_catalog(fmri).unwrap(); assert!(manifest.is_some()); - + // Get the manifest for the obsolete package let fmri = &obsolete_packages[0].fmri; let manifest = image.get_manifest_from_catalog(fmri).unwrap(); assert!(manifest.is_some()); - + // Verify that the obsolete package's manifest has the obsolete attribute let manifest = manifest.unwrap(); let is_obsolete = manifest.attributes.iter().any(|attr| { attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true") }); assert!(is_obsolete); - + // Clean up temp_dir.close().unwrap(); } @@ -159,45 +176,61 @@ fn test_refresh_catalogs_directory_clearing() { // Create a temporary directory for the test let temp_dir = tempdir().unwrap(); let image_path = temp_dir.path().join("image"); - + // Create the image let mut image = Image::create_image(&image_path, ImageType::Full).unwrap(); - + // Add two publishers - image.add_publisher("test1", "http://example.com/repo1", vec![], true).unwrap(); - image.add_publisher("test2", "http://example.com/repo2", vec![], false).unwrap(); - + image + .add_publisher("test1", "http://example.com/repo1", vec![], true) + .unwrap(); + image + .add_publisher("test2", "http://example.com/repo2", vec![], false) + .unwrap(); + // Create the catalog directory structure for both publishers let catalog_dir = image.catalog_dir(); let publisher1_dir = catalog_dir.join("test1"); let publisher2_dir = catalog_dir.join("test2"); fs::create_dir_all(&publisher1_dir).unwrap(); fs::create_dir_all(&publisher2_dir).unwrap(); - + // Create marker files in both publisher directories let marker_file1 = publisher1_dir.join("marker"); let marker_file2 = publisher2_dir.join("marker"); - fs::write(&marker_file1, "This file should be removed during full refresh").unwrap(); - fs::write(&marker_file2, "This file should be removed during full refresh").unwrap(); + fs::write( + &marker_file1, + "This file should be removed during full refresh", + ) + .unwrap(); + fs::write( + &marker_file2, + "This file should be removed during full refresh", + ) + .unwrap(); assert!(marker_file1.exists()); assert!(marker_file2.exists()); - + // Directly test the directory clearing functionality for a specific publisher // This simulates the behavior of refresh_catalogs with full=true for a specific publisher if publisher1_dir.exists() { fs::remove_dir_all(&publisher1_dir).unwrap(); } fs::create_dir_all(&publisher1_dir).unwrap(); - + // Verify that the marker file for publisher1 was removed assert!(!marker_file1.exists()); // Verify that the marker file for publisher2 still exists assert!(marker_file2.exists()); - + // Create a new marker file for publisher1 - fs::write(&marker_file1, "This file should be removed during full refresh").unwrap(); + fs::write( + &marker_file1, + "This file should be removed during full refresh", + ) + .unwrap(); assert!(marker_file1.exists()); - + // Directly test the directory clearing functionality for all publishers // This simulates the behavior of refresh_catalogs with full=true for all publishers for publisher in &image.publishers { @@ -207,11 +240,11 @@ fn test_refresh_catalogs_directory_clearing() { } fs::create_dir_all(&publisher_dir).unwrap(); } - + // Verify that both marker files were removed assert!(!marker_file1.exists()); assert!(!marker_file2.exists()); - + // Clean up temp_dir.close().unwrap(); -} \ No newline at end of file +} diff --git a/libips/src/lib.rs b/libips/src/lib.rs index 9fe993e..b25fd6e 100644 --- a/libips/src/lib.rs +++ b/libips/src/lib.rs @@ -5,17 +5,17 @@ #[allow(clippy::result_large_err)] pub mod actions; +pub mod api; +pub mod depend; pub mod digest; pub mod fmri; pub mod image; pub mod payload; -pub mod repository; pub mod publisher; -pub mod transformer; +pub mod repository; pub mod solver; -pub mod depend; -pub mod api; mod test_json_manifest; +pub mod transformer; #[cfg(test)] mod publisher_tests; @@ -69,91 +69,101 @@ set name=pkg.summary value=\"'XZ Utils - loss-less file compression application ); let test_results = vec![ - Attr{ + Attr { key: String::from("pkg.fmri"), - values: vec![String::from("pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z")], + values: vec![String::from( + "pkg://openindiana.org/web/server/nginx@1.18.0,5.11-2020.0.1.0:20200421T195136Z", + )], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("com.oracle.info.name"), values: vec![String::from("nginx"), String::from("test")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("userland.info.git-remote"), values: vec![String::from("git://github.com/OpenIndiana/oi-userland.git")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("userland.info.git-branch"), values: vec![String::from("HEAD")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("userland.info.git-rev"), values: vec![String::from("1665491ba61bd494bf73e2916cd2250f3024260e")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("pkg.summary"), values: vec![String::from("Nginx Webserver")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("info.classification"), - values: vec![String::from("org.opensolaris.category.2008:Web Services/Application and Web Servers")], + values: vec![String::from( + "org.opensolaris.category.2008:Web Services/Application and Web Servers", + )], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("info.upstream-url"), values: vec![String::from("http://nginx.net/")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("info.source-url"), - values: vec![String::from("http://nginx.org/download/nginx-1.18.0.tar.gz")], + values: vec![String::from( + "http://nginx.org/download/nginx-1.18.0.tar.gz", + )], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("org.opensolaris.consolidation"), values: vec![String::from("userland")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("com.oracle.info.version"), values: vec![String::from("1.18.0")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("pkg.summary"), values: vec![String::from("provided mouse accessibility enhancements")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("info.upstream"), values: vec![String::from("X.Org Foundation")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("pkg.description"), values: vec![String::from("Latvian language support's extra files")], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("variant.arch"), values: vec![String::from("i386")], properties: optional_hash, }, - Attr{ + Attr { key: String::from("info.source-url"), - values: vec![String::from("http://www.pgpool.net/download.php?f=pgpool-II-3.3.1.tar.gz")], + values: vec![String::from( + "http://www.pgpool.net/download.php?f=pgpool-II-3.3.1.tar.gz", + )], properties: HashMap::new(), }, - Attr{ + Attr { key: String::from("pkg.summary"), - values: vec![String::from("'XZ Utils - loss-less file compression application and library.'")], //TODO knock out the single quotes + values: vec![String::from( + "'XZ Utils - loss-less file compression application and library.'", + )], //TODO knock out the single quotes properties: HashMap::new(), - } + }, ]; let res = Manifest::parse_string(manifest_string); diff --git a/libips/src/publisher.rs b/libips/src/publisher.rs index b442719..ae4dcec 100644 --- a/libips/src/publisher.rs +++ b/libips/src/publisher.rs @@ -3,15 +3,15 @@ // MPL was not distributed with this file, You can // obtain one at https://mozilla.org/MPL/2.0/. -use std::path::{Path, PathBuf}; use std::fs; +use std::path::{Path, PathBuf}; use miette::Diagnostic; use thiserror::Error; use crate::actions::{File as FileAction, Manifest, Transform as TransformAction}; -use crate::repository::{ReadableRepository, RepositoryError, WritableRepository}; use crate::repository::file_backend::{FileBackend, Transaction}; +use crate::repository::{ReadableRepository, RepositoryError, WritableRepository}; use crate::transformer; /// Error type for high-level publishing operations @@ -30,7 +30,10 @@ pub enum PublisherError { Io(String), #[error("invalid root path: {0}")] - #[diagnostic(code(ips::publisher_error::invalid_root_path), help("Ensure the directory exists and is readable"))] + #[diagnostic( + code(ips::publisher_error::invalid_root_path), + help("Ensure the directory exists and is readable") + )] InvalidRoot(String), } @@ -51,7 +54,12 @@ impl PublisherClient { /// Open an existing repository located at `path` with a selected `publisher`. pub fn open>(path: P, publisher: impl Into) -> Result { let backend = FileBackend::open(path)?; - Ok(Self { backend, publisher: publisher.into(), tx: None, transform_rules: Vec::new() }) + Ok(Self { + backend, + publisher: publisher.into(), + tx: None, + transform_rules: Vec::new(), + }) } /// Open a transaction if not already open and return whether a new transaction was created. @@ -70,9 +78,13 @@ impl PublisherClient { return Err(PublisherError::InvalidRoot(root.display().to_string())); } let mut manifest = Manifest::new(); - let root = root.canonicalize().map_err(|_| PublisherError::InvalidRoot(root.display().to_string()))?; + let root = root + .canonicalize() + .map_err(|_| PublisherError::InvalidRoot(root.display().to_string()))?; - let walker = walkdir::WalkDir::new(&root).into_iter().filter_map(|e| e.ok()); + let walker = walkdir::WalkDir::new(&root) + .into_iter() + .filter_map(|e| e.ok()); // Ensure a transaction is open if self.tx.is_none() { self.open_transaction()?; diff --git a/libips/src/publisher_tests.rs b/libips/src/publisher_tests.rs index 3040e3c..9ed3bcb 100644 --- a/libips/src/publisher_tests.rs +++ b/libips/src/publisher_tests.rs @@ -21,7 +21,8 @@ mod tests { let repo_path = tmp.path().to_path_buf(); // Initialize repository - let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo"); + let mut backend = + FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo"); backend.add_publisher("test").expect("add publisher"); // Prepare a prototype directory with a nested file @@ -36,16 +37,27 @@ mod tests { // Use PublisherClient to publish let mut client = PublisherClient::open(&repo_path, "test").expect("open client"); client.open_transaction().expect("open tx"); - let manifest = client.build_manifest_from_dir(&proto_dir).expect("build manifest"); + let manifest = client + .build_manifest_from_dir(&proto_dir) + .expect("build manifest"); client.publish(manifest, true).expect("publish"); // Verify the manifest exists at the default path for unknown version - let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest"); - assert!(manifest_path.exists(), "manifest not found at {}", manifest_path.display()); + let manifest_path = + FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest"); + assert!( + manifest_path.exists(), + "manifest not found at {}", + manifest_path.display() + ); // Verify at least one file was stored under publisher/test/file let file_root = repo_path.join("publisher").join("test").join("file"); - assert!(file_root.exists(), "file store root does not exist: {}", file_root.display()); + assert!( + file_root.exists(), + "file store root does not exist: {}", + file_root.display() + ); let mut any_file = false; if let Ok(entries) = fs::read_dir(&file_root) { for entry in entries.flatten() { @@ -62,14 +74,15 @@ mod tests { } else if path.is_file() { any_file = true; } - if any_file { break; } + if any_file { + break; + } } } assert!(any_file, "no stored file found in file store"); } } - #[cfg(test)] mod transform_rule_integration_tests { use crate::actions::Manifest; @@ -85,7 +98,8 @@ mod transform_rule_integration_tests { // Setup repository and publisher let tmp = TempDir::new().expect("tempdir"); let repo_path = tmp.path().to_path_buf(); - let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo"); + let mut backend = + FileBackend::create(&repo_path, RepositoryVersion::V4).expect("create repo"); backend.add_publisher("test").expect("add publisher"); // Prototype directory with a file @@ -102,18 +116,33 @@ mod transform_rule_integration_tests { // Use PublisherClient to load rules, build manifest and publish let mut client = PublisherClient::open(&repo_path, "test").expect("open client"); - let loaded = client.load_transform_rules_from_file(&rules_path).expect("load rules"); + let loaded = client + .load_transform_rules_from_file(&rules_path) + .expect("load rules"); assert!(loaded >= 1, "expected at least one rule loaded"); client.open_transaction().expect("open tx"); - let manifest = client.build_manifest_from_dir(&proto_dir).expect("build manifest"); + let manifest = client + .build_manifest_from_dir(&proto_dir) + .expect("build manifest"); client.publish(manifest, false).expect("publish"); // Read stored manifest and verify attribute - let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest"); - assert!(manifest_path.exists(), "manifest missing: {}", manifest_path.display()); + let manifest_path = + FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest"); + assert!( + manifest_path.exists(), + "manifest missing: {}", + manifest_path.display() + ); let json = fs::read_to_string(&manifest_path).expect("read manifest"); let parsed: Manifest = serde_json::from_str(&json).expect("parse manifest json"); - let has_summary = parsed.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| v == "Added via rules")); - assert!(has_summary, "pkg.summary attribute added via rules not found"); + let has_summary = parsed + .attributes + .iter() + .any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| v == "Added via rules")); + assert!( + has_summary, + "pkg.summary attribute added via rules not found" + ); } } diff --git a/libips/src/repository/catalog_writer.rs b/libips/src/repository/catalog_writer.rs index 09338cd..15cd614 100644 --- a/libips/src/repository/catalog_writer.rs +++ b/libips/src/repository/catalog_writer.rs @@ -21,21 +21,31 @@ fn sha1_hex(bytes: &[u8]) -> String { fn atomic_write_bytes(path: &Path, bytes: &[u8]) -> Result<()> { let parent = path.parent().unwrap_or(Path::new(".")); - fs::create_dir_all(parent) - .map_err(|e| RepositoryError::DirectoryCreateError { path: parent.to_path_buf(), source: e })?; + fs::create_dir_all(parent).map_err(|e| RepositoryError::DirectoryCreateError { + path: parent.to_path_buf(), + source: e, + })?; let tmp: PathBuf = path.with_extension("tmp"); { - let mut f = std::fs::File::create(&tmp) - .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + let mut f = std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError { + path: tmp.clone(), + source: e, + })?; f.write_all(bytes) - .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; - f.flush() - .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + .map_err(|e| RepositoryError::FileWriteError { + path: tmp.clone(), + source: e, + })?; + f.flush().map_err(|e| RepositoryError::FileWriteError { + path: tmp.clone(), + source: e, + })?; } - fs::rename(&tmp, path) - .map_err(|e| RepositoryError::FileWriteError { path: path.to_path_buf(), source: e })? - ; + fs::rename(&tmp, path).map_err(|e| RepositoryError::FileWriteError { + path: path.to_path_buf(), + source: e, + })?; Ok(()) } @@ -43,53 +53,71 @@ fn atomic_write_bytes(path: &Path, bytes: &[u8]) -> Result<()> { pub(crate) fn write_catalog_attrs(path: &Path, attrs: &mut CatalogAttrs) -> Result { // Compute signature over content without _SIGNATURE attrs.signature = None; - let bytes_without_sig = serde_json::to_vec(&attrs) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?; + let bytes_without_sig = serde_json::to_vec(&attrs).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)) + })?; let sig = sha1_hex(&bytes_without_sig); let mut sig_map = std::collections::HashMap::new(); sig_map.insert("sha-1".to_string(), sig); attrs.signature = Some(sig_map); - let final_bytes = serde_json::to_vec(&attrs) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?; + let final_bytes = serde_json::to_vec(&attrs).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)) + })?; debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog.attrs"); atomic_write_bytes(path, &final_bytes)?; // safe to unwrap as signature was just inserted - Ok(attrs.signature.as_ref().and_then(|m| m.get("sha-1").cloned()).unwrap_or_default()) + Ok(attrs + .signature + .as_ref() + .and_then(|m| m.get("sha-1").cloned()) + .unwrap_or_default()) } #[instrument(level = "debug", skip(part))] pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result { // Compute signature over content without _SIGNATURE part.signature = None; - let bytes_without_sig = serde_json::to_vec(&part) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?; + let bytes_without_sig = serde_json::to_vec(&part).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)) + })?; let sig = sha1_hex(&bytes_without_sig); let mut sig_map = std::collections::HashMap::new(); sig_map.insert("sha-1".to_string(), sig); part.signature = Some(sig_map); - let final_bytes = serde_json::to_vec(&part) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?; + let final_bytes = serde_json::to_vec(&part).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)) + })?; debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part"); atomic_write_bytes(path, &final_bytes)?; - Ok(part.signature.as_ref().and_then(|m| m.get("sha-1").cloned()).unwrap_or_default()) + Ok(part + .signature + .as_ref() + .and_then(|m| m.get("sha-1").cloned()) + .unwrap_or_default()) } #[instrument(level = "debug", skip(log))] pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result { // Compute signature over content without _SIGNATURE log.signature = None; - let bytes_without_sig = serde_json::to_vec(&log) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; + let bytes_without_sig = serde_json::to_vec(&log).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)) + })?; let sig = sha1_hex(&bytes_without_sig); let mut sig_map = std::collections::HashMap::new(); sig_map.insert("sha-1".to_string(), sig); log.signature = Some(sig_map); - let final_bytes = serde_json::to_vec(&log) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; + let final_bytes = serde_json::to_vec(&log).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)) + })?; debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log"); atomic_write_bytes(path, &final_bytes)?; - Ok(log.signature.as_ref().and_then(|m| m.get("sha-1").cloned()).unwrap_or_default()) + Ok(log + .signature + .as_ref() + .and_then(|m| m.get("sha-1").cloned()) + .unwrap_or_default()) } diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index 2243d38..35e5dbe 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -4,8 +4,8 @@ // obtain one at https://mozilla.org/MPL/2.0/. use super::{RepositoryError, Result}; -use flate2::write::GzEncoder; use flate2::Compression as GzipCompression; +use flate2::write::GzEncoder; use lz4::EncoderBuilder; use regex::Regex; use serde::{Deserialize, Serialize}; @@ -25,11 +25,11 @@ use crate::digest::Digest; use crate::fmri::Fmri; use crate::payload::{Payload, PayloadCompressionAlgorithm}; -use super::{ - PackageContents, PackageInfo, PublisherInfo, ReadableRepository, RepositoryConfig, - RepositoryInfo, RepositoryVersion, WritableRepository, REPOSITORY_CONFIG_FILENAME, -}; use super::catalog_writer; +use super::{ + PackageContents, PackageInfo, PublisherInfo, REPOSITORY_CONFIG_FILENAME, ReadableRepository, + RepositoryConfig, RepositoryInfo, RepositoryVersion, WritableRepository, +}; use ini::Ini; // Define a struct to hold the content vectors for each package @@ -224,7 +224,8 @@ pub struct FileBackend { /// Uses RefCell for interior mutability to allow mutation through immutable references catalog_manager: Option>, /// Manager for obsoleted packages - obsoleted_manager: Option>, + obsoleted_manager: + Option>, } /// Format a SystemTime as an ISO 8601 timestamp string @@ -342,20 +343,16 @@ impl Transaction { // Check if the temp file already exists if temp_file_path.exists() { // If it exists, remove it to avoid any issues with existing content - fs::remove_file(&temp_file_path).map_err(|e| { - RepositoryError::FileWriteError { - path: temp_file_path.clone(), - source: e, - } + fs::remove_file(&temp_file_path).map_err(|e| RepositoryError::FileWriteError { + path: temp_file_path.clone(), + source: e, })?; } // Read the file content - let file_content = fs::read(file_path).map_err(|e| { - RepositoryError::FileReadError { - path: file_path.to_path_buf(), - source: e, - } + let file_content = fs::read(file_path).map_err(|e| RepositoryError::FileReadError { + path: file_path.to_path_buf(), + source: e, })?; // Create a payload with the hash information if it doesn't exist @@ -493,7 +490,8 @@ impl Transaction { // Copy files to their final location for (source_path, hash) in self.files { // Create the destination path using the helper function with publisher - let dest_path = FileBackend::construct_file_path_with_publisher(&self.repo, &publisher, &hash); + let dest_path = + FileBackend::construct_file_path_with_publisher(&self.repo, &publisher, &hash); // Create parent directories if they don't exist if let Some(parent) = dest_path.parent() { @@ -567,7 +565,8 @@ impl Transaction { // Construct the manifest path using the helper method let pkg_manifest_path = if package_version.is_empty() { // If no version was provided, store as a default manifest file - FileBackend::construct_package_dir(&self.repo, &publisher, &package_stem).join("manifest") + FileBackend::construct_package_dir(&self.repo, &publisher, &package_stem) + .join("manifest") } else { FileBackend::construct_manifest_path( &self.repo, @@ -597,14 +596,14 @@ impl Transaction { if config_path.exists() { let config_content = fs::read_to_string(&config_path)?; let config: RepositoryConfig = serde_json::from_str(&config_content)?; - + // Check if this publisher was just added in this transaction let publisher_dir = self.repo.join("publisher").join(&publisher); let pub_p5i_path = publisher_dir.join("pub.p5i"); - + if !pub_p5i_path.exists() { debug!("Creating pub.p5i file for publisher: {}", publisher); - + // Create the pub.p5i file let repo = FileBackend { path: self.repo.clone(), @@ -612,7 +611,7 @@ impl Transaction { catalog_manager: None, obsoleted_manager: None, }; - + repo.create_pub_p5i_file(&publisher)?; } } @@ -667,13 +666,15 @@ impl ReadableRepository for FileBackend { let config5_path = path.join("pkg5.repository"); let config: RepositoryConfig = if config6_path.exists() { - let config_data = fs::read_to_string(&config6_path) - .map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e)))?; + let config_data = fs::read_to_string(&config6_path).map_err(|e| { + RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e)) + })?; serde_json::from_str(&config_data)? } else if config5_path.exists() { // Minimal mapping for legacy INI: take publishers only from INI; do not scan disk. - let ini = Ini::load_from_file(&config5_path) - .map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e)))?; + let ini = Ini::load_from_file(&config5_path).map_err(|e| { + RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e)) + })?; // Default repository version for legacy format is v4 let mut cfg = RepositoryConfig::default(); @@ -829,7 +830,10 @@ impl ReadableRepository for FileBackend { pattern: Option<&str>, action_types: Option<&[String]>, ) -> Result> { - debug!("show_contents called with publisher: {:?}, pattern: {:?}", publisher, pattern); + debug!( + "show_contents called with publisher: {:?}, pattern: {:?}", + publisher, pattern + ); // Use a HashMap to store package information let mut packages = HashMap::new(); @@ -889,7 +893,9 @@ impl ReadableRepository for FileBackend { // Check if the file starts with a valid manifest marker if bytes_read == 0 - || (buffer[0] != b'{' && buffer[0] != b'<' && buffer[0] != b's') + || (buffer[0] != b'{' + && buffer[0] != b'<' + && buffer[0] != b's') { continue; } @@ -901,7 +907,9 @@ impl ReadableRepository for FileBackend { let mut pkg_id = String::new(); for attr in &manifest.attributes { - if attr.key == "pkg.fmri" && !attr.values.is_empty() { + if attr.key == "pkg.fmri" + && !attr.values.is_empty() + { let fmri = &attr.values[0]; // Parse the FMRI using our Fmri type @@ -913,14 +921,22 @@ impl ReadableRepository for FileBackend { match Regex::new(pat) { Ok(regex) => { // Use regex matching - if !regex.is_match(parsed_fmri.stem()) { + if !regex.is_match( + parsed_fmri.stem(), + ) { continue; } } Err(err) => { // Log the error but fall back to the simple string contains - error!("FileBackend::show_contents: Error compiling regex pattern '{}': {}", pat, err); - if !parsed_fmri.stem().contains(pat) { + error!( + "FileBackend::show_contents: Error compiling regex pattern '{}': {}", + pat, err + ); + if !parsed_fmri + .stem() + .contains(pat) + { continue; } } @@ -970,7 +986,9 @@ impl ReadableRepository for FileBackend { .contains(&"file".to_string()) { for file in &manifest.files { - content_vectors.files.push(file.path.clone()); + content_vectors + .files + .push(file.path.clone()); } } @@ -982,7 +1000,9 @@ impl ReadableRepository for FileBackend { .contains(&"dir".to_string()) { for dir in &manifest.directories { - content_vectors.directories.push(dir.path.clone()); + content_vectors + .directories + .push(dir.path.clone()); } } @@ -994,7 +1014,9 @@ impl ReadableRepository for FileBackend { .contains(&"link".to_string()) { for link in &manifest.links { - content_vectors.links.push(link.path.clone()); + content_vectors + .links + .push(link.path.clone()); } } @@ -1007,7 +1029,9 @@ impl ReadableRepository for FileBackend { { for depend in &manifest.dependencies { if let Some(fmri) = &depend.fmri { - content_vectors.dependencies.push(fmri.to_string()); + content_vectors + .dependencies + .push(fmri.to_string()); } } } @@ -1020,12 +1044,22 @@ impl ReadableRepository for FileBackend { .contains(&"license".to_string()) { for license in &manifest.licenses { - if let Some(path_prop) = license.properties.get("path") { - content_vectors.licenses.push(path_prop.value.clone()); - } else if let Some(license_prop) = license.properties.get("license") { - content_vectors.licenses.push(license_prop.value.clone()); + if let Some(path_prop) = + license.properties.get("path") + { + content_vectors + .licenses + .push(path_prop.value.clone()); + } else if let Some(license_prop) = + license.properties.get("license") + { + content_vectors + .licenses + .push(license_prop.value.clone()); } else { - content_vectors.licenses.push(license.payload.clone()); + content_vectors + .licenses + .push(license.payload.clone()); } } } @@ -1103,7 +1137,10 @@ impl ReadableRepository for FileBackend { } Err(err) => { // Log the error but fall back to the simple string contains - error!("FileBackend::show_contents: Error compiling regex pattern '{}': {}", pat, err); + error!( + "FileBackend::show_contents: Error compiling regex pattern '{}': {}", + pat, err + ); if !parsed_fmri.stem().contains(pat) { continue; @@ -1323,16 +1360,30 @@ impl ReadableRepository for FileBackend { // If destination already exists and matches digest, do nothing if dest.exists() { - let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError { path: dest.to_path_buf(), source: e })?; - match crate::digest::Digest::from_bytes(&bytes, algo.clone(), crate::digest::DigestSource::PrimaryPayloadHash) { + let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError { + path: dest.to_path_buf(), + source: e, + })?; + match crate::digest::Digest::from_bytes( + &bytes, + algo.clone(), + crate::digest::DigestSource::PrimaryPayloadHash, + ) { Ok(comp) if comp.hash == hash => return Ok(()), _ => { /* fall through to overwrite */ } } } // Read source content and verify digest - let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError { path: source_path.clone(), source: e })?; - match crate::digest::Digest::from_bytes(&bytes, algo, crate::digest::DigestSource::PrimaryPayloadHash) { + let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError { + path: source_path.clone(), + source: e, + })?; + match crate::digest::Digest::from_bytes( + &bytes, + algo, + crate::digest::DigestSource::PrimaryPayloadHash, + ) { Ok(comp) => { if comp.hash != hash { return Err(RepositoryError::DigestError(format!( @@ -1363,7 +1414,9 @@ impl ReadableRepository for FileBackend { // Require a concrete version let version = fmri.version(); if version.is_empty() { - return Err(RepositoryError::Other("FMRI must include a version to fetch manifest".into())); + return Err(RepositoryError::Other( + "FMRI must include a version to fetch manifest".into(), + )); } // Preferred path: publisher-scoped manifest path @@ -1375,7 +1428,11 @@ impl ReadableRepository for FileBackend { // Fallbacks: global pkg layout without publisher let encoded_stem = Self::url_encode(fmri.stem()); let encoded_version = Self::url_encode(&version); - let alt1 = self.path.join("pkg").join(&encoded_stem).join(&encoded_version); + let alt1 = self + .path + .join("pkg") + .join(&encoded_stem) + .join(&encoded_version); if alt1.exists() { return crate::actions::Manifest::parse_file(&alt1).map_err(RepositoryError::from); } @@ -1520,10 +1577,10 @@ impl WritableRepository for FileBackend { let config_path = self.path.join(REPOSITORY_CONFIG_FILENAME); let config_data = serde_json::to_string_pretty(&self.config)?; fs::write(config_path, config_data)?; - + // Save the legacy INI format for backward compatibility self.save_legacy_config()?; - + Ok(()) } @@ -1744,7 +1801,10 @@ impl FileBackend { locale: &str, fmri: &crate::fmri::Fmri, op_type: crate::repository::catalog::CatalogOperationType, - catalog_parts: std::collections::HashMap>>, + catalog_parts: std::collections::HashMap< + String, + std::collections::HashMap>, + >, signature_sha1: Option, ) -> Result<()> { let catalog_dir = Self::construct_catalog_path(&self.path, publisher); @@ -1816,19 +1876,29 @@ impl FileBackend { // Require a concrete version let version = fmri.version(); if version.is_empty() { - return Err(RepositoryError::Other("FMRI must include a version to fetch manifest".into())); + return Err(RepositoryError::Other( + "FMRI must include a version to fetch manifest".into(), + )); } // Preferred path: publisher-scoped manifest path let path = Self::construct_manifest_path(&self.path, publisher, fmri.stem(), &version); if path.exists() { - return std::fs::read_to_string(&path).map_err(|e| RepositoryError::FileReadError { path, source: e }); + return std::fs::read_to_string(&path) + .map_err(|e| RepositoryError::FileReadError { path, source: e }); } // Fallbacks: global pkg layout without publisher let encoded_stem = Self::url_encode(fmri.stem()); let encoded_version = Self::url_encode(&version); - let alt1 = self.path.join("pkg").join(&encoded_stem).join(&encoded_version); + let alt1 = self + .path + .join("pkg") + .join(&encoded_stem) + .join(&encoded_version); if alt1.exists() { - return std::fs::read_to_string(&alt1).map_err(|e| RepositoryError::FileReadError { path: alt1, source: e }); + return std::fs::read_to_string(&alt1).map_err(|e| RepositoryError::FileReadError { + path: alt1, + source: e, + }); } let alt2 = self .path @@ -1838,9 +1908,15 @@ impl FileBackend { .join(&encoded_stem) .join(&encoded_version); if alt2.exists() { - return std::fs::read_to_string(&alt2).map_err(|e| RepositoryError::FileReadError { path: alt2, source: e }); + return std::fs::read_to_string(&alt2).map_err(|e| RepositoryError::FileReadError { + path: alt2, + source: e, + }); } - Err(RepositoryError::NotFound(format!("manifest for {} not found", fmri))) + Err(RepositoryError::NotFound(format!( + "manifest for {} not found", + fmri + ))) } /// Fetch catalog file path pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result { @@ -1865,32 +1941,31 @@ impl FileBackend { pub fn save_legacy_config(&self) -> Result<()> { let legacy_config_path = self.path.join("pkg5.repository"); let mut conf = Ini::new(); - + // Add publisher section with default publisher if let Some(default_publisher) = &self.config.default_publisher { conf.with_section(Some("publisher")) .set("prefix", default_publisher); } - + // Add repository section with version and default values conf.with_section(Some("repository")) .set("version", "4") .set("trust-anchor-directory", "/etc/certs/CA/") .set("signature-required-names", "[]") .set("check-certificate-revocation", "False"); - + // Add CONFIGURATION section with version - conf.with_section(Some("CONFIGURATION")) - .set("version", "4"); - + conf.with_section(Some("CONFIGURATION")).set("version", "4"); + // Write the INI file conf.write_to_file(legacy_config_path)?; - + Ok(()) } /// Create a pub.p5i file for a publisher for backward compatibility - /// + /// /// Format: base_path/publisher/publisher_name/pub.p5i fn create_pub_p5i_file(&self, publisher: &str) -> Result<()> { // Define the structure for the pub.p5i file @@ -1937,17 +2012,14 @@ impl FileBackend { } /// Helper method to construct a catalog path consistently - /// + /// /// Format: base_path/publisher/publisher_name/catalog - pub fn construct_catalog_path( - base_path: &Path, - publisher: &str, - ) -> PathBuf { + pub fn construct_catalog_path(base_path: &Path, publisher: &str) -> PathBuf { base_path.join("publisher").join(publisher).join("catalog") } /// Helper method to construct a manifest path consistently - /// + /// /// Format: base_path/publisher/publisher_name/pkg/stem/encoded_version pub fn construct_manifest_path( base_path: &Path, @@ -1959,27 +2031,24 @@ impl FileBackend { let encoded_version = Self::url_encode(version); pkg_dir.join(encoded_version) } - + /// Helper method to construct a package directory path consistently - /// + /// /// Format: base_path/publisher/publisher_name/pkg/url_encoded_stem - pub fn construct_package_dir( - base_path: &Path, - publisher: &str, - stem: &str, - ) -> PathBuf { + pub fn construct_package_dir(base_path: &Path, publisher: &str, stem: &str) -> PathBuf { let encoded_stem = Self::url_encode(stem); - base_path.join("publisher").join(publisher).join("pkg").join(encoded_stem) + base_path + .join("publisher") + .join(publisher) + .join("pkg") + .join(encoded_stem) } - + /// Helper method to construct a file path consistently - /// + /// /// Format: base_path/file/XX/hash /// Where XX is the first two characters of the hash - pub fn construct_file_path( - base_path: &Path, - hash: &str, - ) -> PathBuf { + pub fn construct_file_path(base_path: &Path, hash: &str) -> PathBuf { if hash.len() < 2 { // Fallback for very short hashes (shouldn't happen with SHA256) base_path.join("file").join(hash) @@ -1988,15 +2057,12 @@ impl FileBackend { let first_two = &hash[0..2]; // Create the path: $REPO/file/XX/XXYY... - base_path - .join("file") - .join(first_two) - .join(hash) + base_path.join("file").join(first_two).join(hash) } } - + /// Helper method to construct a file path consistently with publisher - /// + /// /// Format: base_path/publisher/publisher_name/file/XX/hash /// Where XX is the first two characters of the hash pub fn construct_file_path_with_publisher( @@ -2006,7 +2072,11 @@ impl FileBackend { ) -> PathBuf { if hash.len() < 2 { // Fallback for very short hashes (shouldn't happen with SHA256) - base_path.join("publisher").join(publisher).join("file").join(hash) + base_path + .join("publisher") + .join(publisher) + .join("file") + .join(hash) } else { // Extract the first two characters from the hash let first_two = &hash[0..2]; @@ -2094,7 +2164,10 @@ impl FileBackend { } Err(err) => { // Log the error but fall back to the simple string contains - error!("FileBackend::find_manifests_recursive: Error compiling regex pattern '{}': {}", pat, err); + error!( + "FileBackend::find_manifests_recursive: Error compiling regex pattern '{}': {}", + pat, err + ); if !parsed_fmri.stem().contains(pat) { continue; } @@ -2111,20 +2184,22 @@ impl FileBackend { } else { parsed_fmri.clone() }; - + // Check if the package is obsoleted - let is_obsoleted = if let Some(obsoleted_manager) = &self.obsoleted_manager { - obsoleted_manager.borrow().is_obsoleted(publisher, &final_fmri) + let is_obsoleted = if let Some(obsoleted_manager) = + &self.obsoleted_manager + { + obsoleted_manager + .borrow() + .is_obsoleted(publisher, &final_fmri) } else { false }; - + // Only add the package if it's not obsoleted if !is_obsoleted { // Create a PackageInfo struct and add it to the list - packages.push(PackageInfo { - fmri: final_fmri, - }); + packages.push(PackageInfo { fmri: final_fmri }); } // Found the package info, no need to check other attributes @@ -2186,7 +2261,7 @@ impl FileBackend { opts: crate::repository::BatchOptions, ) -> Result<()> { info!("Rebuilding catalog (batched) for publisher: {}", publisher); - + // Create the catalog directory for the publisher if it doesn't exist let catalog_dir = Self::construct_catalog_path(&self.path, publisher); debug!("Publisher catalog directory: {}", catalog_dir.display()); @@ -2245,7 +2320,11 @@ impl FileBackend { } // Read the manifest content for hash calculation - let manifest_content = fs::read_to_string(&manifest_path).map_err(|e| RepositoryError::FileReadError { path: manifest_path.clone(), source: e })?; + let manifest_content = + fs::read_to_string(&manifest_path).map_err(|e| RepositoryError::FileReadError { + path: manifest_path.clone(), + source: e, + })?; // Parse the manifest using parse_file which handles JSON correctly let manifest = Manifest::parse_file(&manifest_path)?; @@ -2334,7 +2413,12 @@ impl FileBackend { processed_in_batch += 1; if processed_in_batch >= opts.batch_size { batch_no += 1; - tracing::debug!(publisher, batch_no, processed_in_batch, "catalog rebuild batch processed"); + tracing::debug!( + publisher, + batch_no, + processed_in_batch, + "catalog rebuild batch processed" + ); processed_in_batch = 0; } } @@ -2407,7 +2491,8 @@ impl FileBackend { for (fmri, actions, signature) in dependency_entries { dependency_part.add_package(publisher, &fmri, actions, Some(signature)); } - let dependency_sig = catalog_writer::write_catalog_part(&dependency_part_path, &mut dependency_part)?; + let dependency_sig = + catalog_writer::write_catalog_part(&dependency_part_path, &mut dependency_part)?; debug!("Wrote dependency part file"); // Summary part @@ -2417,7 +2502,8 @@ impl FileBackend { for (fmri, actions, signature) in summary_entries { summary_part.add_package(publisher, &fmri, actions, Some(signature)); } - let summary_sig = catalog_writer::write_catalog_part(&summary_part_path, &mut summary_part)?; + let summary_sig = + catalog_writer::write_catalog_part(&summary_part_path, &mut summary_part)?; debug!("Wrote summary part file"); // Update part signatures in attrs (written after parts) @@ -2495,29 +2581,46 @@ impl FileBackend { // Ensure catalog dir exists let catalog_dir = Self::construct_catalog_path(&self.path, publisher); - std::fs::create_dir_all(&catalog_dir).map_err(|e| RepositoryError::DirectoryCreateError { path: catalog_dir.clone(), source: e })?; + std::fs::create_dir_all(&catalog_dir).map_err(|e| { + RepositoryError::DirectoryCreateError { + path: catalog_dir.clone(), + source: e, + } + })?; // Serialize JSON - let json = serde_json::to_vec_pretty(log) - .map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; + let json = serde_json::to_vec_pretty(log).map_err(|e| { + RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)) + })?; // Write atomically let target = catalog_dir.join(log_filename); let tmp = target.with_extension("tmp"); { - let mut f = std::fs::File::create(&tmp) - .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + let mut f = + std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError { + path: tmp.clone(), + source: e, + })?; use std::io::Write as _; f.write_all(&json) - .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; - f.flush().map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + .map_err(|e| RepositoryError::FileWriteError { + path: tmp.clone(), + source: e, + })?; + f.flush().map_err(|e| RepositoryError::FileWriteError { + path: tmp.clone(), + source: e, + })?; } - std::fs::rename(&tmp, &target) - .map_err(|e| RepositoryError::FileWriteError { path: target.clone(), source: e })?; + std::fs::rename(&tmp, &target).map_err(|e| RepositoryError::FileWriteError { + path: target.clone(), + source: e, + })?; Ok(()) } - + /// Generate the file path for a given hash using the new directory structure with publisher /// This is a wrapper around the construct_file_path_with_publisher helper method fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf { @@ -2528,7 +2631,7 @@ impl FileBackend { /// /// This method returns a mutable reference to the catalog manager. /// It uses interior mutability with RefCell to allow mutation through an immutable reference. - /// + /// /// The catalog manager is specific to the given publisher. pub fn get_catalog_manager( &mut self, @@ -2536,7 +2639,8 @@ impl FileBackend { ) -> Result> { if self.catalog_manager.is_none() { let publisher_dir = self.path.join("publisher"); - let manager = crate::repository::catalog::CatalogManager::new(&publisher_dir, publisher)?; + let manager = + crate::repository::catalog::CatalogManager::new(&publisher_dir, publisher)?; let refcell = std::cell::RefCell::new(manager); self.catalog_manager = Some(refcell); } @@ -2544,7 +2648,7 @@ impl FileBackend { // This is safe because we just checked that catalog_manager is Some Ok(self.catalog_manager.as_ref().unwrap().borrow_mut()) } - + /// Get or initialize the obsoleted package manager /// /// This method returns a mutable reference to the obsoleted package manager. @@ -2597,7 +2701,7 @@ impl FileBackend { .filter_map(|e| e.ok()) { let path = entry.path(); - + if path.is_file() { // Try to read the first few bytes of the file to check if it's a manifest file let mut file = match fs::File::open(&path) { @@ -2669,18 +2773,17 @@ impl FileBackend { None }; - let directories = - if !manifest.directories.is_empty() { - Some( - manifest - .directories - .iter() - .map(|d| d.path.clone()) - .collect(), - ) - } else { - None - }; + let directories = if !manifest.directories.is_empty() { + Some( + manifest + .directories + .iter() + .map(|d| d.path.clone()) + .collect(), + ) + } else { + None + }; let links = if !manifest.links.is_empty() { Some( @@ -2694,22 +2797,20 @@ impl FileBackend { None }; - let dependencies = - if !manifest.dependencies.is_empty() { - Some( - manifest - .dependencies - .iter() - .filter_map(|d| { - d.fmri - .as_ref() - .map(|f| f.to_string()) - }) - .collect(), - ) - } else { - None - }; + let dependencies = if !manifest.dependencies.is_empty() + { + Some( + manifest + .dependencies + .iter() + .filter_map(|d| { + d.fmri.as_ref().map(|f| f.to_string()) + }) + .collect(), + ) + } else { + None + }; let licenses = if !manifest.licenses.is_empty() { Some( @@ -2746,8 +2847,11 @@ impl FileBackend { }; // Add the package to the index - index.add_package(&package_info, Some(&package_contents)); - + index.add_package( + &package_info, + Some(&package_contents), + ); + // Found the package info, no need to check other attributes break; } diff --git a/libips/src/repository/mod.rs b/libips/src/repository/mod.rs index 88f613b..865c639 100644 --- a/libips/src/repository/mod.rs +++ b/libips/src/repository/mod.rs @@ -216,8 +216,8 @@ impl From for RepositoryError { } } pub mod catalog; -pub(crate) mod file_backend; mod catalog_writer; +pub(crate) mod file_backend; mod obsoleted; pub mod progress; mod rest_backend; @@ -231,7 +231,7 @@ pub use catalog::{ }; pub use file_backend::FileBackend; pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata}; -pub use progress::{ProgressInfo, ProgressReporter, NoopProgressReporter}; +pub use progress::{NoopProgressReporter, ProgressInfo, ProgressReporter}; pub use rest_backend::RestBackend; /// Repository configuration filename @@ -248,7 +248,10 @@ pub struct BatchOptions { impl Default for BatchOptions { fn default() -> Self { - BatchOptions { batch_size: 2000, flush_every_n: 1 } + BatchOptions { + batch_size: 2000, + flush_every_n: 1, + } } } @@ -367,12 +370,7 @@ pub trait ReadableRepository { /// Fetch a content payload identified by digest into the destination path. /// Implementations should download/copy the payload to a temporary path, /// verify integrity, and atomically move into `dest`. - fn fetch_payload( - &mut self, - publisher: &str, - digest: &str, - dest: &Path, - ) -> Result<()>; + fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()>; /// Fetch a package manifest by FMRI from the repository. /// Implementations should retrieve and parse the manifest for the given diff --git a/libips/src/repository/obsoleted.rs b/libips/src/repository/obsoleted.rs index f57b2c5..2227a0a 100644 --- a/libips/src/repository/obsoleted.rs +++ b/libips/src/repository/obsoleted.rs @@ -1,17 +1,17 @@ use crate::fmri::Fmri; -use crate::repository::{Result, RepositoryError}; +use crate::repository::{RepositoryError, Result}; use chrono::{DateTime, Duration as ChronoDuration, Utc}; use miette::Diagnostic; -use regex::Regex; use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition}; +use regex::Regex; use serde::{Deserialize, Serialize}; -use serde_json; use serde_cbor; +use serde_json; +use sha2::Digest; use std::fs; use std::path::{Path, PathBuf}; use std::sync::RwLock; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use sha2::Digest; use thiserror::Error; use tracing::{debug, error, info, warn}; @@ -106,49 +106,49 @@ pub enum ObsoletedPackageError { help("Check system resources and permissions") )] IoError(String), - + #[error("failed to remove obsoleted package: {0}")] #[diagnostic( code(ips::obsoleted_package_error::remove), help("Check that the package exists and is not in use") )] RemoveError(String), - + #[error("invalid pagination parameters: {0}")] #[diagnostic( code(ips::obsoleted_package_error::pagination), help("Check that the page number and page size are valid") )] PaginationError(String), - + #[error("search pattern error: {0}")] #[diagnostic( code(ips::obsoleted_package_error::search), help("Check that the search pattern is valid") )] SearchPatternError(String), - + #[error("index error: {0}")] #[diagnostic( code(ips::obsoleted_package_error::index), help("An error occurred with the obsoleted package index") )] IndexError(String), - + #[error("cache error: {0}")] #[diagnostic( code(ips::obsoleted_package_error::cache), help("An error occurred with the obsoleted package cache") )] CacheError(String), - + #[error("database error: {0}")] #[diagnostic( code(ips::obsoleted_package_error::database), help("An error occurred with the obsoleted package database") )] DatabaseError(String), - + #[error("serialization error: {0}")] #[diagnostic( code(ips::obsoleted_package_error::serialization), @@ -230,7 +230,9 @@ impl From for RepositoryError { fn from(err: ObsoletedPackageError) -> Self { match err { ObsoletedPackageError::NotFound(msg) => RepositoryError::NotFound(msg), - ObsoletedPackageError::IoError(msg) => RepositoryError::IoError(std::io::Error::new(std::io::ErrorKind::Other, msg)), + ObsoletedPackageError::IoError(msg) => { + RepositoryError::IoError(std::io::Error::new(std::io::ErrorKind::Other, msg)) + } _ => RepositoryError::Other(err.to_string()), } } @@ -271,7 +273,7 @@ impl ObsoletedPackageKey { version: fmri.version().to_string(), } } - + /// Create a new ObsoletedPackageKey from components fn from_components(publisher: &str, stem: &str, version: &str) -> Self { Self { @@ -280,47 +282,56 @@ impl ObsoletedPackageKey { version: version.to_string(), } } - + /// Get the FMRI string for this key fn to_fmri_string(&self) -> String { format!("pkg://{}/{}@{}", self.publisher, self.stem, self.version) } - + /// Parse an FMRI string and create a key from it fn from_fmri_string(fmri: &str) -> Result { // Parse the FMRI string to extract publisher, stem, and version // Format: pkg://publisher/stem@version - + // Remove the pkg:// prefix if present let fmri = fmri.trim_start_matches("pkg://"); - + // Split by / to get publisher and the rest let parts: Vec<&str> = fmri.splitn(2, '/').collect(); if parts.len() != 2 { - return Err(ObsoletedPackageError::FmriParseError(format!("Invalid FMRI format: {}", fmri)).into()); + return Err(ObsoletedPackageError::FmriParseError(format!( + "Invalid FMRI format: {}", + fmri + )) + .into()); } - + let publisher = parts[0]; - + // Split the rest by @ to get stem and version let parts: Vec<&str> = parts[1].splitn(2, '@').collect(); if parts.len() != 2 { - return Err(ObsoletedPackageError::FmriParseError(format!("Invalid FMRI format: {}", fmri)).into()); + return Err(ObsoletedPackageError::FmriParseError(format!( + "Invalid FMRI format: {}", + fmri + )) + .into()); } - + let stem = parts[0]; let version = parts[1]; - + Ok(Self::from_components(publisher, stem, version)) } } - // Table definitions for the redb database // Table for mapping FMRI directly to metadata -static FMRI_TO_METADATA_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("fmri_to_metadata"); +static FMRI_TO_METADATA_TABLE: TableDefinition<&[u8], &[u8]> = + TableDefinition::new("fmri_to_metadata"); // Table for mapping content hash to manifest (for non-NULL_HASH entries) -static HASH_TO_MANIFEST_TABLE: TableDefinition<&str, &str> = TableDefinition::new("hash_to_manifest"); +static HASH_TO_MANIFEST_TABLE: TableDefinition<&str, &str> = + TableDefinition::new("hash_to_manifest"); /// Index of obsoleted packages using redb for faster lookups and content-addressable storage #[derive(Debug)] @@ -340,10 +351,10 @@ impl RedbObsoletedPackageIndex { fn new>(base_path: P) -> Result { let db_path = base_path.as_ref().join("index.redb"); debug!("Creating redb database at {}", db_path.display()); - + // Create the database let db = Database::create(&db_path)?; - + // Create the tables if they don't exist let write_txn = db.begin_write()?; { @@ -352,7 +363,7 @@ impl RedbObsoletedPackageIndex { write_txn.open_table(HASH_TO_MANIFEST_TABLE)?; } write_txn.commit()?; - + Ok(Self { db, last_accessed: Instant::now(), @@ -360,34 +371,34 @@ impl RedbObsoletedPackageIndex { max_age: Duration::from_secs(300), // 5 minutes }) } - + /// Check if the index is stale and needs to be rebuilt fn is_stale(&self) -> bool { self.dirty || self.last_accessed.elapsed() > self.max_age } - + /// Create an empty temporary file-based RedbObsoletedPackageIndex - /// + /// /// This is used as a fallback when the database creation fails. /// It creates a database in a temporary directory that can be used temporarily. fn empty() -> Self { debug!("Creating empty temporary file-based redb database"); - + // Create a temporary directory let temp_dir = tempfile::tempdir().unwrap_or_else(|e| { error!("Failed to create temporary directory: {}", e); panic!("Failed to create temporary directory: {}", e); }); - + // Create a database file in the temporary directory let db_path = temp_dir.path().join("empty.redb"); - + // Create the database let db = Database::create(&db_path).unwrap_or_else(|e| { error!("Failed to create temporary database: {}", e); panic!("Failed to create temporary database: {}", e); }); - + // Create the tables let write_txn = db.begin_write().unwrap(); { @@ -396,7 +407,7 @@ impl RedbObsoletedPackageIndex { let _ = write_txn.open_table(HASH_TO_MANIFEST_TABLE).unwrap(); } write_txn.commit().unwrap(); - + Self { db, last_accessed: Instant::now(), @@ -404,12 +415,12 @@ impl RedbObsoletedPackageIndex { max_age: Duration::from_secs(300), // 5 minutes } } - + /// Open an existing RedbObsoletedPackageIndex fn open>(base_path: P) -> Result { let db_path = base_path.as_ref().join("index.redb"); debug!("Opening redb database at {}", db_path.display()); - + // Open the database let db = Database::open(&db_path)?; @@ -420,23 +431,30 @@ impl RedbObsoletedPackageIndex { max_age: Duration::from_secs(300), // 5 minutes }) } - + /// Create or open a RedbObsoletedPackageIndex fn create_or_open>(base_path: P) -> Result { let db_path = base_path.as_ref().join("index.redb"); - + if db_path.exists() { Self::open(base_path) } else { Self::new(base_path) } } - + /// Add an entry to the index - fn add_entry(&self, key: &ObsoletedPackageKey, metadata: &ObsoletedPackageMetadata, manifest: &str) -> Result<()> { - debug!("Adding entry to index: publisher={}, stem={}, version={}, fmri={}", - key.publisher, key.stem, key.version, metadata.fmri); - + fn add_entry( + &self, + key: &ObsoletedPackageKey, + metadata: &ObsoletedPackageMetadata, + manifest: &str, + ) -> Result<()> { + debug!( + "Adding entry to index: publisher={}, stem={}, version={}, fmri={}", + key.publisher, key.stem, key.version, metadata.fmri + ); + // Calculate content hash if not already present let content_hash = if metadata.content_hash.is_empty() { let mut hasher = sha2::Sha256::new(); @@ -445,18 +463,22 @@ impl RedbObsoletedPackageIndex { } else { metadata.content_hash.clone() }; - + // Use the FMRI string directly as the key let key_bytes = metadata.fmri.as_bytes(); - + let metadata_bytes = match serde_cbor::to_vec(metadata) { Ok(bytes) => bytes, Err(e) => { error!("Failed to serialize metadata with CBOR: {}", e); - return Err(ObsoletedPackageError::SerializationError(format!("Failed to serialize metadata with CBOR: {}", e)).into()); + return Err(ObsoletedPackageError::SerializationError(format!( + "Failed to serialize metadata with CBOR: {}", + e + )) + .into()); } }; - + // Begin write transaction let write_txn = match self.db.begin_write() { Ok(txn) => txn, @@ -465,7 +487,7 @@ impl RedbObsoletedPackageIndex { return Err(e.into()); } }; - + { // Open the tables let mut fmri_to_metadata = match write_txn.open_table(FMRI_TO_METADATA_TABLE) { @@ -475,7 +497,7 @@ impl RedbObsoletedPackageIndex { return Err(e.into()); } }; - + let mut hash_to_manifest = match write_txn.open_table(HASH_TO_MANIFEST_TABLE) { Ok(table) => table, Err(e) => { @@ -483,14 +505,14 @@ impl RedbObsoletedPackageIndex { return Err(e.into()); } }; - + // Insert the metadata directly with FMRI as the key // This is the new approach that eliminates the intermediate hash lookup if let Err(e) = fmri_to_metadata.insert(key_bytes, metadata_bytes.as_slice()) { error!("Failed to insert into FMRI_TO_METADATA_TABLE: {}", e); return Err(e.into()); } - + // Only store the manifest if it's not a NULL_HASH entry // For NULL_HASH entries, a minimal manifest will be generated when requested if content_hash != NULL_HASH { @@ -500,35 +522,35 @@ impl RedbObsoletedPackageIndex { } } } - + if let Err(e) = write_txn.commit() { error!("Failed to commit transaction: {}", e); return Err(e.into()); } - + debug!("Successfully added entry to index: {}", metadata.fmri); Ok(()) } - + /// Remove an entry from the index fn remove_entry(&self, key: &ObsoletedPackageKey) -> Result { // Use the FMRI string directly as the key let fmri = key.to_fmri_string(); let key_bytes = fmri.as_bytes(); - + // First, check if the key exists in the new table let exists_in_new_table = { let read_txn = self.db.begin_read()?; let fmri_to_metadata = read_txn.open_table(FMRI_TO_METADATA_TABLE)?; - + fmri_to_metadata.get(key_bytes)?.is_some() }; - + // If the key doesn't exist in either table, return early if !exists_in_new_table { return Ok(false); } - + // Now perform the actual removal let write_txn = self.db.begin_write()?; { @@ -538,23 +560,26 @@ impl RedbObsoletedPackageIndex { fmri_to_metadata.remove(key_bytes)?; } } - + write_txn.commit()?; - + Ok(true) } - + /// Get an entry from the index - fn get_entry(&self, key: &ObsoletedPackageKey) -> Result> { + fn get_entry( + &self, + key: &ObsoletedPackageKey, + ) -> Result> { // Use the FMRI string directly as the key let fmri = key.to_fmri_string(); let key_bytes = fmri.as_bytes(); - + // First, try to get the metadata directly from the new table let metadata_result = { let read_txn = self.db.begin_read()?; let fmri_to_metadata = read_txn.open_table(FMRI_TO_METADATA_TABLE)?; - + // Get the metadata bytes match fmri_to_metadata.get(key_bytes)? { Some(bytes) => { @@ -564,15 +589,18 @@ impl RedbObsoletedPackageIndex { match serde_cbor::from_slice::(&metadata_bytes) { Ok(metadata) => Some(metadata), Err(e) => { - warn!("Failed to deserialize metadata from FMRI_TO_METADATA_TABLE with CBOR: {}", e); + warn!( + "Failed to deserialize metadata from FMRI_TO_METADATA_TABLE with CBOR: {}", + e + ); None } } - }, + } None => None, } }; - + // If we found the metadata in the new table, use it if let Some(metadata) = metadata_result { // Get the content hash from the metadata @@ -582,7 +610,8 @@ impl RedbObsoletedPackageIndex { let manifest_str = if content_hash == NULL_HASH { // Generate a minimal manifest for NULL_HASH entries // Construct an FMRI string from the metadata - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -597,7 +626,9 @@ impl RedbObsoletedPackageIndex { ] }} ] -}}"#, metadata.fmri) +}}"#, + metadata.fmri + ) } else { // For non-NULL_HASH entries, get the manifest from the database let read_txn = self.db.begin_read()?; @@ -607,9 +638,13 @@ impl RedbObsoletedPackageIndex { match hash_to_manifest.get(content_hash.as_str())? { Some(manifest) => manifest.value().to_string(), None => { - warn!("Manifest not found for content hash: {}, generating minimal manifest", content_hash); + warn!( + "Manifest not found for content hash: {}, generating minimal manifest", + content_hash + ); // Generate a minimal manifest as a fallback - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -624,7 +659,9 @@ impl RedbObsoletedPackageIndex { ] }} ] -}}"#, metadata.fmri) +}}"#, + metadata.fmri + ) } } }; @@ -633,26 +670,28 @@ impl RedbObsoletedPackageIndex { Ok(None) } } - + /// Get all entries in the index - fn get_all_entries(&self) -> Result> { + fn get_all_entries( + &self, + ) -> Result> { let mut entries = Vec::new(); let mut processed_keys = std::collections::HashSet::new(); - + // First, collect all entries from the new table { let read_txn = self.db.begin_read()?; let fmri_to_metadata = read_txn.open_table(FMRI_TO_METADATA_TABLE)?; - + let mut iter = fmri_to_metadata.iter()?; - + while let Some(entry) = iter.next() { let (key_bytes, metadata_bytes) = entry?; - + // Convert to owned types before the transaction is dropped let key_data = key_bytes.value().to_vec(); let metadata_data = metadata_bytes.value().to_vec(); - + // Convert key bytes to string and parse as FMRI let fmri_str = match std::str::from_utf8(&key_data) { Ok(s) => s, @@ -661,7 +700,7 @@ impl RedbObsoletedPackageIndex { continue; } }; - + // Parse the FMRI string to create an ObsoletedPackageKey let key = match ObsoletedPackageKey::from_fmri_string(fmri_str) { Ok(key) => key, @@ -670,25 +709,31 @@ impl RedbObsoletedPackageIndex { continue; } }; - - let metadata: ObsoletedPackageMetadata = match serde_cbor::from_slice(&metadata_data) { + + let metadata: ObsoletedPackageMetadata = match serde_cbor::from_slice( + &metadata_data, + ) { Ok(metadata) => metadata, Err(e) => { - warn!("Failed to deserialize metadata from FMRI_TO_METADATA_TABLE with CBOR: {}", e); + warn!( + "Failed to deserialize metadata from FMRI_TO_METADATA_TABLE with CBOR: {}", + e + ); continue; } }; - + // Add the key to the set of processed keys processed_keys.insert(key_data); - + // Get the content hash from the metadata let content_hash = metadata.content_hash.clone(); - + // For NULL_HASH entries, generate a minimal manifest let manifest_str = if content_hash == NULL_HASH { // Generate a minimal manifest for NULL_HASH entries - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -703,18 +748,24 @@ impl RedbObsoletedPackageIndex { ] }} ] -}}"#, metadata.fmri) +}}"#, + metadata.fmri + ) } else { // For non-NULL_HASH entries, get the manifest from the database let hash_to_manifest = read_txn.open_table(HASH_TO_MANIFEST_TABLE)?; - + // Get the manifest string match hash_to_manifest.get(content_hash.as_str())? { Some(manifest) => manifest.value().to_string(), None => { - warn!("Manifest not found for content hash: {}, generating minimal manifest", content_hash); + warn!( + "Manifest not found for content hash: {}, generating minimal manifest", + content_hash + ); // Generate a minimal manifest as a fallback - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -729,42 +780,51 @@ impl RedbObsoletedPackageIndex { ] }} ] -}}"#, metadata.fmri) +}}"#, + metadata.fmri + ) } } }; - + entries.push((key, metadata, manifest_str)); } } - + Ok(entries) } - + /// Get entries matching a publisher - fn get_entries_by_publisher(&self, publisher: &str) -> Result> { + fn get_entries_by_publisher( + &self, + publisher: &str, + ) -> Result> { // Get all entries and filter by publisher // This is more efficient than implementing a separate method with similar logic let all_entries = self.get_all_entries()?; - + // Filter entries by publisher let filtered_entries = all_entries .into_iter() .filter(|(key, _, _)| key.publisher == publisher) .collect(); - + Ok(filtered_entries) } - + /// Search for entries matching a pattern #[allow(dead_code)] - fn search_entries(&self, publisher: &str, pattern: &str) -> Result> { + fn search_entries( + &self, + publisher: &str, + pattern: &str, + ) -> Result> { // Get entries for the publisher let publisher_entries = self.get_entries_by_publisher(publisher)?; - + // Try to compile the pattern as a regex let regex_result = Regex::new(pattern); - + // Filter entries based on the pattern let filtered_entries = match regex_result { Ok(regex) => { @@ -778,7 +838,7 @@ impl RedbObsoletedPackageIndex { regex.is_match(&key.stem) }) .collect() - }, + } Err(_) => { // If regex compilation fails, fall back to simple substring matching publisher_entries @@ -792,10 +852,10 @@ impl RedbObsoletedPackageIndex { .collect() } }; - + Ok(filtered_entries) } - + /// Clear the index fn clear(&self) -> Result<()> { // Begin a writing transaction @@ -825,7 +885,7 @@ impl RedbObsoletedPackageIndex { hash_to_manifest.remove(key.as_slice())?; } } - + // Clear hash_to_manifest table { let mut hash_to_manifest = write_txn.open_table(HASH_TO_MANIFEST_TABLE)?; @@ -841,7 +901,7 @@ impl RedbObsoletedPackageIndex { } keys }; - + // Then remove all keys for key in keys_to_remove { hash_to_manifest.remove(key.as_str())?; @@ -849,37 +909,37 @@ impl RedbObsoletedPackageIndex { } } write_txn.commit()?; - + Ok(()) } - + /// Get the number of entries in the index fn len(&self) -> Result { // Begin a read transaction let read_txn = self.db.begin_read()?; - + // Open the fmri_to_hash table let fmri_to_hash = read_txn.open_table(FMRI_TO_METADATA_TABLE)?; - + // Count the entries let mut count = 0; let mut iter = fmri_to_hash.iter()?; - + // Iterate through all entries and count them while let Some(entry_result) = iter.next() { // Just check if the entry exists, we don't need to access its value entry_result?; count += 1; } - + // Drop the iterator and table before returning drop(iter); drop(fmri_to_hash); drop(read_txn); - + Ok(count) } - + /// Check if the index is empty #[allow(dead_code)] fn is_empty(&self) -> Result { @@ -887,8 +947,6 @@ impl RedbObsoletedPackageIndex { } } - - /// Constant for null hash value, indicating no manifest content is stored /// When this value is used for content_hash, the original manifest is not stored, /// and a minimal manifest with obsoletion attributes is generated on-the-fly when requested @@ -899,24 +957,24 @@ pub const NULL_HASH: &str = "null"; pub struct ObsoletedPackageMetadata { /// The FMRI of the obsoleted package pub fmri: String, - + /// The status of the package (always "obsolete") pub status: String, - + /// The date when the package was obsoleted pub obsolescence_date: String, - + /// A message explaining why the package was obsoleted #[serde(skip_serializing_if = "Option::is_none")] pub deprecation_message: Option, - + /// List of FMRIs that replace this package #[serde(skip_serializing_if = "Option::is_none")] pub obsoleted_by: Option>, - + /// Version of the metadata schema pub metadata_version: u32, - + /// Hash of the original manifest content /// If set to NULL_HASH, no manifest content is stored and a minimal manifest /// with obsoletion attributes will be generated when requested. @@ -926,7 +984,6 @@ pub struct ObsoletedPackageMetadata { pub content_hash: String, } - impl ObsoletedPackageMetadata { /// Create a new ObsoletedPackageMetadata instance with the given content hash pub fn new( @@ -958,9 +1015,9 @@ impl ObsoletedPackageMetadata { content_hash: content_hash.to_string(), } } - + /// Create a new ObsoletedPackageMetadata instance with a null hash - /// + /// /// This indicates that no manifest content is stored and a minimal manifest /// with obsoletion attributes will be generated when requested. pub fn new_with_null_hash( @@ -1016,183 +1073,215 @@ impl ObsoletedPackageManager { /// Create a new ObsoletedPackageManager pub fn new>(repo_path: P) -> Self { let base_path = repo_path.as_ref().join("obsoleted"); - + let index = { // Create or open the redb-based index - let redb_index = RedbObsoletedPackageIndex::create_or_open(&base_path).unwrap_or_else(|e| { - // Log the error and create an empty redb index - error!("Failed to create or open redb-based index: {}", e); - RedbObsoletedPackageIndex::empty() - }); + let redb_index = + RedbObsoletedPackageIndex::create_or_open(&base_path).unwrap_or_else(|e| { + // Log the error and create an empty redb index + error!("Failed to create or open redb-based index: {}", e); + RedbObsoletedPackageIndex::empty() + }); RwLock::new(redb_index) }; - - Self { - base_path, - index, - } + + Self { base_path, index } } /// Initialize the obsoleted packages directory structure pub fn init(&self) -> Result<()> { - debug!("Initializing obsoleted packages directory: {}", self.base_path.display()); + debug!( + "Initializing obsoleted packages directory: {}", + self.base_path.display() + ); fs::create_dir_all(&self.base_path)?; - + // Initialize the index self.build_index()?; - + Ok(()) } - + /// Build the index of obsoleted packages fn build_index(&self) -> Result<()> { debug!("Building index of obsoleted packages"); - + // Get a write lock on the index let index = match self.index.write() { Ok(index) => index, Err(e) => { error!("Failed to acquire write lock on index: {}", e); return Err(ObsoletedPackageError::IndexError(format!( - "Failed to acquire write lock on index: {}", e - )).into()); + "Failed to acquire write lock on index: {}", + e + )) + .into()); } }; - + // Clear the index if let Err(e) = index.clear() { error!("Failed to clear index: {}", e); // Continue anyway, as this is not a fatal error } - + // Check if the base path exists if !self.base_path.exists() { - debug!("Obsoleted packages directory does not exist: {}", self.base_path.display()); + debug!( + "Obsoleted packages directory does not exist: {}", + self.base_path.display() + ); return Ok(()); } - + debug!("Base path exists: {}", self.base_path.display()); - + // Walk through the directory structure to find all obsoleted packages - for publisher_entry in fs::read_dir(&self.base_path) - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read obsoleted packages directory {}: {}", - self.base_path.display(), e - )))? - { - let publisher_entry = publisher_entry.map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read publisher entry: {}", e - )))?; - + for publisher_entry in fs::read_dir(&self.base_path).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to read obsoleted packages directory {}: {}", + self.base_path.display(), + e + )) + })? { + let publisher_entry = publisher_entry.map_err(|e| { + ObsoletedPackageError::IoError(format!("Failed to read publisher entry: {}", e)) + })?; + let publisher_path = publisher_entry.path(); if !publisher_path.is_dir() { continue; } - - let publisher = publisher_path.file_name() - .ok_or_else(|| ObsoletedPackageError::IoError(format!( - "Failed to get publisher name from path: {}", - publisher_path.display() - )))? + + let publisher = publisher_path + .file_name() + .ok_or_else(|| { + ObsoletedPackageError::IoError(format!( + "Failed to get publisher name from path: {}", + publisher_path.display() + )) + })? .to_string_lossy() .to_string(); - + debug!("Indexing obsoleted packages for publisher: {}", publisher); - + // Walk through the package directories - for pkg_entry in fs::read_dir(&publisher_path) - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read publisher directory {}: {}", - publisher_path.display(), e - )))? - { - let pkg_entry = pkg_entry.map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read package entry: {}", e - )))?; - + for pkg_entry in fs::read_dir(&publisher_path).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to read publisher directory {}: {}", + publisher_path.display(), + e + )) + })? { + let pkg_entry = pkg_entry.map_err(|e| { + ObsoletedPackageError::IoError(format!("Failed to read package entry: {}", e)) + })?; + let pkg_path = pkg_entry.path(); if !pkg_path.is_dir() { continue; } - - let stem = pkg_path.file_name() - .ok_or_else(|| ObsoletedPackageError::IoError(format!( - "Failed to get package stem from path: {}", - pkg_path.display() - )))? + + let stem = pkg_path + .file_name() + .ok_or_else(|| { + ObsoletedPackageError::IoError(format!( + "Failed to get package stem from path: {}", + pkg_path.display() + )) + })? .to_string_lossy() .to_string(); - + debug!("Indexing obsoleted package: {}", stem); - + // Walk through the version files - for version_entry in fs::read_dir(&pkg_path) - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read package directory {}: {}", - pkg_path.display(), e - )))? - { - let version_entry = version_entry.map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read version entry: {}", e - )))?; - + for version_entry in fs::read_dir(&pkg_path).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to read package directory {}: {}", + pkg_path.display(), + e + )) + })? { + let version_entry = version_entry.map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to read version entry: {}", + e + )) + })?; + let version_path = version_entry.path(); if !version_path.is_file() { continue; } - + // Check if this is a metadata file if let Some(extension) = version_path.extension() { if extension != "json" { continue; } - + // Extract the version from the filename - let filename = version_path.file_stem() - .ok_or_else(|| ObsoletedPackageError::IoError(format!( - "Failed to get version from path: {}", - version_path.display() - )))? + let filename = version_path + .file_stem() + .ok_or_else(|| { + ObsoletedPackageError::IoError(format!( + "Failed to get version from path: {}", + version_path.display() + )) + })? .to_string_lossy() .to_string(); - + // Construct the manifest path let manifest_path = pkg_path.join(format!("{}.manifest", filename)); - + // Get the last modified time of the metadata file - let metadata = fs::metadata(&version_path) - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to get metadata for file {}: {}", - version_path.display(), e - )))?; - - let _last_modified = metadata.modified() - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to get last modified time for file {}: {}", - version_path.display(), e - )))?; - + let metadata = fs::metadata(&version_path).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to get metadata for file {}: {}", + version_path.display(), + e + )) + })?; + + let _last_modified = metadata.modified().map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to get last modified time for file {}: {}", + version_path.display(), + e + )) + })?; + // Create an index entry - let key = ObsoletedPackageKey::from_components(&publisher, &stem, &filename); - + let key = + ObsoletedPackageKey::from_components(&publisher, &stem, &filename); + // Read the metadata file - let metadata_json = fs::read_to_string(&version_path) - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read metadata file {}: {}", - version_path.display(), e - )))?; - + let metadata_json = fs::read_to_string(&version_path).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to read metadata file {}: {}", + version_path.display(), + e + )) + })?; + // Parse the metadata - let metadata: ObsoletedPackageMetadata = serde_json::from_str(&metadata_json) - .map_err(|e| ObsoletedPackageError::MetadataParseError(format!( - "Failed to parse metadata from {}: {}", - version_path.display(), e - )))?; - + let metadata: ObsoletedPackageMetadata = + serde_json::from_str(&metadata_json).map_err(|e| { + ObsoletedPackageError::MetadataParseError(format!( + "Failed to parse metadata from {}: {}", + version_path.display(), + e + )) + })?; + // For NULL_HASH entries, generate a minimal manifest instead of reading the file let manifest_content = if metadata.content_hash == NULL_HASH { // Generate a minimal manifest for NULL_HASH entries - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -1207,80 +1296,102 @@ impl ObsoletedPackageManager { ] }} ] -}}"#, metadata.fmri) +}}"#, + metadata.fmri + ) } else { // For non-NULL_HASH entries, read the manifest file - fs::read_to_string(&manifest_path) - .map_err(|e| ObsoletedPackageError::ManifestReadError(format!( - "Failed to read manifest file {}: {}", - manifest_path.display(), e - )))? + fs::read_to_string(&manifest_path).map_err(|e| { + ObsoletedPackageError::ManifestReadError(format!( + "Failed to read manifest file {}: {}", + manifest_path.display(), + e + )) + })? }; - + // Add the entry to the index index.add_entry(&key, &metadata, &manifest_content)?; } } } } - + // Get the count of indexed packages, handling the Result match index.len() { Ok(count) => debug!("Indexed {} obsoleted packages", count), Err(e) => warn!("Failed to get count of indexed packages: {}", e), } - + Ok(()) } - + /// Ensure the index is fresh, rebuilding it if necessary fn ensure_index_is_fresh(&self) -> Result<()> { // Get a read lock on the index to check if it's stale let is_stale = { - let index = self.index.read().map_err(|e| ObsoletedPackageError::IndexError(format!( - "Failed to acquire read lock on index: {}", e - )))?; - + let index = self.index.read().map_err(|e| { + ObsoletedPackageError::IndexError(format!( + "Failed to acquire read lock on index: {}", + e + )) + })?; + index.is_stale() }; - + // If the index is stale, rebuild it if is_stale { debug!("Index is stale, rebuilding"); self.build_index()?; } - + Ok(()) } - + /// Update an entry in the index - fn update_index_entry(&self, publisher: &str, fmri: &Fmri, metadata_path: &Path, manifest_path: &Path) -> Result<()> { + fn update_index_entry( + &self, + publisher: &str, + fmri: &Fmri, + metadata_path: &Path, + manifest_path: &Path, + ) -> Result<()> { // Get a write lock on the index - let index = self.index.write().map_err(|e| ObsoletedPackageError::IndexError(format!( - "Failed to acquire write lock on index: {}", e - )))?; - + let index = self.index.write().map_err(|e| { + ObsoletedPackageError::IndexError(format!( + "Failed to acquire write lock on index: {}", + e + )) + })?; + // Create the key let key = ObsoletedPackageKey::new(publisher, fmri); - + // Read the metadata file - let metadata_json = fs::read_to_string(metadata_path) - .map_err(|e| ObsoletedPackageError::MetadataReadError(format!( - "Failed to read metadata file {}: {}", - metadata_path.display(), e - )))?; - + let metadata_json = fs::read_to_string(metadata_path).map_err(|e| { + ObsoletedPackageError::MetadataReadError(format!( + "Failed to read metadata file {}: {}", + metadata_path.display(), + e + )) + })?; + // Parse the metadata - let metadata: ObsoletedPackageMetadata = serde_json::from_str(&metadata_json) - .map_err(|e| ObsoletedPackageError::MetadataParseError(format!( - "Failed to parse metadata from {}: {}", - metadata_path.display(), e - )))?; - + let metadata: ObsoletedPackageMetadata = + serde_json::from_str(&metadata_json).map_err(|e| { + ObsoletedPackageError::MetadataParseError(format!( + "Failed to parse metadata from {}: {}", + metadata_path.display(), + e + )) + })?; + // For NULL_HASH entries, generate a minimal manifest instead of reading the file let manifest_content = if metadata.content_hash == NULL_HASH { // Generate a minimal manifest for NULL_HASH entries - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -1295,19 +1406,23 @@ impl ObsoletedPackageManager { ] }} ] -}}"#, metadata.fmri) +}}"#, + metadata.fmri + ) } else { // For non-NULL_HASH entries, read the manifest file - fs::read_to_string(manifest_path) - .map_err(|e| ObsoletedPackageError::ManifestReadError(format!( - "Failed to read manifest file {}: {}", - manifest_path.display(), e - )))? + fs::read_to_string(manifest_path).map_err(|e| { + ObsoletedPackageError::ManifestReadError(format!( + "Failed to read manifest file {}: {}", + manifest_path.display(), + e + )) + })? }; - + // Add the entry to the index index.add_entry(&key, &metadata, &manifest_content)?; - + Ok(()) } @@ -1398,7 +1513,10 @@ impl ObsoletedPackageManager { warn!("Failed to add package to index: {}", e); } } else { - warn!("Failed to acquire write lock on index, package not added to index: {}", fmri); + warn!( + "Failed to acquire write lock on index, package not added to index: {}", + fmri + ); } info!("Stored obsoleted package: {}", fmri); @@ -1411,19 +1529,23 @@ impl ObsoletedPackageManager { let stem = fmri.stem(); let version = fmri.version(); let encoded_version = url_encode(&version); - let metadata_path = self.base_path.join(publisher).join(stem).join(format!("{}.json", encoded_version)); - + let metadata_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.json", encoded_version)); + if metadata_path.exists() { return true; } - + // Ensure the index is fresh if let Err(e) = self.ensure_index_is_fresh() { warn!("Failed to ensure index is fresh: {}", e); // Already checked the filesystem above, so return false return false; } - + // Check the index let key = ObsoletedPackageKey::new(publisher, fmri); match self.index.read() { @@ -1437,7 +1559,7 @@ impl ObsoletedPackageManager { false } } - }, + } Err(e) => { warn!("Failed to acquire read lock on index: {}", e); // Already checked the filesystem above, so return false @@ -1458,10 +1580,10 @@ impl ObsoletedPackageManager { // Fall back to the filesystem check if the index is not available return self.get_obsoleted_package_metadata_from_filesystem(publisher, fmri); } - + // Check the index let key = ObsoletedPackageKey::new(publisher, fmri); - + // Try to get a read lock on the index let index_read_result = self.index.read(); if let Err(e) = index_read_result { @@ -1469,19 +1591,19 @@ impl ObsoletedPackageManager { // Fall back to the filesystem check if the index is not available return self.get_obsoleted_package_metadata_from_filesystem(publisher, fmri); } - + let index = index_read_result.unwrap(); - + // Check if the package is in the index match index.get_entry(&key) { Ok(Some((metadata, _))) => { // Return the metadata directly from the index Ok(Some(metadata)) - }, + } Ok(None) => { // Package not found in the index, fall back to the filesystem check self.get_obsoleted_package_metadata_from_filesystem(publisher, fmri) - }, + } Err(e) => { warn!("Failed to get entry from index: {}", e); // Fall back to the filesystem to check if there's an error @@ -1489,7 +1611,7 @@ impl ObsoletedPackageManager { } } } - + /// Get metadata for an obsoleted package from the filesystem fn get_obsoleted_package_metadata_from_filesystem( &self, @@ -1499,8 +1621,16 @@ impl ObsoletedPackageManager { let stem = fmri.stem(); let version = fmri.version(); let encoded_version = url_encode(&version); - let metadata_path = self.base_path.join(publisher).join(stem).join(format!("{}.json", encoded_version)); - let manifest_path = self.base_path.join(publisher).join(stem).join(format!("{}.manifest", encoded_version)); + let metadata_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.json", encoded_version)); + let manifest_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.manifest", encoded_version)); if !metadata_path.exists() { debug!("Metadata file not found: {}", metadata_path.display()); @@ -1508,29 +1638,35 @@ impl ObsoletedPackageManager { } // Read the metadata file - let metadata_json = fs::read_to_string(&metadata_path) - .map_err(|e| ObsoletedPackageError::MetadataReadError(format!( - "Failed to read metadata file {}: {}", - metadata_path.display(), e - )))?; - + let metadata_json = fs::read_to_string(&metadata_path).map_err(|e| { + ObsoletedPackageError::MetadataReadError(format!( + "Failed to read metadata file {}: {}", + metadata_path.display(), + e + )) + })?; + // Parse the metadata JSON - let metadata: ObsoletedPackageMetadata = serde_json::from_str(&metadata_json) - .map_err(|e| ObsoletedPackageError::MetadataParseError(format!( - "Failed to parse metadata from {}: {}", - metadata_path.display(), e - )))?; - + let metadata: ObsoletedPackageMetadata = + serde_json::from_str(&metadata_json).map_err(|e| { + ObsoletedPackageError::MetadataParseError(format!( + "Failed to parse metadata from {}: {}", + metadata_path.display(), + e + )) + })?; + // Update the index with this package if metadata_path.exists() && manifest_path.exists() { - if let Err(e) = self.update_index_entry(publisher, fmri, &metadata_path, &manifest_path) { + if let Err(e) = self.update_index_entry(publisher, fmri, &metadata_path, &manifest_path) + { warn!("Failed to update index entry: {}", e); } } - + Ok(Some(metadata)) } - + /// Get the manifest content for an obsoleted package /// /// This method retrieves the original manifest content for an obsoleted package. @@ -1557,10 +1693,10 @@ impl ObsoletedPackageManager { // Fall back to the filesystem check if the index is not available return self.get_obsoleted_package_manifest_from_filesystem(publisher, fmri); } - + // Check the index let key = ObsoletedPackageKey::new(publisher, fmri); - + // Try to get a read lock on the index let index_read_result = self.index.read(); if let Err(e) = index_read_result { @@ -1568,25 +1704,28 @@ impl ObsoletedPackageManager { // Fall back to the filesystem check if the index is not available return self.get_obsoleted_package_manifest_from_filesystem(publisher, fmri); } - + let index = index_read_result.unwrap(); - + // Check if the package is in the index match index.get_entry(&key) { Ok(Some((metadata, manifest))) => { // If the content hash is NULL_HASH, generate a minimal manifest if metadata.content_hash == NULL_HASH { - debug!("Generating minimal manifest for obsoleted package with null hash: {}", fmri); + debug!( + "Generating minimal manifest for obsoleted package with null hash: {}", + fmri + ); return Ok(Some(self.generate_minimal_obsoleted_manifest(fmri))); } - + // Return the manifest content directly from the index Ok(Some(manifest)) - }, + } Ok(None) => { // Package not found in the index, fall back to the filesystem check self.get_obsoleted_package_manifest_from_filesystem(publisher, fmri) - }, + } Err(e) => { warn!("Failed to get entry from index: {}", e); // Fall back to the filesystem to check if there's an error @@ -1594,11 +1733,12 @@ impl ObsoletedPackageManager { } } } - + /// Generate a minimal manifest for an obsoleted package fn generate_minimal_obsoleted_manifest(&self, fmri: &Fmri) -> String { // Create a minimal JSON manifest with obsoletion attributes - format!(r#"{{ + format!( + r#"{{ "attributes": [ {{ "key": "pkg.fmri", @@ -1613,9 +1753,11 @@ impl ObsoletedPackageManager { ] }} ] -}}"#, fmri) +}}"#, + fmri + ) } - + /// Get the manifest content for an obsoleted package from the filesystem fn get_obsoleted_package_manifest_from_filesystem( &self, @@ -1625,56 +1767,75 @@ impl ObsoletedPackageManager { let stem = fmri.stem(); let version = fmri.version(); let encoded_version = url_encode(&version); - let metadata_path = self.base_path.join(publisher).join(stem).join(format!("{}.json", encoded_version)); - let manifest_path = self.base_path.join(publisher).join(stem).join(format!("{}.manifest", encoded_version)); + let metadata_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.json", encoded_version)); + let manifest_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.manifest", encoded_version)); // If the manifest file doesn't exist, check if the metadata exists and has a null hash if !manifest_path.exists() { debug!("Manifest file not found: {}", manifest_path.display()); - + // Check if the metadata file exists if metadata_path.exists() { // Read the metadata file - let metadata_json = fs::read_to_string(&metadata_path) - .map_err(|e| ObsoletedPackageError::MetadataReadError(format!( - "Failed to read metadata file {}: {}", - metadata_path.display(), e - )))?; - + let metadata_json = fs::read_to_string(&metadata_path).map_err(|e| { + ObsoletedPackageError::MetadataReadError(format!( + "Failed to read metadata file {}: {}", + metadata_path.display(), + e + )) + })?; + // Parse the metadata let metadata: ObsoletedPackageMetadata = serde_json::from_str(&metadata_json) - .map_err(|e| ObsoletedPackageError::MetadataParseError(format!( - "Failed to parse metadata from {}: {}", - metadata_path.display(), e - )))?; - + .map_err(|e| { + ObsoletedPackageError::MetadataParseError(format!( + "Failed to parse metadata from {}: {}", + metadata_path.display(), + e + )) + })?; + // If the content hash is NULL_HASH, generate a minimal manifest if metadata.content_hash == NULL_HASH { - debug!("Generating minimal manifest for obsoleted package with null hash: {}", fmri); + debug!( + "Generating minimal manifest for obsoleted package with null hash: {}", + fmri + ); return Ok(Some(self.generate_minimal_obsoleted_manifest(fmri))); } } - + return Ok(None); } // Read the manifest file - let manifest_content = fs::read_to_string(&manifest_path) - .map_err(|e| ObsoletedPackageError::ManifestReadError(format!( - "Failed to read manifest file {}: {}", - manifest_path.display(), e - )))?; - + let manifest_content = fs::read_to_string(&manifest_path).map_err(|e| { + ObsoletedPackageError::ManifestReadError(format!( + "Failed to read manifest file {}: {}", + manifest_path.display(), + e + )) + })?; + // Update the index with this package if metadata_path.exists() && manifest_path.exists() { - if let Err(e) = self.update_index_entry(publisher, fmri, &metadata_path, &manifest_path) { + if let Err(e) = self.update_index_entry(publisher, fmri, &metadata_path, &manifest_path) + { warn!("Failed to update index entry: {}", e); } } - + Ok(Some(manifest_content)) } - + /// Get manifest content and remove an obsoleted package /// /// This method retrieves the manifest content of an obsoleted package and removes it @@ -1689,13 +1850,12 @@ impl ObsoletedPackageManager { /// # Returns /// /// The manifest content if the package was found, or an error if the operation failed - pub fn get_and_remove_obsoleted_package( - &self, - publisher: &str, - fmri: &Fmri, - ) -> Result { - debug!("Getting and removing obsoleted package: {} (publisher: {})", fmri, publisher); - + pub fn get_and_remove_obsoleted_package(&self, publisher: &str, fmri: &Fmri) -> Result { + debug!( + "Getting and removing obsoleted package: {} (publisher: {})", + fmri, publisher + ); + // Get the manifest content let manifest_content = match self.get_obsoleted_package_manifest(publisher, fmri)? { Some(content) => content, @@ -1703,17 +1863,18 @@ impl ObsoletedPackageManager { return Err(ObsoletedPackageError::NotFound(format!( "Obsoleted package not found: {}", fmri - )).into()); + )) + .into()); } }; - + // Remove the obsoleted package from the obsoleted packages directory self.remove_obsoleted_package(publisher, fmri)?; - + info!("Retrieved and removed obsoleted package: {}", fmri); Ok(manifest_content) } - + /// Remove an obsoleted package /// /// This method removes an obsoleted package from the obsoleted packages' directory. @@ -1727,70 +1888,91 @@ impl ObsoletedPackageManager { /// # Returns /// /// `true` if the package was removed, `false` if it was not found - pub fn remove_obsoleted_package( - &self, - publisher: &str, - fmri: &Fmri, - ) -> Result { + pub fn remove_obsoleted_package(&self, publisher: &str, fmri: &Fmri) -> Result { let stem = fmri.stem(); let version = fmri.version(); let encoded_version = url_encode(&version); - let metadata_path = self.base_path.join(publisher).join(stem).join(format!("{}.json", encoded_version)); - let manifest_path = self.base_path.join(publisher).join(stem).join(format!("{}.manifest", encoded_version)); - - debug!("Removing obsoleted package: {} (publisher: {})", fmri, publisher); + let metadata_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.json", encoded_version)); + let manifest_path = self + .base_path + .join(publisher) + .join(stem) + .join(format!("{}.manifest", encoded_version)); + + debug!( + "Removing obsoleted package: {} (publisher: {})", + fmri, publisher + ); debug!("Metadata path: {}", metadata_path.display()); debug!("Manifest path: {}", manifest_path.display()); - + if !metadata_path.exists() && !manifest_path.exists() { // Package not found debug!("Obsoleted package not found: {}", fmri); return Ok(false); } - + // Remove the metadata file if it exists if metadata_path.exists() { debug!("Removing metadata file: {}", metadata_path.display()); - fs::remove_file(&metadata_path) - .map_err(|e| ObsoletedPackageError::RemoveError(format!( - "Failed to remove metadata file {}: {}", - metadata_path.display(), e - )))?; + fs::remove_file(&metadata_path).map_err(|e| { + ObsoletedPackageError::RemoveError(format!( + "Failed to remove metadata file {}: {}", + metadata_path.display(), + e + )) + })?; } - + // Remove the manifest file if it exists if manifest_path.exists() { debug!("Removing manifest file: {}", manifest_path.display()); - fs::remove_file(&manifest_path) - .map_err(|e| ObsoletedPackageError::RemoveError(format!( - "Failed to remove manifest file {}: {}", - manifest_path.display(), e - )))?; + fs::remove_file(&manifest_path).map_err(|e| { + ObsoletedPackageError::RemoveError(format!( + "Failed to remove manifest file {}: {}", + manifest_path.display(), + e + )) + })?; } - + // Check if the package directory is empty and remove it if it is let pkg_dir = self.base_path.join(publisher).join(stem); if pkg_dir.exists() { - debug!("Checking if package directory is empty: {}", pkg_dir.display()); + debug!( + "Checking if package directory is empty: {}", + pkg_dir.display() + ); let is_empty = fs::read_dir(&pkg_dir) - .map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to read directory {}: {}", - pkg_dir.display(), e - )))?.next().is_none(); - + .map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to read directory {}: {}", + pkg_dir.display(), + e + )) + })? + .next() + .is_none(); + if is_empty { debug!("Removing empty package directory: {}", pkg_dir.display()); - fs::remove_dir(&pkg_dir) - .map_err(|e| ObsoletedPackageError::RemoveError(format!( - "Failed to remove directory {}: {}", - pkg_dir.display(), e - )))?; + fs::remove_dir(&pkg_dir).map_err(|e| { + ObsoletedPackageError::RemoveError(format!( + "Failed to remove directory {}: {}", + pkg_dir.display(), + e + )) + })?; } } - + // Remove the package from the index let key = ObsoletedPackageKey::new(publisher, fmri); - + // Try to get a write lock on the index match self.index.write() { Ok(index) => { @@ -1798,7 +1980,7 @@ impl ObsoletedPackageManager { match index.remove_entry(&key) { Ok(true) => { debug!("Removed package from index: {}", fmri); - }, + } Ok(false) => { debug!("Package not found in index: {}", fmri); // If the package is not in the index, we need to rebuild the index @@ -1806,7 +1988,7 @@ impl ObsoletedPackageManager { if let Err(e) = self.build_index() { warn!("Failed to rebuild index after package not found: {}", e); } - }, + } Err(e) => { warn!("Failed to remove package from index: {}: {}", fmri, e); // If there's an error removing the entry, rebuild the index @@ -1815,9 +1997,12 @@ impl ObsoletedPackageManager { } } } - }, + } Err(e) => { - warn!("Failed to acquire write lock on index, package not removed from index: {}: {}", fmri, e); + warn!( + "Failed to acquire write lock on index, package not removed from index: {}: {}", + fmri, e + ); // If we can't get a write lock, mark the index as dirty so it will be rebuilt next time if let Ok(index) = self.index.write() { // This is a new writing attempt, so it might succeed even if the previous one failed @@ -1827,7 +2012,7 @@ impl ObsoletedPackageManager { } } } - + info!("Removed obsoleted package: {}", fmri); Ok(true) } @@ -1848,7 +2033,7 @@ impl ObsoletedPackageManager { } } } - + /// List all obsoleted packages for a publisher using the index /// /// This is a helper method that attempts to list packages using the redb index. @@ -1859,18 +2044,20 @@ impl ObsoletedPackageManager { warn!("Failed to ensure index is fresh: {}", e); return Err(e); } - + // Try to get a read lock on the index let index_read_result = self.index.read(); if let Err(e) = index_read_result { warn!("Failed to acquire read lock on index: {}", e); return Err(ObsoletedPackageError::IndexError(format!( - "Failed to acquire read lock on index: {}", e - )).into()); + "Failed to acquire read lock on index: {}", + e + )) + .into()); } - + let index = index_read_result.unwrap(); - + // Use get_entries_by_publisher to get all entries for the specified publisher let entries = match index.get_entries_by_publisher(publisher) { Ok(entries) => entries, @@ -1879,7 +2066,7 @@ impl ObsoletedPackageManager { return Err(e); } }; - + // Convert entries to FMRIs let mut packages = Vec::new(); for (key, _, _) in entries { @@ -1894,10 +2081,10 @@ impl ObsoletedPackageManager { } } } - + Ok(packages) } - + /// List all obsoleted packages for a publisher from the filesystem /// /// This method is used as a fallback when the index is not available. @@ -1919,7 +2106,9 @@ impl ObsoletedPackageManager { if path.is_file() && path.extension().map_or(false, |ext| ext == "json") { // Read the metadata file if let Ok(metadata_json) = fs::read_to_string(path) { - if let Ok(metadata) = serde_json::from_str::(&metadata_json) { + if let Ok(metadata) = + serde_json::from_str::(&metadata_json) + { // Parse the FMRI if let Ok(fmri) = Fmri::parse(&metadata.fmri) { obsoleted_packages.push(fmri); @@ -1931,7 +2120,6 @@ impl ObsoletedPackageManager { Ok(obsoleted_packages) } - /// List obsoleted packages for a publisher with pagination /// @@ -1976,7 +2164,9 @@ impl ObsoletedPackageManager { if path.is_file() && path.extension().map_or(false, |ext| ext == "json") { // Read the metadata file if let Ok(metadata_json) = fs::read_to_string(path) { - if let Ok(metadata) = serde_json::from_str::(&metadata_json) { + if let Ok(metadata) = + serde_json::from_str::(&metadata_json) + { // Parse the FMRI if let Ok(fmri) = Fmri::parse(&metadata.fmri) { all_packages.push(fmri); @@ -1985,7 +2175,7 @@ impl ObsoletedPackageManager { } } } - + // Sort packages by name and version for consistent pagination all_packages.sort_by(|a, b| { let name_cmp = a.stem().cmp(b.stem()); @@ -2005,7 +2195,7 @@ impl ObsoletedPackageManager { } else { (total_count + page_size - 1) / page_size }; - + // If no pagination is requested or there's only one page, return all packages if page_size == 0 || total_pages <= 1 { return Ok(PaginatedObsoletedPackages { @@ -2016,11 +2206,11 @@ impl ObsoletedPackageManager { total_pages, }); } - + // Calculate start and end indices for the requested page let start_idx = (page - 1) * page_size; let end_idx = start_idx + page_size; - + // Get packages for the requested page let packages = if start_idx >= total_count { // If the start index is beyond the total count, return an empty page @@ -2028,7 +2218,7 @@ impl ObsoletedPackageManager { } else { all_packages[start_idx..end_idx.min(total_count)].to_vec() }; - + Ok(PaginatedObsoletedPackages { packages, total_count, @@ -2037,7 +2227,7 @@ impl ObsoletedPackageManager { total_pages, }) } - + /// Search for obsoleted packages matching a pattern /// /// This method searches for obsoleted packages that match the given pattern. @@ -2058,7 +2248,7 @@ impl ObsoletedPackageManager { // Fall back to the filesystem-based search return self.search_obsoleted_packages_fallback(publisher, pattern); } - + // Try to get a read lock on the index let index_read_result = self.index.read(); if let Err(e) = index_read_result { @@ -2066,9 +2256,9 @@ impl ObsoletedPackageManager { // Fall back to the filesystem-based search return self.search_obsoleted_packages_fallback(publisher, pattern); } - + let index = index_read_result.unwrap(); - + // Get all entries from the index let entries = match index.get_all_entries() { Ok(entries) => entries, @@ -2078,7 +2268,7 @@ impl ObsoletedPackageManager { return self.search_obsoleted_packages_fallback(publisher, pattern); } }; - + // Check if the pattern looks like a version number if pattern.chars().all(|c| c.is_digit(10) || c == '.') { // This looks like a version number, so match only against the version part @@ -2087,7 +2277,7 @@ impl ObsoletedPackageManager { if key.publisher == publisher && key.version.contains(pattern) { // Construct the FMRI string let fmri_str = format!("pkg://{}/{}@{}", key.publisher, key.stem, key.version); - + // Parse the FMRI if let Ok(fmri) = Fmri::parse(&fmri_str) { packages.push(fmri); @@ -2096,7 +2286,7 @@ impl ObsoletedPackageManager { } return Ok(packages); } - + // Try to compile the pattern as a regex let result = match Regex::new(pattern) { Ok(regex) => { @@ -2105,8 +2295,9 @@ impl ObsoletedPackageManager { for (key, _, _) in entries { if key.publisher == publisher { // Construct the FMRI string for regex matching - let fmri_str = format!("pkg://{}/{}@{}", key.publisher, key.stem, key.version); - + let fmri_str = + format!("pkg://{}/{}@{}", key.publisher, key.stem, key.version); + // Match against the FMRI string or the package name if regex.is_match(&fmri_str) || regex.is_match(&key.stem) { // Parse the FMRI @@ -2117,15 +2308,16 @@ impl ObsoletedPackageManager { } } packages - }, + } Err(_) => { // Fall back to simple substring matching let mut packages = Vec::new(); for (key, _, _) in entries { if key.publisher == publisher { // Construct the FMRI string - let fmri_str = format!("pkg://{}/{}@{}", key.publisher, key.stem, key.version); - + let fmri_str = + format!("pkg://{}/{}@{}", key.publisher, key.stem, key.version); + // Match against the FMRI string or the package name // For "package-" pattern, we want to match only packages that start with "package-" if pattern.ends_with("-") && key.stem.starts_with(pattern) { @@ -2133,7 +2325,7 @@ impl ObsoletedPackageManager { if let Ok(fmri) = Fmri::parse(&fmri_str) { packages.push(fmri); } - } + } // For version searches like "2.0", match only the version part else if pattern.chars().all(|c| c.is_digit(10) || c == '.') { // This looks like a version number, so match only against the version part @@ -2143,8 +2335,7 @@ impl ObsoletedPackageManager { packages.push(fmri); } } - } - else if fmri_str.contains(pattern) || key.stem.contains(pattern) { + } else if fmri_str.contains(pattern) || key.stem.contains(pattern) { // Parse the FMRI if let Ok(fmri) = Fmri::parse(&fmri_str) { packages.push(fmri); @@ -2155,15 +2346,19 @@ impl ObsoletedPackageManager { packages } }; - + Ok(result) } - + /// Fallback implementation of search_obsoleted_packages that uses the filesystem - fn search_obsoleted_packages_fallback(&self, publisher: &str, pattern: &str) -> Result> { + fn search_obsoleted_packages_fallback( + &self, + publisher: &str, + pattern: &str, + ) -> Result> { // Get all obsoleted packages for the publisher let all_packages = self.list_obsoleted_packages(publisher)?; - + // Check if the pattern looks like a version number if pattern.chars().all(|c| c.is_digit(10) || c == '.') { // This looks like a version number, so match only against the version part @@ -2172,7 +2367,7 @@ impl ObsoletedPackageManager { .filter(|fmri| fmri.version().contains(pattern)) .collect()); } - + // Try to compile the pattern as a regex let result = match Regex::new(pattern) { Ok(regex) => { @@ -2186,7 +2381,7 @@ impl ObsoletedPackageManager { regex.is_match(fmri.stem()) }) .collect() - }, + } Err(_) => { // If regex compilation fails, fall back to simple substring matching all_packages @@ -2196,13 +2391,12 @@ impl ObsoletedPackageManager { // For "package-" pattern, we want to match only packages that start with "package-" if pattern.ends_with("-") && fmri.stem().starts_with(pattern) { true - } + } // For version searches like "2.0", match only the version part else if pattern.chars().all(|c| c.is_digit(10) || c == '.') { // This looks like a version number, so match only against the version part fmri.version().contains(pattern) - } - else { + } else { // Match against the FMRI string fmri.to_string().contains(pattern) || // Match against the package name @@ -2212,10 +2406,10 @@ impl ObsoletedPackageManager { .collect() } }; - + Ok(result) } - + /// Export obsoleted packages to a file /// /// This method exports obsoleted packages to a JSON file that can be imported into another repository. @@ -2236,28 +2430,28 @@ impl ObsoletedPackageManager { output_file: &Path, ) -> Result { info!("Exporting obsoleted packages for publisher: {}", publisher); - + // Get the packages to export let packages = if let Some(pattern) = pattern { self.search_obsoleted_packages(publisher, pattern)? } else { self.list_obsoleted_packages(publisher)? }; - + if packages.is_empty() { info!("No packages found to export"); return Ok(0); } - + info!("Found {} packages to export", packages.len()); - + // Create the export structure let mut export = ObsoletedPackagesExport { version: 1, export_date: format_timestamp(&SystemTime::now()), packages: Vec::new(), }; - + // Add each package to the export for fmri in packages { // Get the metadata @@ -2268,7 +2462,7 @@ impl ObsoletedPackageManager { continue; } }; - + // Get the manifest content let manifest = match self.get_obsoleted_package_manifest(publisher, &fmri)? { Some(manifest) => manifest, @@ -2277,7 +2471,7 @@ impl ObsoletedPackageManager { continue; } }; - + // Add the package to the export export.packages.push(ObsoletedPackageExport { publisher: publisher.to_string(), @@ -2286,23 +2480,33 @@ impl ObsoletedPackageManager { manifest, }); } - + // Write the export to the output file - let file = fs::File::create(output_file).map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to create output file {}: {}", - output_file.display(), e - )))?; - + let file = fs::File::create(output_file).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to create output file {}: {}", + output_file.display(), + e + )) + })?; + let writer = std::io::BufWriter::new(file); - serde_json::to_writer_pretty(writer, &export).map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to write export to file {}: {}", - output_file.display(), e - )))?; - - info!("Exported {} packages to {}", export.packages.len(), output_file.display()); + serde_json::to_writer_pretty(writer, &export).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to write export to file {}: {}", + output_file.display(), + e + )) + })?; + + info!( + "Exported {} packages to {}", + export.packages.len(), + output_file.display() + ); Ok(export.packages.len()) } - + /// Import obsoleted packages from a file /// /// This method imports obsoleted packages from a JSON file created by `export_obsoleted_packages`. @@ -2321,27 +2525,33 @@ impl ObsoletedPackageManager { override_publisher: Option<&str>, ) -> Result { info!("Importing obsoleted packages from {}", input_file.display()); - + // Read the export file - let file = fs::File::open(input_file).map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to open input file {}: {}", - input_file.display(), e - )))?; - + let file = fs::File::open(input_file).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to open input file {}: {}", + input_file.display(), + e + )) + })?; + let reader = std::io::BufReader::new(file); - let export: ObsoletedPackagesExport = serde_json::from_reader(reader).map_err(|e| ObsoletedPackageError::IoError(format!( - "Failed to parse export from file {}: {}", - input_file.display(), e - )))?; - + let export: ObsoletedPackagesExport = serde_json::from_reader(reader).map_err(|e| { + ObsoletedPackageError::IoError(format!( + "Failed to parse export from file {}: {}", + input_file.display(), + e + )) + })?; + info!("Found {} packages to import", export.packages.len()); - + // Import each package let mut imported_count = 0; for package in export.packages { // Determine the publisher to use let publisher = override_publisher.unwrap_or(&package.publisher); - + // Parse the FMRI let fmri = match Fmri::parse(&package.fmri) { Ok(fmri) => fmri, @@ -2350,7 +2560,7 @@ impl ObsoletedPackageManager { continue; } }; - + // Store the obsoleted package match self.store_obsoleted_package( publisher, @@ -2362,17 +2572,17 @@ impl ObsoletedPackageManager { Ok(_) => { info!("Imported obsoleted package: {}", fmri); imported_count += 1; - }, + } Err(e) => { warn!("Failed to import obsoleted package {}: {}", fmri, e); } } } - + info!("Imported {} packages", imported_count); Ok(imported_count) } - + /// Find obsoleted packages that are older than a specified TTL (time-to-live) /// /// This method finds obsoleted packages for a publisher that were obsoleted @@ -2393,38 +2603,42 @@ impl ObsoletedPackageManager { ) -> Result> { // Get all obsoleted packages for the publisher let all_packages = self.list_obsoleted_packages(publisher)?; - + // Calculate the cutoff time (current time minus TTL) let now = Utc::now(); let ttl_duration = ChronoDuration::days(ttl_days as i64); let cutoff_time = now - ttl_duration; - + let mut older_packages = Vec::new(); - + // Check each package's obsolescence_date for fmri in all_packages { // Get the metadata for the package if let Ok(Some(metadata)) = self.get_obsoleted_package_metadata(publisher, &fmri) { // Parse the obsolescence_date - if let Ok(obsolescence_date) = DateTime::parse_from_rfc3339(&metadata.obsolescence_date) { + if let Ok(obsolescence_date) = + DateTime::parse_from_rfc3339(&metadata.obsolescence_date) + { // Convert to UTC for comparison let obsolescence_date_utc = obsolescence_date.with_timezone(&Utc); - + // Check if the package is older than the TTL if obsolescence_date_utc < cutoff_time { older_packages.push(fmri); } } else { // If we can't parse the date, log a warning and skip this package - warn!("Failed to parse obsolescence_date for package {}: {}", - fmri, metadata.obsolescence_date); + warn!( + "Failed to parse obsolescence_date for package {}: {}", + fmri, metadata.obsolescence_date + ); } } } - + Ok(older_packages) } - + /// Clean up obsoleted packages that are older than a specified TTL (time-to-live) /// /// This method finds and removes obsoleted packages for a publisher that were @@ -2447,16 +2661,22 @@ impl ObsoletedPackageManager { ) -> Result { // Find packages older than the TTL let older_packages = self.find_obsoleted_packages_older_than_ttl(publisher, ttl_days)?; - + if older_packages.is_empty() { - info!("No obsoleted packages older than {} days found for publisher {}", - ttl_days, publisher); + info!( + "No obsoleted packages older than {} days found for publisher {}", + ttl_days, publisher + ); return Ok(0); } - - info!("Found {} obsoleted packages older than {} days for publisher {}", - older_packages.len(), ttl_days, publisher); - + + info!( + "Found {} obsoleted packages older than {} days for publisher {}", + older_packages.len(), + ttl_days, + publisher + ); + if dry_run { // In dry run mode, just report what would be removed for fmri in &older_packages { @@ -2464,18 +2684,21 @@ impl ObsoletedPackageManager { } return Ok(older_packages.len()); } - + // Process packages in batches let results = self.batch_process(publisher, &older_packages, None, |pub_name, fmri| { info!("Removing obsoleted package: {}", fmri); self.remove_obsoleted_package(pub_name, fmri) })?; - + // Count successful removals - let removed_count = results.iter().filter(|r| r.as_ref().map_or(false, |&b| b)).count(); - + let removed_count = results + .iter() + .filter(|r| r.as_ref().map_or(false, |&b| b)) + .count(); + info!("Successfully removed {} obsoleted packages", removed_count); - + Ok(removed_count) } @@ -2494,11 +2717,12 @@ impl ObsoletedPackageManager { /// # Returns /// /// A list of results, one for each input FMRI - pub fn batch_process(&self, - publisher: &str, - fmris: &[Fmri], + pub fn batch_process( + &self, + publisher: &str, + fmris: &[Fmri], batch_size: Option, - processor: F + processor: F, ) -> Result>> where F: Fn(&str, &Fmri) -> std::result::Result, @@ -2506,7 +2730,7 @@ impl ObsoletedPackageManager { { let batch_size = batch_size.unwrap_or(100); let mut results = Vec::with_capacity(fmris.len()); - + // Process packages in batches for chunk in fmris.chunks(batch_size) { for fmri in chunk { @@ -2514,7 +2738,7 @@ impl ObsoletedPackageManager { results.push(result); } } - + Ok(results) } } @@ -2567,7 +2791,8 @@ mod tests { }"#; let obsoleted_by = Some(vec!["pkg://test/new-package@2.0".to_string()]); - let deprecation_message = Some("This package is deprecated. Use new-package instead.".to_string()); + let deprecation_message = + Some("This package is deprecated. Use new-package instead.".to_string()); let metadata_path = manager .store_obsoleted_package( @@ -2586,7 +2811,10 @@ mod tests { assert!(manager.is_obsoleted("test", &fmri)); // Get the metadata - let metadata = manager.get_obsoleted_package_metadata("test", &fmri).unwrap().unwrap(); + let metadata = manager + .get_obsoleted_package_metadata("test", &fmri) + .unwrap() + .unwrap(); assert_eq!(metadata.fmri, fmri.to_string()); assert_eq!(metadata.status, "obsolete"); assert_eq!(metadata.obsoleted_by, obsoleted_by); @@ -2597,19 +2825,19 @@ mod tests { assert_eq!(obsoleted_packages.len(), 1); assert_eq!(obsoleted_packages[0].to_string(), fmri.to_string()); } - + #[test] fn test_obsoleted_package_manager_search() { // Create a temporary directory for testing let temp_dir = tempdir().unwrap(); let manager = ObsoletedPackageManager::new(temp_dir.path()); manager.init().unwrap(); - + // Create multiple test FMRIs let fmri1 = Fmri::parse("pkg://test/package-one@1.0,5.11-0.1:20250101T000000Z").unwrap(); let fmri2 = Fmri::parse("pkg://test/package-two@2.0,5.11-0.1:20250101T000000Z").unwrap(); let fmri3 = Fmri::parse("pkg://test/other-package@3.0,5.11-0.1:20250101T000000Z").unwrap(); - + // Store obsoleted packages let manifest_template = r#"{ "attributes": [ @@ -2623,42 +2851,54 @@ mod tests { } ] }"#; - + let manifest1 = manifest_template.replace("%s", &fmri1.to_string()); let manifest2 = manifest_template.replace("%s", &fmri2.to_string()); let manifest3 = manifest_template.replace("%s", &fmri3.to_string()); - - manager.store_obsoleted_package("test", &fmri1, &manifest1, None, None).unwrap(); - manager.store_obsoleted_package("test", &fmri2, &manifest2, None, None).unwrap(); - manager.store_obsoleted_package("test", &fmri3, &manifest3, None, None).unwrap(); - + + manager + .store_obsoleted_package("test", &fmri1, &manifest1, None, None) + .unwrap(); + manager + .store_obsoleted_package("test", &fmri2, &manifest2, None, None) + .unwrap(); + manager + .store_obsoleted_package("test", &fmri3, &manifest3, None, None) + .unwrap(); + // Test search with substring - let results = manager.search_obsoleted_packages("test", "package-").unwrap(); + let results = manager + .search_obsoleted_packages("test", "package-") + .unwrap(); assert_eq!(results.len(), 2); assert!(results.iter().any(|f| f.to_string() == fmri1.to_string())); assert!(results.iter().any(|f| f.to_string() == fmri2.to_string())); - + // Test search with regex - let results = manager.search_obsoleted_packages("test", "package-.*").unwrap(); + let results = manager + .search_obsoleted_packages("test", "package-.*") + .unwrap(); assert_eq!(results.len(), 2); - + // Test search for a specific version let results = manager.search_obsoleted_packages("test", "2.0").unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0].to_string(), fmri2.to_string()); - + // Test search with no matches - let results = manager.search_obsoleted_packages("test", "nonexistent").unwrap(); + let results = manager + .search_obsoleted_packages("test", "nonexistent") + .unwrap(); assert_eq!(results.len(), 0); } - + #[test] fn test_obsoleted_package_manager_pagination() { // Create a temporary directory for testing let temp_dir = tempdir().unwrap(); let manager = ObsoletedPackageManager::new(temp_dir.path()); manager.init().unwrap(); - + // Create 10 test FMRIs let mut fmris = Vec::new(); let manifest_template = r#"{ @@ -2673,50 +2913,66 @@ mod tests { } ] }"#; - + for i in 1..=10 { - let fmri = Fmri::parse(&format!("pkg://test/package-{:02}@1.0,5.11-0.1:20250101T000000Z", i)).unwrap(); + let fmri = Fmri::parse(&format!( + "pkg://test/package-{:02}@1.0,5.11-0.1:20250101T000000Z", + i + )) + .unwrap(); let manifest = manifest_template.replace("%s", &fmri.to_string()); - manager.store_obsoleted_package("test", &fmri, &manifest, None, None).unwrap(); + manager + .store_obsoleted_package("test", &fmri, &manifest, None, None) + .unwrap(); fmris.push(fmri); } - + // Test pagination with page size 3 - let page1 = manager.list_obsoleted_packages_paginated("test", Some(1), Some(3)).unwrap(); + let page1 = manager + .list_obsoleted_packages_paginated("test", Some(1), Some(3)) + .unwrap(); assert_eq!(page1.packages.len(), 3); assert_eq!(page1.total_count, 10); assert_eq!(page1.page, 1); assert_eq!(page1.page_size, 3); assert_eq!(page1.total_pages, 4); - - let page2 = manager.list_obsoleted_packages_paginated("test", Some(2), Some(3)).unwrap(); + + let page2 = manager + .list_obsoleted_packages_paginated("test", Some(2), Some(3)) + .unwrap(); assert_eq!(page2.packages.len(), 3); assert_eq!(page2.page, 2); - - let page4 = manager.list_obsoleted_packages_paginated("test", Some(4), Some(3)).unwrap(); + + let page4 = manager + .list_obsoleted_packages_paginated("test", Some(4), Some(3)) + .unwrap(); assert_eq!(page4.packages.len(), 1); // The last page has only 1 item - + // Test pagination with page beyond total - let empty_page = manager.list_obsoleted_packages_paginated("test", Some(5), Some(3)).unwrap(); + let empty_page = manager + .list_obsoleted_packages_paginated("test", Some(5), Some(3)) + .unwrap(); assert_eq!(empty_page.packages.len(), 0); assert_eq!(empty_page.total_count, 10); assert_eq!(empty_page.page, 5); - + // Test with no pagination - let all_packages = manager.list_obsoleted_packages_paginated("test", None, None).unwrap(); + let all_packages = manager + .list_obsoleted_packages_paginated("test", None, None) + .unwrap(); assert_eq!(all_packages.packages.len(), 10); } - + #[test] fn test_obsoleted_package_manager_remove() { // Create a temporary directory for testing let temp_dir = tempdir().unwrap(); let manager = ObsoletedPackageManager::new(temp_dir.path()); manager.init().unwrap(); - + // Create a test FMRI let fmri = Fmri::parse("pkg://test/package@1.0,5.11-0.1:20250101T000000Z").unwrap(); - + // Store an obsoleted package let manifest_content = r#"{ "attributes": [ @@ -2730,36 +2986,38 @@ mod tests { } ] }"#; - - manager.store_obsoleted_package("test", &fmri, manifest_content, None, None).unwrap(); - + + manager + .store_obsoleted_package("test", &fmri, manifest_content, None, None) + .unwrap(); + // Verify the package exists assert!(manager.is_obsoleted("test", &fmri)); - + // Remove the package let removed = manager.remove_obsoleted_package("test", &fmri).unwrap(); assert!(removed); - + // Verify the package no longer exists assert!(!manager.is_obsoleted("test", &fmri)); - + // Try to remove a non-existent package let not_removed = manager.remove_obsoleted_package("test", &fmri).unwrap(); assert!(!not_removed); } - + #[test] fn test_obsoleted_package_manager_batch_processing() { // Create a temporary directory for testing let temp_dir = tempdir().unwrap(); let manager = ObsoletedPackageManager::new(temp_dir.path()); manager.init().unwrap(); - + // Create multiple test FMRIs let fmri1 = Fmri::parse("pkg://test/package-one@1.0,5.11-0.1:20250101T000000Z").unwrap(); let fmri2 = Fmri::parse("pkg://test/package-two@2.0,5.11-0.1:20250101T000000Z").unwrap(); let fmri3 = Fmri::parse("pkg://test/package-three@3.0,5.11-0.1:20250101T000000Z").unwrap(); - + // Store obsoleted packages let manifest_template = r#"{ "attributes": [ @@ -2773,41 +3031,49 @@ mod tests { } ] }"#; - + let manifest1 = manifest_template.replace("%s", &fmri1.to_string()); let manifest2 = manifest_template.replace("%s", &fmri2.to_string()); let manifest3 = manifest_template.replace("%s", &fmri3.to_string()); - - manager.store_obsoleted_package("test", &fmri1, &manifest1, None, None).unwrap(); - manager.store_obsoleted_package("test", &fmri2, &manifest2, None, None).unwrap(); - manager.store_obsoleted_package("test", &fmri3, &manifest3, None, None).unwrap(); - + + manager + .store_obsoleted_package("test", &fmri1, &manifest1, None, None) + .unwrap(); + manager + .store_obsoleted_package("test", &fmri2, &manifest2, None, None) + .unwrap(); + manager + .store_obsoleted_package("test", &fmri3, &manifest3, None, None) + .unwrap(); + // Test batch processing with is_obsoleted let fmris = vec![fmri1.clone(), fmri2.clone(), fmri3.clone()]; - let results: Vec> = - manager.batch_process("test", &fmris, Some(2), |pub_name, fmri| { + let results: Vec> = manager + .batch_process("test", &fmris, Some(2), |pub_name, fmri| { Ok(manager.is_obsoleted(pub_name, fmri)) - }).unwrap(); - + }) + .unwrap(); + assert_eq!(results.len(), 3); assert!(results[0].as_ref().unwrap()); assert!(results[1].as_ref().unwrap()); assert!(results[2].as_ref().unwrap()); - + // Test batch processing with remove - let results: Vec> = - manager.batch_process("test", &fmris, Some(2), |pub_name, fmri| { + let results: Vec> = manager + .batch_process("test", &fmris, Some(2), |pub_name, fmri| { manager.remove_obsoleted_package(pub_name, fmri) - }).unwrap(); - + }) + .unwrap(); + assert_eq!(results.len(), 3); assert!(results[0].as_ref().unwrap()); assert!(results[1].as_ref().unwrap()); assert!(results[2].as_ref().unwrap()); - + // Verify all packages are removed assert!(!manager.is_obsoleted("test", &fmri1)); assert!(!manager.is_obsoleted("test", &fmri2)); assert!(!manager.is_obsoleted("test", &fmri3)); } -} \ No newline at end of file +} diff --git a/libips/src/repository/progress.rs b/libips/src/repository/progress.rs index f9d4527..579cf3d 100644 --- a/libips/src/repository/progress.rs +++ b/libips/src/repository/progress.rs @@ -63,13 +63,13 @@ pub trait ProgressReporter { pub struct ProgressInfo { /// The name of the operation being performed pub operation: String, - + /// The current progress value (e.g., bytes downloaded, files processed) pub current: Option, - + /// The total expected value (e.g., total bytes, total files) pub total: Option, - + /// Additional context about the operation (e.g., current file name) pub context: Option, } @@ -139,18 +139,18 @@ impl ProgressInfo { impl fmt::Display for ProgressInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.operation)?; - + if let (Some(current), Some(total)) = (self.current, self.total) { let percentage = (current as f64 / total as f64) * 100.0; write!(f, " {:.1}% ({}/{})", percentage, current, total)?; } else if let Some(current) = self.current { write!(f, " {}", current)?; } - + if let Some(context) = &self.context { write!(f, " - {}", context)?; } - + Ok(()) } } @@ -165,4 +165,4 @@ impl ProgressReporter for NoopProgressReporter { fn start(&self, _info: &ProgressInfo) {} fn update(&self, _info: &ProgressInfo) {} fn finish(&self, _info: &ProgressInfo) {} -} \ No newline at end of file +} diff --git a/libips/src/repository/rest_backend.rs b/libips/src/repository/rest_backend.rs index 472b422..30f65d9 100644 --- a/libips/src/repository/rest_backend.rs +++ b/libips/src/repository/rest_backend.rs @@ -13,12 +13,12 @@ use tracing::{debug, info, warn}; use reqwest::blocking::Client; use serde_json::Value; +use super::catalog::CatalogManager; use super::{ NoopProgressReporter, PackageContents, PackageInfo, ProgressInfo, ProgressReporter, PublisherInfo, ReadableRepository, RepositoryConfig, RepositoryError, RepositoryInfo, RepositoryVersion, Result, WritableRepository, }; -use super::catalog::CatalogManager; /// Repository implementation that uses a REST API to interact with a remote repository. /// @@ -125,27 +125,33 @@ impl WritableRepository for RestBackend { println!("Creating publisher directory..."); let publisher_dir = cache_path.join("publisher").join(publisher); println!("Publisher directory path: {}", publisher_dir.display()); - + match fs::create_dir_all(&publisher_dir) { Ok(_) => println!("Successfully created publisher directory"), Err(e) => println!("Failed to create publisher directory: {}", e), } - + // Check if the directory was created - println!("Publisher directory exists after creation: {}", publisher_dir.exists()); - + println!( + "Publisher directory exists after creation: {}", + publisher_dir.exists() + ); + // Create catalog directory let catalog_dir = publisher_dir.join("catalog"); println!("Catalog directory path: {}", catalog_dir.display()); - + match fs::create_dir_all(&catalog_dir) { Ok(_) => println!("Successfully created catalog directory"), Err(e) => println!("Failed to create catalog directory: {}", e), } - + // Check if the directory was created - println!("Catalog directory exists after creation: {}", catalog_dir.exists()); - + println!( + "Catalog directory exists after creation: {}", + catalog_dir.exists() + ); + debug!("Created publisher directory: {}", publisher_dir.display()); } else { println!("No local cache path set, skipping directory creation"); @@ -256,10 +262,12 @@ impl WritableRepository for RestBackend { client: Client::new(), catalog_managers: HashMap::new(), }; - + // Check if we have a local cache path if cloned_self.local_cache_path.is_none() { - return Err(RepositoryError::Other("No local cache path set".to_string())); + return Err(RepositoryError::Other( + "No local cache path set".to_string(), + )); } // Filter publishers if specified @@ -316,18 +324,18 @@ impl ReadableRepository for RestBackend { /// Open an existing repository fn open>(uri: P) -> Result { let uri_str = uri.as_ref().to_string_lossy().to_string(); - + // Create an HTTP client let client = Client::new(); - + // Fetch the repository configuration from the remote server // We'll try to get the publisher information using the publisher endpoint let url = format!("{}/publisher/0", uri_str); - + debug!("Fetching repository configuration from: {}", url); - + let mut config = RepositoryConfig::default(); - + // Try to fetch publisher information match client.get(&url).send() { Ok(response) => { @@ -336,31 +344,36 @@ impl ReadableRepository for RestBackend { match response.json::() { Ok(json) => { // Extract publisher information - if let Some(publishers) = json.get("publishers").and_then(|p| p.as_object()) { + if let Some(publishers) = + json.get("publishers").and_then(|p| p.as_object()) + { for (name, _) in publishers { debug!("Found publisher: {}", name); config.publishers.push(name.clone()); } } - }, + } Err(e) => { warn!("Failed to parse publisher information: {}", e); } } } else { - warn!("Failed to fetch publisher information: HTTP status {}", response.status()); + warn!( + "Failed to fetch publisher information: HTTP status {}", + response.status() + ); } - }, + } Err(e) => { warn!("Failed to connect to repository: {}", e); } } - + // If we couldn't get any publishers, add a default one if config.publishers.is_empty() { config.publishers.push("openindiana.org".to_string()); } - + // Create the repository instance Ok(RestBackend { uri: uri_str, @@ -536,12 +549,7 @@ impl ReadableRepository for RestBackend { Ok(package_contents) } - fn fetch_payload( - &mut self, - publisher: &str, - digest: &str, - dest: &Path, - ) -> Result<()> { + fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()> { // Determine hash and algorithm from the provided digest string let mut hash = digest.to_string(); let mut algo: Option = None; @@ -556,10 +564,17 @@ impl ReadableRepository for RestBackend { return Err(RepositoryError::Other("Empty digest provided".to_string())); } - let shard = if hash.len() >= 2 { &hash[0..2] } else { &hash[..] }; + let shard = if hash.len() >= 2 { + &hash[0..2] + } else { + &hash[..] + }; let candidates = vec![ format!("{}/file/{}/{}", self.uri, shard, hash), - format!("{}/publisher/{}/file/{}/{}", self.uri, publisher, shard, hash), + format!( + "{}/publisher/{}/file/{}/{}", + self.uri, publisher, shard, hash + ), ]; // Ensure destination directory exists @@ -571,11 +586,17 @@ impl ReadableRepository for RestBackend { for url in candidates { match self.client.get(&url).send() { Ok(resp) if resp.status().is_success() => { - let body = resp.bytes().map_err(|e| RepositoryError::Other(format!("Failed to read payload body: {}", e)))?; + let body = resp.bytes().map_err(|e| { + RepositoryError::Other(format!("Failed to read payload body: {}", e)) + })?; // Verify digest if algorithm is known if let Some(alg) = algo.clone() { - match crate::digest::Digest::from_bytes(&body, alg, crate::digest::DigestSource::PrimaryPayloadHash) { + match crate::digest::Digest::from_bytes( + &body, + alg, + crate::digest::DigestSource::PrimaryPayloadHash, + ) { Ok(comp) => { if comp.hash != hash { return Err(RepositoryError::DigestError(format!( @@ -605,7 +626,9 @@ impl ReadableRepository for RestBackend { } } - Err(RepositoryError::NotFound(last_err.unwrap_or_else(|| "payload not found".to_string()))) + Err(RepositoryError::NotFound( + last_err.unwrap_or_else(|| "payload not found".to_string()), + )) } fn fetch_manifest( @@ -636,14 +659,18 @@ impl RestBackend { // Require versioned FMRI let version = fmri.version(); if version.is_empty() { - return Err(RepositoryError::Other("FMRI must include a version to fetch manifest".into())); + return Err(RepositoryError::Other( + "FMRI must include a version to fetch manifest".into(), + )); } // URL-encode helper let url_encode = |s: &str| -> String { let mut out = String::new(); for b in s.bytes() { match b { - b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => out.push(b as char), + b'-' | b'_' | b'.' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => { + out.push(b as char) + } b' ' => out.push('+'), _ => { out.push('%'); @@ -658,16 +685,24 @@ impl RestBackend { let encoded_version = url_encode(&version); let candidates = vec![ format!("{}/manifest/0/{}", self.uri, encoded_fmri), - format!("{}/publisher/{}/manifest/0/{}", self.uri, publisher, encoded_fmri), + format!( + "{}/publisher/{}/manifest/0/{}", + self.uri, publisher, encoded_fmri + ), // Fallbacks to direct file-style paths if server exposes static files format!("{}/pkg/{}/{}", self.uri, encoded_stem, encoded_version), - format!("{}/publisher/{}/pkg/{}/{}", self.uri, publisher, encoded_stem, encoded_version), + format!( + "{}/publisher/{}/pkg/{}/{}", + self.uri, publisher, encoded_stem, encoded_version + ), ]; let mut last_err: Option = None; for url in candidates { match self.client.get(&url).send() { Ok(resp) if resp.status().is_success() => { - let text = resp.text().map_err(|e| RepositoryError::Other(format!("Failed to read manifest body: {}", e)))?; + let text = resp.text().map_err(|e| { + RepositoryError::Other(format!("Failed to read manifest body: {}", e)) + })?; return Ok(text); } Ok(resp) => { @@ -678,7 +713,9 @@ impl RestBackend { } } } - Err(RepositoryError::NotFound(last_err.unwrap_or_else(|| "manifest not found".to_string()))) + Err(RepositoryError::NotFound( + last_err.unwrap_or_else(|| "manifest not found".to_string()), + )) } /// Sets the local path where catalog files will be cached. /// @@ -698,15 +735,15 @@ impl RestBackend { /// Returns an error if the directory could not be created. pub fn set_local_cache_path>(&mut self, path: P) -> Result<()> { self.local_cache_path = Some(path.as_ref().to_path_buf()); - + // Create the directory if it doesn't exist if let Some(path) = &self.local_cache_path { fs::create_dir_all(path)?; } - + Ok(()) } - + /// Initializes the repository by downloading catalog files for all publishers. /// /// This method should be called after setting the local cache path with @@ -729,21 +766,27 @@ impl RestBackend { pub fn initialize(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> { // Check if we have a local cache path if self.local_cache_path.is_none() { - return Err(RepositoryError::Other("No local cache path set".to_string())); + return Err(RepositoryError::Other( + "No local cache path set".to_string(), + )); } - + // Download catalogs for all publishers self.download_all_catalogs(progress)?; - + Ok(()) } - + /// Get the catalog manager for a publisher fn get_catalog_manager(&mut self, publisher: &str) -> Result<&mut CatalogManager> { // Check if we have a local cache path let cache_path = match &self.local_cache_path { Some(path) => path, - None => return Err(RepositoryError::Other("No local cache path set".to_string())), + None => { + return Err(RepositoryError::Other( + "No local cache path set".to_string(), + )); + } }; // The local cache path is expected to already point to the per-publisher directory @@ -753,12 +796,13 @@ impl RestBackend { // Get or create the catalog manager pointing at the per-publisher directory directly if !self.catalog_managers.contains_key(publisher) { let catalog_manager = CatalogManager::new(cache_path, publisher)?; - self.catalog_managers.insert(publisher.to_string(), catalog_manager); + self.catalog_managers + .insert(publisher.to_string(), catalog_manager); } Ok(self.catalog_managers.get_mut(publisher).unwrap()) } - + /// Downloads a catalog file from the remote server. /// /// # Arguments @@ -789,12 +833,18 @@ impl RestBackend { // Prepare candidate URLs to support both modern and legacy pkg5 depotd layouts let mut urls: Vec = vec![ format!("{}/catalog/1/{}", self.uri, file_name), - format!("{}/publisher/{}/catalog/1/{}", self.uri, publisher, file_name), + format!( + "{}/publisher/{}/catalog/1/{}", + self.uri, publisher, file_name + ), ]; if file_name == "catalog.attrs" { // Some older depots expose catalog.attrs at the root or under publisher path urls.insert(1, format!("{}/catalog.attrs", self.uri)); - urls.push(format!("{}/publisher/{}/catalog.attrs", self.uri, publisher)); + urls.push(format!( + "{}/publisher/{}/catalog.attrs", + self.uri, publisher + )); } debug!( @@ -855,13 +905,10 @@ impl RestBackend { "Failed to download '{}' from any known endpoint: {}", file_name, s ), - None => format!( - "Failed to download '{}' from any known endpoint", - file_name - ), + None => format!("Failed to download '{}' from any known endpoint", file_name), })) } - + /// Download and store a catalog file /// /// # Arguments @@ -890,7 +937,11 @@ impl RestBackend { // Check if we have a local cache path let cache_path = match &self.local_cache_path { Some(path) => path, - None => return Err(RepositoryError::Other("No local cache path set".to_string())), + None => { + return Err(RepositoryError::Other( + "No local cache path set".to_string(), + )); + } }; // Ensure the per-publisher directory (local cache path) exists @@ -913,19 +964,23 @@ impl RestBackend { // Store the file directly under the per-publisher directory let file_path = cache_path.join(file_name); - let mut file = File::create(&file_path) - .map_err(|e| { - // Report failure - progress.finish(&progress_info); - RepositoryError::FileWriteError { path: file_path.clone(), source: e } - })?; + let mut file = File::create(&file_path).map_err(|e| { + // Report failure + progress.finish(&progress_info); + RepositoryError::FileWriteError { + path: file_path.clone(), + source: e, + } + })?; - file.write_all(&content) - .map_err(|e| { - // Report failure - progress.finish(&progress_info); - RepositoryError::FileWriteError { path: file_path.clone(), source: e } - })?; + file.write_all(&content).map_err(|e| { + // Report failure + progress.finish(&progress_info); + RepositoryError::FileWriteError { + path: file_path.clone(), + source: e, + } + })?; debug!("Stored catalog file: {}", file_path.display()); @@ -935,7 +990,7 @@ impl RestBackend { Ok(file_path) } - + /// Downloads all catalog files for a specific publisher. /// /// This method downloads the catalog.attrs file first to determine what catalog parts @@ -967,73 +1022,77 @@ impl RestBackend { ) -> Result<()> { // Use a no-op reporter if none was provided let progress_reporter = progress.unwrap_or(&NoopProgressReporter); - + // Create progress info for the overall operation - let mut overall_progress = ProgressInfo::new(format!("Downloading catalog for {}", publisher)); - + let mut overall_progress = + ProgressInfo::new(format!("Downloading catalog for {}", publisher)); + // Notify that we're starting the download progress_reporter.start(&overall_progress); - + // First download catalog.attrs to get the list of available parts - let attrs_path = self.download_and_store_catalog_file(publisher, "catalog.attrs", progress)?; - + let attrs_path = + self.download_and_store_catalog_file(publisher, "catalog.attrs", progress)?; + // Parse the catalog.attrs file to get the list of parts - let attrs_content = fs::read_to_string(&attrs_path) - .map_err(|e| { - progress_reporter.finish(&overall_progress); - RepositoryError::FileReadError { path: attrs_path.clone(), source: e } - })?; - - let attrs: Value = serde_json::from_str(&attrs_content) - .map_err(|e| { - progress_reporter.finish(&overall_progress); - RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e)) - })?; - + let attrs_content = fs::read_to_string(&attrs_path).map_err(|e| { + progress_reporter.finish(&overall_progress); + RepositoryError::FileReadError { + path: attrs_path.clone(), + source: e, + } + })?; + + let attrs: Value = serde_json::from_str(&attrs_content).map_err(|e| { + progress_reporter.finish(&overall_progress); + RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e)) + })?; + // Get the list of parts let parts = attrs["parts"].as_object().ok_or_else(|| { progress_reporter.finish(&overall_progress); RepositoryError::JsonParseError("Missing 'parts' field in catalog.attrs".to_string()) })?; - + // Update progress with total number of parts let total_parts = parts.len() as u64 + 1; // +1 for catalog.attrs overall_progress = overall_progress.with_total(total_parts).with_current(1); progress_reporter.update(&overall_progress); - + // Download each part for (i, part_name) in parts.keys().enumerate() { debug!("Downloading catalog part: {}", part_name); - + // Update progress with current part - overall_progress = overall_progress.with_current(i as u64 + 2) // +2 because we already downloaded catalog.attrs + overall_progress = overall_progress + .with_current(i as u64 + 2) // +2 because we already downloaded catalog.attrs .with_context(format!("Downloading part: {}", part_name)); progress_reporter.update(&overall_progress); - + self.download_and_store_catalog_file(publisher, part_name, progress)?; } - + // Get the catalog manager for this publisher let catalog_manager = self.get_catalog_manager(publisher)?; - + // Update progress for loading parts overall_progress = overall_progress.with_context("Loading catalog parts".to_string()); progress_reporter.update(&overall_progress); - + // Load the catalog parts for part_name in parts.keys() { catalog_manager.load_part(part_name)?; } - + // Report completion overall_progress = overall_progress.with_current(total_parts); progress_reporter.finish(&overall_progress); - + info!("Downloaded catalog for publisher: {}", publisher); - + Ok(()) } - + /// Download catalogs for all publishers /// /// # Arguments @@ -1046,19 +1105,19 @@ impl RestBackend { pub fn download_all_catalogs(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> { // Use a no-op reporter if none was provided let progress_reporter = progress.unwrap_or(&NoopProgressReporter); - + // Clone the publishers list to avoid borrowing issues let publishers = self.config.publishers.clone(); let total_publishers = publishers.len() as u64; - + // Create progress info for the overall operation let mut overall_progress = ProgressInfo::new("Downloading all catalogs") .with_total(total_publishers) .with_current(0); - + // Notify that we're starting the download progress_reporter.start(&overall_progress); - + // Download catalogs for each publisher for (i, publisher) in publishers.iter().enumerate() { // Update progress with current publisher @@ -1066,21 +1125,21 @@ impl RestBackend { .with_current(i as u64) .with_context(format!("Publisher: {}", publisher)); progress_reporter.update(&overall_progress); - + // Download catalog for this publisher self.download_catalog(publisher, progress)?; - + // Update progress after completing this publisher overall_progress = overall_progress.with_current(i as u64 + 1); progress_reporter.update(&overall_progress); } - + // Report completion progress_reporter.finish(&overall_progress); - + Ok(()) } - + /// Refresh the catalog for a publisher /// /// # Arguments @@ -1091,7 +1150,11 @@ impl RestBackend { /// # Returns /// /// * `Result<()>` - Ok if the catalog was refreshed successfully, Err otherwise - pub fn refresh_catalog(&mut self, publisher: &str, progress: Option<&dyn ProgressReporter>) -> Result<()> { + pub fn refresh_catalog( + &mut self, + publisher: &str, + progress: Option<&dyn ProgressReporter>, + ) -> Result<()> { self.download_catalog(publisher, progress) } } diff --git a/libips/src/repository/tests.rs b/libips/src/repository/tests.rs index 2dec5a5..53ae6f3 100644 --- a/libips/src/repository/tests.rs +++ b/libips/src/repository/tests.rs @@ -8,9 +8,9 @@ mod tests { use crate::actions::Manifest; use crate::fmri::Fmri; use crate::repository::{ - CatalogManager, FileBackend, ProgressInfo, ProgressReporter, - ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result, WritableRepository, - REPOSITORY_CONFIG_FILENAME, + CatalogManager, FileBackend, ProgressInfo, ProgressReporter, REPOSITORY_CONFIG_FILENAME, + ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result, + WritableRepository, }; use std::fs; use std::path::PathBuf; @@ -208,15 +208,21 @@ mod tests { assert!(repo.config.publishers.contains(&"example.com".to_string())); assert!(FileBackend::construct_catalog_path(&repo_path, "example.com").exists()); assert!(FileBackend::construct_package_dir(&repo_path, "example.com", "").exists()); - + // Check that the pub.p5i file was created for backward compatibility - let pub_p5i_path = repo_path.join("publisher").join("example.com").join("pub.p5i"); - assert!(pub_p5i_path.exists(), "pub.p5i file should be created for backward compatibility"); - + let pub_p5i_path = repo_path + .join("publisher") + .join("example.com") + .join("pub.p5i"); + assert!( + pub_p5i_path.exists(), + "pub.p5i file should be created for backward compatibility" + ); + // Verify the content of the pub.p5i file let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap(); let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap(); - + // Check the structure of the pub.p5i file assert_eq!(pub_p5i_json["version"], 1); assert!(pub_p5i_json["packages"].is_array()); @@ -246,7 +252,9 @@ mod tests { // Add a package to the part using the stored publisher let fmri = Fmri::parse("pkg://test/example@1.0.0").unwrap(); - catalog_manager.add_package_to_part("test_part", &fmri, None, None).unwrap(); + catalog_manager + .add_package_to_part("test_part", &fmri, None, None) + .unwrap(); // Save the part catalog_manager.save_part("test_part").unwrap(); @@ -286,7 +294,13 @@ mod tests { publish_package(&mut repo, &manifest_path, &prototype_dir, "test").unwrap(); // Check that the files were published in the publisher-specific directory - assert!(repo_path.join("publisher").join("test").join("file").exists()); + assert!( + repo_path + .join("publisher") + .join("test") + .join("file") + .exists() + ); // Get repository information let repo_info = repo.get_info().unwrap(); @@ -364,9 +378,11 @@ mod tests { // Check for specific files assert!(files.iter().any(|f| f.contains("usr/bin/hello"))); - assert!(files - .iter() - .any(|f| f.contains("usr/share/doc/example/README.txt"))); + assert!( + files + .iter() + .any(|f| f.contains("usr/share/doc/example/README.txt")) + ); assert!(files.iter().any(|f| f.contains("etc/config/example.conf"))); // Clean up @@ -428,7 +444,8 @@ mod tests { let hash = repo.store_file(&test_file_path, "test").unwrap(); // Check if the file was stored in the correct directory structure - let expected_path = FileBackend::construct_file_path_with_publisher(&repo_path, "test", &hash); + let expected_path = + FileBackend::construct_file_path_with_publisher(&repo_path, "test", &hash); // Verify that the file exists at the expected path assert!( @@ -448,7 +465,7 @@ mod tests { // Clean up cleanup_test_dir(&test_dir); } - + #[test] fn test_transaction_pub_p5i_creation() { // Run the setup script to prepare the test environment @@ -463,39 +480,42 @@ mod tests { // Create a new publisher through a transaction let publisher = "transaction_test"; - + // Start a transaction let mut transaction = repo.begin_transaction().unwrap(); - + // Set the publisher for the transaction transaction.set_publisher(publisher); - + // Add a simple manifest to the transaction let manifest_path = manifest_dir.join("example.p5m"); let manifest = Manifest::parse_file(&manifest_path).unwrap(); transaction.update_manifest(manifest); - + // Commit the transaction transaction.commit().unwrap(); - + // Check that the pub.p5i file was created for the new publisher let pub_p5i_path = repo_path.join("publisher").join(publisher).join("pub.p5i"); - assert!(pub_p5i_path.exists(), "pub.p5i file should be created for new publisher in transaction"); - + assert!( + pub_p5i_path.exists(), + "pub.p5i file should be created for new publisher in transaction" + ); + // Verify the content of the pub.p5i file let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap(); let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap(); - + // Check the structure of the pub.p5i file assert_eq!(pub_p5i_json["version"], 1); assert!(pub_p5i_json["packages"].is_array()); assert!(pub_p5i_json["publishers"].is_array()); assert_eq!(pub_p5i_json["publishers"][0]["name"], publisher); - + // Clean up cleanup_test_dir(&test_dir); } - + #[test] fn test_legacy_pkg5_repository_creation() { // Create a test directory @@ -508,20 +528,23 @@ mod tests { // Add a publisher let publisher = "openindiana.org"; repo.add_publisher(publisher).unwrap(); - + // Set as default publisher repo.set_default_publisher(publisher).unwrap(); - + // Check that the pkg5.repository file was created let pkg5_repo_path = repo_path.join("pkg5.repository"); - assert!(pkg5_repo_path.exists(), "pkg5.repository file should be created for backward compatibility"); - + assert!( + pkg5_repo_path.exists(), + "pkg5.repository file should be created for backward compatibility" + ); + // Verify the content of the pkg5.repository file let pkg5_content = fs::read_to_string(&pkg5_repo_path).unwrap(); - + // Print the content for debugging println!("pkg5.repository content:\n{}", pkg5_content); - + // Check that the file contains the expected sections and values assert!(pkg5_content.contains("[publisher]")); assert!(pkg5_content.contains("prefix=openindiana.org")); @@ -531,55 +554,58 @@ mod tests { assert!(pkg5_content.contains("signature-required-names=[]")); assert!(pkg5_content.contains("check-certificate-revocation=False")); assert!(pkg5_content.contains("[CONFIGURATION]")); - + // Clean up cleanup_test_dir(&test_dir); } - + #[test] fn test_rest_repository_local_functionality() { use crate::repository::RestBackend; - + // Create a test directory let test_dir = create_test_dir("rest_repository"); let cache_path = test_dir.join("cache"); - + println!("Test directory: {}", test_dir.display()); println!("Cache path: {}", cache_path.display()); - + // Create a REST repository let uri = "http://pkg.opensolaris.org/release"; let mut repo = RestBackend::open(uri).unwrap(); - + // Set the local cache path repo.set_local_cache_path(&cache_path).unwrap(); - + println!("Local cache path set to: {:?}", repo.local_cache_path); - + // Add a publisher let publisher = "openindiana.org"; repo.add_publisher(publisher).unwrap(); - + println!("Publisher added: {}", publisher); println!("Publishers in config: {:?}", repo.config.publishers); - + // Verify that the directory structure was created correctly let publisher_dir = cache_path.join("publisher").join(publisher); println!("Publisher directory: {}", publisher_dir.display()); println!("Publisher directory exists: {}", publisher_dir.exists()); - - assert!(publisher_dir.exists(), "Publisher directory should be created"); - + + assert!( + publisher_dir.exists(), + "Publisher directory should be created" + ); + let catalog_dir = publisher_dir.join("catalog"); println!("Catalog directory: {}", catalog_dir.display()); println!("Catalog directory exists: {}", catalog_dir.exists()); - + assert!(catalog_dir.exists(), "Catalog directory should be created"); - + // Clean up cleanup_test_dir(&test_dir); } - + /// A test progress reporter that records all progress events #[derive(Debug, Clone)] struct TestProgressReporter { @@ -590,7 +616,7 @@ mod tests { /// Records of all finish events finish_events: Arc>>, } - + impl TestProgressReporter { /// Create a new test progress reporter fn new() -> Self { @@ -600,116 +626,116 @@ mod tests { finish_events: Arc::new(Mutex::new(Vec::new())), } } - + /// Get the number of start events recorded fn start_count(&self) -> usize { self.start_events.lock().unwrap().len() } - + /// Get the number of update events recorded fn update_count(&self) -> usize { self.update_events.lock().unwrap().len() } - + /// Get the number of finish events recorded fn finish_count(&self) -> usize { self.finish_events.lock().unwrap().len() } - + /// Get a clone of all start events fn get_start_events(&self) -> Vec { self.start_events.lock().unwrap().clone() } - + /// Get a clone of all update events fn get_update_events(&self) -> Vec { self.update_events.lock().unwrap().clone() } - + /// Get a clone of all finish events fn get_finish_events(&self) -> Vec { self.finish_events.lock().unwrap().clone() } } - + impl ProgressReporter for TestProgressReporter { fn start(&self, info: &ProgressInfo) { let mut events = self.start_events.lock().unwrap(); events.push(info.clone()); } - + fn update(&self, info: &ProgressInfo) { let mut events = self.update_events.lock().unwrap(); events.push(info.clone()); } - + fn finish(&self, info: &ProgressInfo) { let mut events = self.finish_events.lock().unwrap(); events.push(info.clone()); } } - + #[test] fn test_progress_reporter() { // Create a test progress reporter let reporter = TestProgressReporter::new(); - + // Create some progress info let info1 = ProgressInfo::new("Test operation 1"); let info2 = ProgressInfo::new("Test operation 2") .with_current(50) .with_total(100); - + // Report some progress reporter.start(&info1); reporter.update(&info2); reporter.finish(&info1); - + // Check that the events were recorded assert_eq!(reporter.start_count(), 1); assert_eq!(reporter.update_count(), 1); assert_eq!(reporter.finish_count(), 1); - + // Check the content of the events let start_events = reporter.get_start_events(); let update_events = reporter.get_update_events(); let finish_events = reporter.get_finish_events(); - + assert_eq!(start_events[0].operation, "Test operation 1"); assert_eq!(update_events[0].operation, "Test operation 2"); assert_eq!(update_events[0].current, Some(50)); assert_eq!(update_events[0].total, Some(100)); assert_eq!(finish_events[0].operation, "Test operation 1"); } - + #[test] fn test_rest_backend_with_progress() { // This test is a mock test that doesn't actually connect to a remote server // It just verifies that the progress reporting mechanism works correctly - + // Create a test directory let test_dir = create_test_dir("rest_progress"); let cache_path = test_dir.join("cache"); - + // Create a REST repository let uri = "http://pkg.opensolaris.org/release"; let mut repo = RestBackend::create(uri, RepositoryVersion::V4).unwrap(); - + // Set the local cache path repo.set_local_cache_path(&cache_path).unwrap(); - + // Create a test progress reporter let reporter = TestProgressReporter::new(); - + // Add a publisher let publisher = "test"; repo.add_publisher(publisher).unwrap(); - + // Create a mock catalog.attrs file let publisher_dir = cache_path.join("publisher").join(publisher); let catalog_dir = publisher_dir.join("catalog"); fs::create_dir_all(&catalog_dir).unwrap(); - + let attrs_content = r#"{ "created": "20250803T124900Z", "last-modified": "20250803T124900Z", @@ -728,35 +754,39 @@ mod tests { }, "version": 1 }"#; - + let attrs_path = catalog_dir.join("catalog.attrs"); fs::write(&attrs_path, attrs_content).unwrap(); - + // Create mock catalog part files - for part_name in ["catalog.base.C", "catalog.dependency.C", "catalog.summary.C"] { + for part_name in [ + "catalog.base.C", + "catalog.dependency.C", + "catalog.summary.C", + ] { let part_path = catalog_dir.join(part_name); fs::write(&part_path, "{}").unwrap(); } - + // Mock the download_catalog_file method to avoid actual HTTP requests // This is done by creating the files before calling download_catalog - + // Create a simple progress update to ensure update events are recorded let progress_info = ProgressInfo::new("Test update") .with_current(1) .with_total(2); reporter.update(&progress_info); - + // Call download_catalog with the progress reporter // This will fail because we're not actually connecting to a server, // but we can still verify that the progress reporter was called let _ = repo.download_catalog(publisher, Some(&reporter)); - + // Check that the progress reporter was called assert!(reporter.start_count() > 0, "No start events recorded"); assert!(reporter.update_count() > 0, "No update events recorded"); assert!(reporter.finish_count() > 0, "No finish events recorded"); - + // Clean up cleanup_test_dir(&test_dir); } diff --git a/libips/src/solver/advice.rs b/libips/src/solver/advice.rs index 7e6c802..1c0a3ef 100644 --- a/libips/src/solver/advice.rs +++ b/libips/src/solver/advice.rs @@ -33,38 +33,52 @@ pub struct AdviceReport { #[derive(Debug, Default, Clone)] pub struct AdviceOptions { - pub max_depth: usize, // 0 = unlimited - pub dependency_cap: usize, // 0 = unlimited per node + pub max_depth: usize, // 0 = unlimited + pub dependency_cap: usize, // 0 = unlimited per node } #[derive(Default)] struct Ctx { // caches catalog_cache: HashMap>, // stem -> [(publisher, fmri)] - manifest_cache: HashMap, // fmri string -> manifest - lock_cache: HashMap>, // stem -> incorporated release - candidate_cache: HashMap<(String, Option, Option, Option), Option>, // (stem, rel, branch, publisher) + manifest_cache: HashMap, // fmri string -> manifest + lock_cache: HashMap>, // stem -> incorporated release + candidate_cache: + HashMap<(String, Option, Option, Option), Option>, // (stem, rel, branch, publisher) publisher_filter: Option, cap: usize, } impl Ctx { fn new(publisher_filter: Option, cap: usize) -> Self { - Self { publisher_filter, cap, ..Default::default() } + Self { + publisher_filter, + cap, + ..Default::default() + } } } -pub fn advise_from_error(image: &Image, err: &SolverError, opts: AdviceOptions) -> Result { +pub fn advise_from_error( + image: &Image, + err: &SolverError, + opts: AdviceOptions, +) -> Result { let mut report = AdviceReport::default(); let Some(problem) = err.problem() else { return Ok(report); }; match &problem.kind { - SolverProblemKind::NoCandidates { stem, release, branch } => { + SolverProblemKind::NoCandidates { + stem, + release, + branch, + } => { // Advise directly on the missing root let mut ctx = Ctx::new(None, opts.dependency_cap); - let details = build_missing_detail(image, &mut ctx, stem, release.as_deref(), branch.as_deref()); + let details = + build_missing_detail(image, &mut ctx, stem, release.as_deref(), branch.as_deref()); report.issues.push(AdviceIssue { path: vec![stem.clone()], stem: stem.clone(), @@ -78,11 +92,23 @@ pub fn advise_from_error(image: &Image, err: &SolverError, opts: AdviceOptions) // Fall back to analyzing roots and traversing dependencies to find a missing candidate leaf. let mut ctx = Ctx::new(None, opts.dependency_cap); for root in &problem.roots { - let root_fmri = match find_best_candidate(image, &mut ctx, &root.stem, root.version_req.as_deref(), root.branch.as_deref()) { + let root_fmri = match find_best_candidate( + image, + &mut ctx, + &root.stem, + root.version_req.as_deref(), + root.branch.as_deref(), + ) { Ok(Some(f)) => f, _ => { // Missing root candidate - let details = build_missing_detail(image, &mut ctx, &root.stem, root.version_req.as_deref(), root.branch.as_deref()); + let details = build_missing_detail( + image, + &mut ctx, + &root.stem, + root.version_req.as_deref(), + root.branch.as_deref(), + ); report.issues.push(AdviceIssue { path: vec![root.stem.clone()], stem: root.stem.clone(), @@ -97,7 +123,16 @@ pub fn advise_from_error(image: &Image, err: &SolverError, opts: AdviceOptions) // Depth-first traversal looking for missing candidates let mut path = vec![root.stem.clone()]; let mut seen = std::collections::HashSet::new(); - advise_recursive(image, &mut ctx, &root_fmri, &mut path, 1, opts.max_depth, &mut seen, &mut report)?; + advise_recursive( + image, + &mut ctx, + &root_fmri, + &mut path, + 1, + opts.max_depth, + &mut seen, + &mut report, + )?; } Ok(report) } @@ -114,30 +149,43 @@ fn advise_recursive( seen: &mut std::collections::HashSet, report: &mut AdviceReport, ) -> Result<(), AdviceError> { - if max_depth != 0 && depth > max_depth { return Ok(()); } + if max_depth != 0 && depth > max_depth { + return Ok(()); + } let manifest = get_manifest_cached(image, ctx, fmri)?; let mut processed = 0usize; - for dep in manifest.dependencies.iter().filter(|d| d.dependency_type == "require" || d.dependency_type == "incorporate") { - let Some(df) = &dep.fmri else { continue; }; + for dep in manifest + .dependencies + .iter() + .filter(|d| d.dependency_type == "require" || d.dependency_type == "incorporate") + { + let Some(df) = &dep.fmri else { + continue; + }; let dep_stem = df.stem().to_string(); // Extract constraints from optional properties and, if absent, from the dependency FMRI version string let (mut rel, mut br) = extract_constraint(&dep.optional); let df_ver_str = df.version(); if !df_ver_str.is_empty() { - if rel.is_none() { rel = version_release(&df_ver_str); } - if br.is_none() { br = version_branch(&df_ver_str); } + if rel.is_none() { + rel = version_release(&df_ver_str); + } + if br.is_none() { + br = version_branch(&df_ver_str); + } } // Mirror solver behavior: lock child to parent's branch when not explicitly constrained if br.is_none() { - let parent_branch = fmri - .version - .as_ref() - .and_then(|v| v.branch.clone()); - if let Some(pb) = parent_branch { br = Some(pb); } + let parent_branch = fmri.version.as_ref().and_then(|v| v.branch.clone()); + if let Some(pb) = parent_branch { + br = Some(pb); + } } - if ctx.cap != 0 && processed >= ctx.cap { break; } + if ctx.cap != 0 && processed >= ctx.cap { + break; + } processed += 1; match find_best_candidate(image, ctx, &dep_stem, rel.as_deref(), br.as_deref())? { @@ -150,7 +198,8 @@ fn advise_recursive( } } None => { - let details = build_missing_detail(image, ctx, &dep_stem, rel.as_deref(), br.as_deref()); + let details = + build_missing_detail(image, ctx, &dep_stem, rel.as_deref(), br.as_deref()); report.issues.push(AdviceIssue { path: path.clone(), stem: dep_stem.clone(), @@ -177,32 +226,76 @@ fn extract_constraint(optional: &[Property]) -> (Option, Option) (release, branch) } -fn build_missing_detail(image: &Image, ctx: &mut Ctx, stem: &str, release: Option<&str>, branch: Option<&str>) -> String { +fn build_missing_detail( + image: &Image, + ctx: &mut Ctx, + stem: &str, + release: Option<&str>, + branch: Option<&str>, +) -> String { let mut available: Vec = Vec::new(); if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) { for (pubname, fmri) in list { - if let Some(ref pfilter) = ctx.publisher_filter { if &pubname != pfilter { continue; } } - if fmri.stem() != stem { continue; } + if let Some(ref pfilter) = ctx.publisher_filter { + if &pubname != pfilter { + continue; + } + } + if fmri.stem() != stem { + continue; + } let ver = fmri.version(); - if ver.is_empty() { continue; } + if ver.is_empty() { + continue; + } available.push(ver); } } available.sort(); available.dedup(); - let available_str = if available.is_empty() { "".to_string() } else { available.join(", ") }; - let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten(); + let available_str = if available.is_empty() { + "".to_string() + } else { + available.join(", ") + }; + let lock = get_incorporated_release_cached(image, ctx, stem) + .ok() + .flatten(); match (release, branch, lock.as_deref()) { - (Some(r), Some(b), Some(lr)) => format!("Required release={}, branch={} not found. Image incorporation lock release={} may constrain candidates. Available versions: {}", r, b, lr, available_str), - (Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str), - (Some(r), None, Some(lr)) => format!("Required release={} not found. Image incorporation lock release={} present. Available versions: {}", r, lr, available_str), - (Some(r), None, None) => format!("Required release={} not found. Available versions: {}", r, available_str), - (None, Some(b), Some(lr)) => format!("Required branch={} not found. Image incorporation lock release={} present. Available versions: {}", b, lr, available_str), - (None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str), - (None, None, Some(lr)) => format!("No candidates matched. Image incorporation lock release={} present. Available versions: {}", lr, available_str), - (None, None, None) => format!("No candidates matched. Available versions: {}", available_str), + (Some(r), Some(b), Some(lr)) => format!( + "Required release={}, branch={} not found. Image incorporation lock release={} may constrain candidates. Available versions: {}", + r, b, lr, available_str + ), + (Some(r), Some(b), None) => format!( + "Required release={}, branch={} not found. Available versions: {}", + r, b, available_str + ), + (Some(r), None, Some(lr)) => format!( + "Required release={} not found. Image incorporation lock release={} present. Available versions: {}", + r, lr, available_str + ), + (Some(r), None, None) => format!( + "Required release={} not found. Available versions: {}", + r, available_str + ), + (None, Some(b), Some(lr)) => format!( + "Required branch={} not found. Image incorporation lock release={} present. Available versions: {}", + b, lr, available_str + ), + (None, Some(b), None) => format!( + "Required branch={} not found. Available versions: {}", + b, available_str + ), + (None, None, Some(lr)) => format!( + "No candidates matched. Image incorporation lock release={} present. Available versions: {}", + lr, available_str + ), + (None, None, None) => format!( + "No candidates matched. Available versions: {}", + available_str + ), } } @@ -219,20 +312,48 @@ fn find_best_candidate( req_branch.map(|s| s.to_string()), ctx.publisher_filter.clone(), ); - if let Some(cached) = ctx.candidate_cache.get(&key) { return Ok(cached.clone()); } + if let Some(cached) = ctx.candidate_cache.get(&key) { + return Ok(cached.clone()); + } - let lock_release = if req_release.is_none() { get_incorporated_release_cached(image, ctx, stem).ok().flatten() } else { None }; + let lock_release = if req_release.is_none() { + get_incorporated_release_cached(image, ctx, stem) + .ok() + .flatten() + } else { + None + }; let mut candidates: Vec<(String, Fmri)> = Vec::new(); for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? { - if let Some(ref pfilter) = ctx.publisher_filter { if &pubf != pfilter { continue; } } - if pfmri.stem() != stem { continue; } + if let Some(ref pfilter) = ctx.publisher_filter { + if &pubf != pfilter { + continue; + } + } + if pfmri.stem() != stem { + continue; + } let ver = pfmri.version(); - if ver.is_empty() { continue; } + if ver.is_empty() { + continue; + } let rel = version_release(&ver); let br = version_branch(&ver); - if let Some(req_r) = req_release { if Some(req_r) != rel.as_deref() { continue; } } else if let Some(lock_r) = lock_release.as_deref() { if Some(lock_r) != rel.as_deref() { continue; } } - if let Some(req_b) = req_branch { if Some(req_b) != br.as_deref() { continue; } } + if let Some(req_r) = req_release { + if Some(req_r) != rel.as_deref() { + continue; + } + } else if let Some(lock_r) = lock_release.as_deref() { + if Some(lock_r) != rel.as_deref() { + continue; + } + } + if let Some(req_b) = req_branch { + if Some(req_b) != br.as_deref() { + continue; + } + } candidates.push((ver.clone(), pfmri.clone())); } @@ -247,7 +368,9 @@ fn version_release(version: &str) -> Option { } fn version_branch(version: &str) -> Option { - if let Some((_, rest)) = version.split_once(',') { return rest.split_once('-').map(|(b, _)| b.to_string()); } + if let Some((_, rest)) = version.split_once(',') { + return rest.split_once('-').map(|(b, _)| b.to_string()); + } None } @@ -256,8 +379,13 @@ fn query_catalog_cached( ctx: &Ctx, stem: &str, ) -> Result, AdviceError> { - if let Some(v) = ctx.catalog_cache.get(stem) { return Ok(v.clone()); } - let mut tmp = Ctx { catalog_cache: ctx.catalog_cache.clone(), ..Default::default() }; + if let Some(v) = ctx.catalog_cache.get(stem) { + return Ok(v.clone()); + } + let mut tmp = Ctx { + catalog_cache: ctx.catalog_cache.clone(), + ..Default::default() + }; query_catalog_cached_mut(image, &mut tmp, stem) } @@ -266,26 +394,48 @@ fn query_catalog_cached_mut( ctx: &mut Ctx, stem: &str, ) -> Result, AdviceError> { - if let Some(v) = ctx.catalog_cache.get(stem) { return Ok(v.clone()); } + if let Some(v) = ctx.catalog_cache.get(stem) { + return Ok(v.clone()); + } let mut out = Vec::new(); - let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError{ message: format!("Failed to query catalog for {}: {}", stem, e) })?; - for p in res { out.push((p.publisher, p.fmri)); } + let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError { + message: format!("Failed to query catalog for {}: {}", stem, e), + })?; + for p in res { + out.push((p.publisher, p.fmri)); + } ctx.catalog_cache.insert(stem.to_string(), out.clone()); Ok(out) } fn get_manifest_cached(image: &Image, ctx: &mut Ctx, fmri: &Fmri) -> Result { let key = fmri.to_string(); - if let Some(m) = ctx.manifest_cache.get(&key) { return Ok(m.clone()); } - let manifest_opt = image.get_manifest_from_catalog(fmri).map_err(|e| AdviceError { message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e) })?; + if let Some(m) = ctx.manifest_cache.get(&key) { + return Ok(m.clone()); + } + let manifest_opt = image + .get_manifest_from_catalog(fmri) + .map_err(|e| AdviceError { + message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e), + })?; let manifest = manifest_opt.unwrap_or_else(Manifest::new); ctx.manifest_cache.insert(key, manifest.clone()); Ok(manifest) } -fn get_incorporated_release_cached(image: &Image, ctx: &mut Ctx, stem: &str) -> Result, AdviceError> { - if let Some(v) = ctx.lock_cache.get(stem) { return Ok(v.clone()); } - let v = image.get_incorporated_release(stem).map_err(|e| AdviceError{ message: format!("Failed to read incorporation lock for {}: {}", stem, e) })?; +fn get_incorporated_release_cached( + image: &Image, + ctx: &mut Ctx, + stem: &str, +) -> Result, AdviceError> { + if let Some(v) = ctx.lock_cache.get(stem) { + return Ok(v.clone()); + } + let v = image + .get_incorporated_release(stem) + .map_err(|e| AdviceError { + message: format!("Failed to read incorporation lock for {}: {}", stem, e), + })?; ctx.lock_cache.insert(stem.to_string(), v.clone()); Ok(v) } diff --git a/libips/src/solver/mod.rs b/libips/src/solver/mod.rs index 121bbf5..8b96bcf 100644 --- a/libips/src/solver/mod.rs +++ b/libips/src/solver/mod.rs @@ -18,14 +18,19 @@ use miette::Diagnostic; // Begin resolvo wiring imports (names discovered by compiler) // We start broad and refine with compiler guidance. -use resolvo::{self, Candidates, Condition, ConditionId, ConditionalRequirement, Dependencies as RDependencies, DependencyProvider, HintDependenciesAvailable, Interner, KnownDependencies, Mapping, NameId, Problem as RProblem, SolvableId, Solver as RSolver, SolverCache, StringId, UnsolvableOrCancelled, VersionSetId, VersionSetUnionId}; +use lz4::Decoder as Lz4Decoder; +use redb::{ReadableDatabase, ReadableTable}; +use resolvo::{ + self, Candidates, Condition, ConditionId, ConditionalRequirement, + Dependencies as RDependencies, DependencyProvider, HintDependenciesAvailable, Interner, + KnownDependencies, Mapping, NameId, Problem as RProblem, SolvableId, Solver as RSolver, + SolverCache, StringId, UnsolvableOrCancelled, VersionSetId, VersionSetUnionId, +}; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; -use thiserror::Error; -use redb::{ReadableDatabase, ReadableTable}; -use lz4::Decoder as Lz4Decoder; use std::io::{Cursor, Read}; +use thiserror::Error; use crate::actions::Manifest; use crate::image::catalog::{CATALOG_TABLE, INCORPORATE_TABLE}; @@ -36,8 +41,12 @@ pub mod advice; // Local helpers to decode manifest bytes stored in catalog DB (JSON or LZ4-compressed JSON) fn is_likely_json_local(bytes: &[u8]) -> bool { let mut i = 0; - while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { i += 1; } - if i >= bytes.len() { return false; } + while i < bytes.len() && matches!(bytes[i], b' ' | b'\n' | b'\r' | b'\t') { + i += 1; + } + if i >= bytes.len() { + return false; + } matches!(bytes[i], b'{' | b'[') } @@ -92,7 +101,7 @@ struct IpsProvider<'a> { vs_name: RefCell>, unions: RefCell>>, // per-name publisher preference order; set by dependency processing or top-level specs - publisher_prefs: RefCell>>, + publisher_prefs: RefCell>>, } use crate::fmri::Fmri; use crate::image::Image; @@ -145,27 +154,31 @@ impl<'a> IpsProvider<'a> { .iter() .map_err(|e| SolverError::new(format!("iterate catalog table: {}", e)))? { - let (k, v) = entry.map_err(|e| SolverError::new(format!("read catalog entry: {}", e)))?; + let (k, v) = + entry.map_err(|e| SolverError::new(format!("read catalog entry: {}", e)))?; let key = k.value(); // stem@version // Try to decode manifest and extract full FMRI (including publisher) let mut pushed = false; if let Ok(manifest) = decode_manifest_bytes_local(v.value()) { - if let Some(attr) = manifest - .attributes - .iter() - .find(|a| a.key == "pkg.fmri") - { + if let Some(attr) = manifest.attributes.iter().find(|a| a.key == "pkg.fmri") { if let Some(fmri_str) = attr.values.get(0) { if let Ok(mut fmri) = Fmri::parse(fmri_str) { // Ensure publisher is present; if missing/empty, use image default publisher - let missing_pub = fmri.publisher.as_deref().map(|s| s.is_empty()).unwrap_or(true); + let missing_pub = fmri + .publisher + .as_deref() + .map(|s| s.is_empty()) + .unwrap_or(true); if missing_pub { if let Ok(defp) = self.image.default_publisher() { fmri.publisher = Some(defp.name.clone()); } } - by_stem.entry(fmri.stem().to_string()).or_default().push(fmri); + by_stem + .entry(fmri.stem().to_string()) + .or_default() + .push(fmri); pushed = true; } } @@ -289,8 +302,9 @@ impl<'a> Interner for IpsProvider<'a> { Some(VersionSetKind::Any) => "any".to_string(), Some(VersionSetKind::ReleaseEq(r)) => format!("release={}", r), Some(VersionSetKind::BranchEq(b)) => format!("branch={}", b), - Some(VersionSetKind::ReleaseAndBranch { release, branch }) => - format!("release={}, branch={}", release, branch), + Some(VersionSetKind::ReleaseAndBranch { release, branch }) => { + format!("release={}, branch={}", release, branch) + } None => "".to_string(), } } @@ -300,7 +314,11 @@ impl<'a> Interner for IpsProvider<'a> { } fn version_set_name(&self, version_set: VersionSetId) -> NameId { - *self.vs_name.borrow().get(version_set).expect("version set name present") + *self + .vs_name + .borrow() + .get(version_set) + .expect("version set name present") } fn solvable_name(&self, solvable: SolvableId) -> NameId { @@ -349,10 +367,7 @@ fn fmri_matches_version_set(fmri: &Fmri, kind: &VersionSetKind) -> bool { VersionSetKind::ReleaseEq(req_rel) => fmri .version .as_ref() - .map(|v| { - release_satisfies(req_rel, &v.release) - || v.branch.as_deref() == Some(req_rel) - }) + .map(|v| release_satisfies(req_rel, &v.release) || v.branch.as_deref() == Some(req_rel)) .unwrap_or(false), VersionSetKind::BranchEq(req_branch) => fmri .version @@ -363,7 +378,8 @@ fn fmri_matches_version_set(fmri: &Fmri, kind: &VersionSetKind) -> bool { VersionSetKind::ReleaseAndBranch { release, branch } => { let (mut ok_rel, mut ok_branch) = (false, false); if let Some(v) = fmri.version.as_ref() { - ok_rel = release_satisfies(release, &v.release) || v.branch.as_deref() == Some(release); + ok_rel = + release_satisfies(release, &v.release) || v.branch.as_deref() == Some(release); ok_branch = v.branch.as_ref().map(|b| b == branch).unwrap_or(false); } ok_rel && ok_branch @@ -421,9 +437,15 @@ impl<'a> DependencyProvider for IpsProvider<'a> { let fmri = &self.solvables.get(*sid).unwrap().fmri; if let Some(cv) = fmri.version.as_ref() { if let Some(lv) = parsed_lock.as_ref() { - if cv.release != lv.release { return false; } - if cv.branch != lv.branch { return false; } - if cv.build != lv.build { return false; } + if cv.release != lv.release { + return false; + } + if cv.branch != lv.branch { + return false; + } + if cv.build != lv.build { + return false; + } if lv.timestamp.is_some() { return cv.timestamp == lv.timestamp; } @@ -501,13 +523,14 @@ impl<'a> DependencyProvider for IpsProvider<'a> { // Build requirements for "require" deps let mut reqs: Vec = Vec::new(); - let parent_branch = fmri - .version - .as_ref() - .and_then(|v| v.branch.clone()); + let parent_branch = fmri.version.as_ref().and_then(|v| v.branch.clone()); let parent_pub = fmri.publisher.as_deref(); - for d in manifest.dependencies.iter().filter(|d| d.dependency_type == "require") { + for d in manifest + .dependencies + .iter() + .filter(|d| d.dependency_type == "require") + { if let Some(df) = &d.fmri { let stem = df.stem().to_string(); let Some(child_name_id) = self.name_by_str.get(&stem).copied() else { @@ -544,7 +567,11 @@ impl<'a> DependencyProvider for IpsProvider<'a> { #[derive(Debug, Clone)] pub enum SolverProblemKind { - NoCandidates { stem: String, release: Option, branch: Option }, + NoCandidates { + stem: String, + release: Option, + branch: Option, + }, Unsolvable, } @@ -558,7 +585,9 @@ pub struct SolverFailure { #[error("Solver error: {message}")] #[diagnostic( code(ips::solver_error::generic), - help("Check package names and repository catalogs. Use 'pkg6 image catalog --dump' for debugging.") + help( + "Check package names and repository catalogs. Use 'pkg6 image catalog --dump' for debugging." + ) )] pub struct SolverError { pub message: String, @@ -566,9 +595,21 @@ pub struct SolverError { } impl SolverError { - fn new(msg: impl Into) -> Self { Self { message: msg.into(), problem: None } } - pub fn with_details(msg: impl Into, problem: SolverFailure) -> Self { Self { message: msg.into(), problem: Some(problem) } } - pub fn problem(&self) -> Option<&SolverFailure> { self.problem.as_ref() } + fn new(msg: impl Into) -> Self { + Self { + message: msg.into(), + problem: None, + } + } + pub fn with_details(msg: impl Into, problem: SolverFailure) -> Self { + Self { + message: msg.into(), + problem: Some(problem), + } + } + pub fn problem(&self) -> Option<&SolverFailure> { + self.problem.as_ref() + } } #[derive(Debug, Clone)] @@ -599,9 +640,6 @@ pub struct Constraint { pub branch: Option, } - - - /// IPS-specific comparison: newest release first; if equal, newest timestamp. fn cmp_release_desc(a: &Fmri, b: &Fmri) -> std::cmp::Ordering { let a_rel = a.version.as_ref(); @@ -647,7 +685,10 @@ fn version_order_desc(a: &Fmri, b: &Fmri) -> std::cmp::Ordering { } /// Resolve an install plan for the given constraints. -pub fn resolve_install(image: &Image, constraints: &[Constraint]) -> Result { +pub fn resolve_install( + image: &Image, + constraints: &[Constraint], +) -> Result { // Build provider indexed from catalog let mut provider = IpsProvider::new(image)?; @@ -678,10 +719,7 @@ pub fn resolve_install(image: &Image, constraints: &[Constraint]) -> Result Result Result = root_names.iter().map(|(_, c)| c.clone()).collect(); let problem = if let Some(c) = first_missing { SolverFailure { - kind: SolverProblemKind::NoCandidates { stem: c.stem.clone(), release: c.version_req.clone(), branch: c.branch.clone() }, + kind: SolverProblemKind::NoCandidates { + stem: c.stem.clone(), + release: c.version_req.clone(), + branch: c.branch.clone(), + }, roots, } } else { - SolverFailure { kind: SolverProblemKind::Unsolvable, roots } + SolverFailure { + kind: SolverProblemKind::Unsolvable, + roots, + } }; return Err(SolverError::with_details( format!( @@ -788,23 +836,28 @@ pub fn resolve_install(image: &Image, constraints: &[Constraint]) -> Result = root_names.iter().map(|(_, c)| c.clone()).collect(); let mut solver = RSolver::new(provider); - let solution_ids = solver.solve(problem).map_err(|conflict_or_cancelled| { - match conflict_or_cancelled { - UnsolvableOrCancelled::Unsolvable(u) => { - let msg = u.display_user_friendly(&solver).to_string(); - SolverError::with_details( - msg, - SolverFailure { kind: SolverProblemKind::Unsolvable, roots: roots_for_err.clone() }, - ) - } - UnsolvableOrCancelled::Cancelled(_) => { - SolverError::with_details( + let solution_ids = + solver + .solve(problem) + .map_err(|conflict_or_cancelled| match conflict_or_cancelled { + UnsolvableOrCancelled::Unsolvable(u) => { + let msg = u.display_user_friendly(&solver).to_string(); + SolverError::with_details( + msg, + SolverFailure { + kind: SolverProblemKind::Unsolvable, + roots: roots_for_err.clone(), + }, + ) + } + UnsolvableOrCancelled::Cancelled(_) => SolverError::with_details( "dependency resolution cancelled".to_string(), - SolverFailure { kind: SolverProblemKind::Unsolvable, roots: roots_for_err.clone() }, - ) - } - } - })?; + SolverFailure { + kind: SolverProblemKind::Unsolvable, + roots: roots_for_err.clone(), + }, + ), + })?; // Build plan from solution let image_ref = image; @@ -821,7 +874,12 @@ pub fn resolve_install(image: &Image, constraints: &[Constraint]) -> Result m, - _ => return Err(SolverError::new(format!("failed to obtain manifest for {}: {}", fmri, repo_err))), + _ => { + return Err(SolverError::new(format!( + "failed to obtain manifest for {}: {}", + fmri, repo_err + ))); + } } } } @@ -833,7 +891,6 @@ pub fn resolve_install(image: &Image, constraints: &[Constraint]) -> Result, image: &Image) -> Vec { let mut order: Vec = Vec::new(); // 1) parent publisher first if provided @@ -855,7 +912,6 @@ fn build_publisher_preference(parent_pub: Option<&str>, image: &Image) -> Vec, timestamp: Option<&str>) -> Version { let mut v = Version::new(release); - if let Some(b) = branch { v.branch = Some(b.to_string()); } - if let Some(t) = timestamp { v.timestamp = Some(t.to_string()); } + if let Some(b) = branch { + v.branch = Some(b.to_string()); + } + if let Some(t) = timestamp { + v.timestamp = Some(t.to_string()); + } v } @@ -917,7 +976,9 @@ mod solver_integration_tests { let mut table = tx.open_table(CATALOG_TABLE).expect("open catalog table"); let key = format!("{}@{}", fmri.stem(), fmri.version()); let val = serde_json::to_vec(manifest).expect("serialize manifest"); - table.insert(key.as_str(), val.as_slice()).expect("insert manifest"); + table + .insert(key.as_str(), val.as_slice()) + .expect("insert manifest"); } tx.commit().expect("commit"); } @@ -926,11 +987,15 @@ mod solver_integration_tests { let db = Database::open(image.obsoleted_db_path()).expect("open obsoleted db"); let tx = db.begin_write().expect("begin write"); { - let mut table = tx.open_table(OBSOLETED_TABLE).expect("open obsoleted table"); + let mut table = tx + .open_table(OBSOLETED_TABLE) + .expect("open obsoleted table"); let key = fmri.to_string(); // store empty value let empty: Vec = Vec::new(); - table.insert(key.as_str(), empty.as_slice()).expect("insert obsolete"); + table + .insert(key.as_str(), empty.as_slice()) + .expect("insert obsolete"); } tx.commit().expect("commit"); } @@ -941,8 +1006,13 @@ mod solver_integration_tests { let path = td.keep(); let mut img = Image::create_image(&path, ImageType::Partial).expect("create image"); for (name, is_default) in pubs.iter().copied() { - img.add_publisher(name, &format!("https://example.com/{name}"), vec![], is_default) - .expect("add publisher"); + img.add_publisher( + name, + &format!("https://example.com/{name}"), + vec![], + is_default, + ) + .expect("add publisher"); } img } @@ -951,15 +1021,32 @@ mod solver_integration_tests { fn select_newest_release_then_timestamp() { let img = make_image_with_publishers(&[("pubA", true)]); - let fmri_100_old = mk_fmri("pubA", "pkg/alpha", mk_version("1.0", None, Some("20200101T000000Z"))); - let fmri_100_new = mk_fmri("pubA", "pkg/alpha", mk_version("1.0", None, Some("20200201T000000Z"))); - let fmri_110_any = mk_fmri("pubA", "pkg/alpha", mk_version("1.1", None, Some("20200115T000000Z"))); + let fmri_100_old = mk_fmri( + "pubA", + "pkg/alpha", + mk_version("1.0", None, Some("20200101T000000Z")), + ); + let fmri_100_new = mk_fmri( + "pubA", + "pkg/alpha", + mk_version("1.0", None, Some("20200201T000000Z")), + ); + let fmri_110_any = mk_fmri( + "pubA", + "pkg/alpha", + mk_version("1.1", None, Some("20200115T000000Z")), + ); write_manifest_to_catalog(&img, &fmri_100_old, &mk_manifest(&fmri_100_old, &[])); write_manifest_to_catalog(&img, &fmri_100_new, &mk_manifest(&fmri_100_new, &[])); write_manifest_to_catalog(&img, &fmri_110_any, &mk_manifest(&fmri_110_any, &[])); - let c = Constraint { stem: "pkg/alpha".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/alpha".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); assert!(!plan.add.is_empty()); let chosen = &plan.add[0].fmri; @@ -970,14 +1057,31 @@ mod solver_integration_tests { fn ignore_obsolete_candidates() { let img = make_image_with_publishers(&[("pubA", true)]); - let fmri_non_obsolete = mk_fmri("pubA", "pkg/beta", mk_version("0.9", None, Some("20200101T000000Z"))); - let fmri_obsolete = mk_fmri("pubA", "pkg/beta", mk_version("1.0", None, Some("20200301T000000Z"))); + let fmri_non_obsolete = mk_fmri( + "pubA", + "pkg/beta", + mk_version("0.9", None, Some("20200101T000000Z")), + ); + let fmri_obsolete = mk_fmri( + "pubA", + "pkg/beta", + mk_version("1.0", None, Some("20200301T000000Z")), + ); - write_manifest_to_catalog(&img, &fmri_non_obsolete, &mk_manifest(&fmri_non_obsolete, &[])); + write_manifest_to_catalog( + &img, + &fmri_non_obsolete, + &mk_manifest(&fmri_non_obsolete, &[]), + ); // mark the 1.0 as obsolete (not adding to catalog table) mark_obsolete(&img, &fmri_obsolete); - let c = Constraint { stem: "pkg/beta".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/beta".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); assert!(!plan.add.is_empty()); let chosen = &plan.add[0].fmri; @@ -1003,16 +1107,24 @@ mod solver_integration_tests { // Configure image publisher to point to file:// repo let origin = format!("file://{}", repo_path.display()); - img.add_publisher("pubA", &origin, vec![], true).expect("add publisher to image"); + img.add_publisher("pubA", &origin, vec![], true) + .expect("add publisher to image"); // Define FMRI and limited manifest in catalog (deps only) - let fmri = mk_fmri("pubA", "pkg/alpha", mk_version("1.0", None, Some("20200401T000000Z"))); + let fmri = mk_fmri( + "pubA", + "pkg/alpha", + mk_version("1.0", None, Some("20200401T000000Z")), + ); let limited = mk_manifest(&fmri, &[]); // no files/dirs write_manifest_to_catalog(&img, &fmri, &limited); // Write full manifest into repository at expected path - let repo_manifest_path = FileBackend::construct_manifest_path(&repo_path, "pubA", fmri.stem(), &fmri.version()); - if let Some(parent) = repo_manifest_path.parent() { fs::create_dir_all(parent).unwrap(); } + let repo_manifest_path = + FileBackend::construct_manifest_path(&repo_path, "pubA", fmri.stem(), &fmri.version()); + if let Some(parent) = repo_manifest_path.parent() { + fs::create_dir_all(parent).unwrap(); + } let full_manifest_text = format!( "set name=pkg.fmri value={}\n\ dir path=opt/test owner=root group=bin mode=0755\n\ @@ -1022,11 +1134,19 @@ mod solver_integration_tests { fs::write(&repo_manifest_path, full_manifest_text).expect("write manifest to repo"); // Resolve and ensure we got the repo (full) manifest with file/dir actions - let c = Constraint { stem: "pkg/alpha".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/alpha".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); assert_eq!(plan.add.len(), 1); let man = &plan.add[0].manifest; - assert!(man.directories.len() >= 1, "expected directories from repo manifest"); + assert!( + man.directories.len() >= 1, + "expected directories from repo manifest" + ); assert!(man.files.len() >= 1, "expected files from repo manifest"); } @@ -1034,23 +1154,52 @@ mod solver_integration_tests { fn dependency_sticks_to_parent_branch() { let img = make_image_with_publishers(&[("pubA", true)]); // Parent pkg on branch 1 with a require on dep@5.11 - let parent = mk_fmri("pubA", "pkg/parent", mk_version("5.11", Some("1"), Some("20200102T000000Z"))); + let parent = mk_fmri( + "pubA", + "pkg/parent", + mk_version("5.11", Some("1"), Some("20200102T000000Z")), + ); let dep_req = Fmri::with_version("pkg/dep", Version::new("5.11")); let parent_manifest = mk_manifest(&parent, &[dep_req.clone()]); write_manifest_to_catalog(&img, &parent, &parent_manifest); // dep on branch 1 (older) and branch 2 (newer) — branch 1 must be selected - let dep_branch1_old = mk_fmri("pubA", "pkg/dep", mk_version("5.11", Some("1"), Some("20200101T000000Z"))); - let dep_branch1_new = mk_fmri("pubA", "pkg/dep", mk_version("5.11", Some("1"), Some("20200201T000000Z"))); - let dep_branch2_newer = mk_fmri("pubA", "pkg/dep", mk_version("5.11", Some("2"), Some("20200401T000000Z"))); + let dep_branch1_old = mk_fmri( + "pubA", + "pkg/dep", + mk_version("5.11", Some("1"), Some("20200101T000000Z")), + ); + let dep_branch1_new = mk_fmri( + "pubA", + "pkg/dep", + mk_version("5.11", Some("1"), Some("20200201T000000Z")), + ); + let dep_branch2_newer = mk_fmri( + "pubA", + "pkg/dep", + mk_version("5.11", Some("2"), Some("20200401T000000Z")), + ); write_manifest_to_catalog(&img, &dep_branch1_old, &mk_manifest(&dep_branch1_old, &[])); write_manifest_to_catalog(&img, &dep_branch1_new, &mk_manifest(&dep_branch1_new, &[])); - write_manifest_to_catalog(&img, &dep_branch2_newer, &mk_manifest(&dep_branch2_newer, &[])); + write_manifest_to_catalog( + &img, + &dep_branch2_newer, + &mk_manifest(&dep_branch2_newer, &[]), + ); - let c = Constraint { stem: "pkg/parent".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/parent".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); // find dep in plan - let dep_pkg = plan.add.iter().find(|p| p.fmri.stem() == "pkg/dep").expect("dep present"); + let dep_pkg = plan + .add + .iter() + .find(|p| p.fmri.stem() == "pkg/dep") + .expect("dep present"); let v = dep_pkg.fmri.version.as_ref().unwrap(); assert_eq!(v.release, "5.11"); assert_eq!(v.branch.as_deref(), Some("1")); @@ -1063,33 +1212,71 @@ mod solver_integration_tests { let img = make_image_with_publishers(&[("pubA", true), ("pubB", false)]); // Ensure image publishers order contains both; default already set by first. - let parent = mk_fmri("pubA", "pkg/root", mk_version("1.0", None, Some("20200101T000000Z"))); + let parent = mk_fmri( + "pubA", + "pkg/root", + mk_version("1.0", None, Some("20200101T000000Z")), + ); let dep_req = Fmri::with_version("pkg/child", Version::new("1.0")); let parent_manifest = mk_manifest(&parent, &[dep_req.clone()]); write_manifest_to_catalog(&img, &parent, &parent_manifest); - let dep_pub_a_old = mk_fmri("pubA", "pkg/child", mk_version("1.0", None, Some("20200101T000000Z"))); - let dep_pub_b_new = mk_fmri("pubB", "pkg/child", mk_version("1.0", None, Some("20200301T000000Z"))); + let dep_pub_a_old = mk_fmri( + "pubA", + "pkg/child", + mk_version("1.0", None, Some("20200101T000000Z")), + ); + let dep_pub_b_new = mk_fmri( + "pubB", + "pkg/child", + mk_version("1.0", None, Some("20200301T000000Z")), + ); write_manifest_to_catalog(&img, &dep_pub_a_old, &mk_manifest(&dep_pub_a_old, &[])); write_manifest_to_catalog(&img, &dep_pub_b_new, &mk_manifest(&dep_pub_b_new, &[])); - let c = Constraint { stem: "pkg/root".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/root".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); - let dep_pkg = plan.add.iter().find(|p| p.fmri.stem() == "pkg/child").expect("child present"); + let dep_pkg = plan + .add + .iter() + .find(|p| p.fmri.stem() == "pkg/child") + .expect("child present"); assert_eq!(dep_pkg.fmri.publisher.as_deref(), Some("pubA")); } #[test] fn top_level_release_only_version_requirement() { let img = make_image_with_publishers(&[("pubA", true)]); - let v10_old = mk_fmri("pubA", "pkg/vers", mk_version("1.0", None, Some("20200101T000000Z"))); - let v10_new = mk_fmri("pubA", "pkg/vers", mk_version("1.0", None, Some("20200201T000000Z"))); - let v11 = mk_fmri("pubA", "pkg/vers", mk_version("1.1", None, Some("20200301T000000Z"))); + let v10_old = mk_fmri( + "pubA", + "pkg/vers", + mk_version("1.0", None, Some("20200101T000000Z")), + ); + let v10_new = mk_fmri( + "pubA", + "pkg/vers", + mk_version("1.0", None, Some("20200201T000000Z")), + ); + let v11 = mk_fmri( + "pubA", + "pkg/vers", + mk_version("1.1", None, Some("20200301T000000Z")), + ); write_manifest_to_catalog(&img, &v10_old, &mk_manifest(&v10_old, &[])); write_manifest_to_catalog(&img, &v10_new, &mk_manifest(&v10_new, &[])); write_manifest_to_catalog(&img, &v11, &mk_manifest(&v11, &[])); - let c = Constraint { stem: "pkg/vers".to_string(), version_req: Some("1.0".to_string()), preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/vers".to_string(), + version_req: Some("1.0".to_string()), + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); let chosen = &plan.add[0].fmri; let v = chosen.version.as_ref().unwrap(); @@ -1098,7 +1285,6 @@ mod solver_integration_tests { } } - #[cfg(test)] mod no_candidate_error_tests { use super::*; @@ -1110,30 +1296,43 @@ mod no_candidate_error_tests { let td = tempfile::tempdir().expect("tempdir"); let img_path = td.path().to_path_buf(); let mut img = Image::create_image(&img_path, ImageType::Partial).expect("create image"); - img.add_publisher("pubA", "https://example.com/pubA", vec![], true).expect("add publisher"); + img.add_publisher("pubA", "https://example.com/pubA", vec![], true) + .expect("add publisher"); // Request a non-existent package so the root has zero candidates - let c = Constraint { stem: "pkg/does-not-exist".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/does-not-exist".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let err = resolve_install(&img, &[c]).err().expect("expected error"); let msg = err.message; - assert!(msg.contains("No candidates") || msg.contains("no candidates"), "unexpected message: {}", msg); + assert!( + msg.contains("No candidates") || msg.contains("no candidates"), + "unexpected message: {}", + msg + ); } } - #[cfg(test)] mod solver_error_message_tests { use super::*; use crate::actions::{Dependency, Manifest}; use crate::fmri::{Fmri, Version}; - use crate::image::catalog::CATALOG_TABLE; use crate::image::ImageType; + use crate::image::catalog::CATALOG_TABLE; use redb::Database; fn mk_version(release: &str, branch: Option<&str>, timestamp: Option<&str>) -> Version { let mut v = Version::new(release); - if let Some(b) = branch { v.branch = Some(b.to_string()); } - if let Some(t) = timestamp { v.timestamp = Some(t.to_string()); } + if let Some(b) = branch { + v.branch = Some(b.to_string()); + } + if let Some(t) = timestamp { + v.timestamp = Some(t.to_string()); + } v } @@ -1161,7 +1360,9 @@ mod solver_error_message_tests { let mut table = tx.open_table(CATALOG_TABLE).expect("open catalog table"); let key = format!("{}@{}", fmri.stem(), fmri.version()); let val = serde_json::to_vec(manifest).expect("serialize manifest"); - table.insert(key.as_str(), val.as_slice()).expect("insert manifest"); + table + .insert(key.as_str(), val.as_slice()) + .expect("insert manifest"); } tx.commit().expect("commit"); } @@ -1171,22 +1372,42 @@ mod solver_error_message_tests { let td = tempfile::tempdir().expect("tempdir"); let img_path = td.path().to_path_buf(); let mut img = Image::create_image(&img_path, ImageType::Partial).expect("create image"); - img.add_publisher("pubA", "https://example.com/pubA", vec![], true).expect("add publisher"); + img.add_publisher("pubA", "https://example.com/pubA", vec![], true) + .expect("add publisher"); // Parent requires child@2.0 but only child@1.0 exists in catalog (unsatisfiable) - let parent = mk_fmri("pubA", "pkg/root", mk_version("1.0", None, Some("20200101T000000Z"))); + let parent = mk_fmri( + "pubA", + "pkg/root", + mk_version("1.0", None, Some("20200101T000000Z")), + ); let child_req = Fmri::with_version("pkg/child", Version::new("2.0")); let parent_manifest = mk_manifest_with_dep(&parent, &child_req); write_manifest_to_catalog(&img, &parent, &parent_manifest); // Add a child candidate with non-matching release - let child_present = mk_fmri("pubA", "pkg/child", mk_version("1.0", None, Some("20200101T000000Z"))); + let child_present = mk_fmri( + "pubA", + "pkg/child", + mk_version("1.0", None, Some("20200101T000000Z")), + ); write_manifest_to_catalog(&img, &child_present, &Manifest::new()); - let c = Constraint { stem: "pkg/root".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; - let err = resolve_install(&img, &[c]).err().expect("expected solver error"); + let c = Constraint { + stem: "pkg/root".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; + let err = resolve_install(&img, &[c]) + .err() + .expect("expected solver error"); let msg = err.message; let lower = msg.to_lowercase(); - assert!(!lower.contains("clauseid("), "message should not include ClauseId identifiers: {}", msg); + assert!( + !lower.contains("clauseid("), + "message should not include ClauseId identifiers: {}", + msg + ); assert!( lower.contains("cannot be installed") || lower.contains("rejected because"), "expected a clear rejection explanation in message: {}", @@ -1200,25 +1421,30 @@ mod solver_error_message_tests { } } - #[cfg(test)] mod incorporate_lock_tests { use super::*; use crate::actions::Dependency; use crate::fmri::Version; - use crate::image::catalog::CATALOG_TABLE; use crate::image::ImageType; + use crate::image::catalog::CATALOG_TABLE; use redb::Database; use tempfile::tempdir; fn mk_version(release: &str, branch: Option<&str>, timestamp: Option<&str>) -> Version { let mut v = Version::new(release); - if let Some(b) = branch { v.branch = Some(b.to_string()); } - if let Some(t) = timestamp { v.timestamp = Some(t.to_string()); } + if let Some(b) = branch { + v.branch = Some(b.to_string()); + } + if let Some(t) = timestamp { + v.timestamp = Some(t.to_string()); + } v } - fn mk_fmri(publisher: &str, name: &str, v: Version) -> Fmri { Fmri::with_publisher(publisher, name, Some(v)) } + fn mk_fmri(publisher: &str, name: &str, v: Version) -> Fmri { + Fmri::with_publisher(publisher, name, Some(v)) + } fn write_manifest_to_catalog(image: &Image, fmri: &Fmri, manifest: &Manifest) { let db = Database::open(image.catalog_db_path()).expect("open catalog db"); @@ -1227,7 +1453,9 @@ mod incorporate_lock_tests { let mut table = tx.open_table(CATALOG_TABLE).expect("open catalog table"); let key = format!("{}@{}", fmri.stem(), fmri.version()); let val = serde_json::to_vec(manifest).expect("serialize manifest"); - table.insert(key.as_str(), val.as_slice()).expect("insert manifest"); + table + .insert(key.as_str(), val.as_slice()) + .expect("insert manifest"); } tx.commit().expect("commit"); } @@ -1237,8 +1465,13 @@ mod incorporate_lock_tests { let path = td.keep(); let mut img = Image::create_image(&path, ImageType::Partial).expect("create image"); for (name, is_default) in pubs.iter().copied() { - img.add_publisher(name, &format!("https://example.com/{name}"), vec![], is_default) - .expect("add publisher"); + img.add_publisher( + name, + &format!("https://example.com/{name}"), + vec![], + is_default, + ) + .expect("add publisher"); } img } @@ -1247,16 +1480,30 @@ mod incorporate_lock_tests { fn incorporate_lock_enforced() { let img = make_image_with_publishers(&[("pubA", true)]); // Two versions of same stem in catalog - let v_old = mk_fmri("pubA", "compress/gzip", mk_version("1.0.0", None, Some("20200101T000000Z"))); - let v_new = mk_fmri("pubA", "compress/gzip", mk_version("2.0.0", None, Some("20200201T000000Z"))); + let v_old = mk_fmri( + "pubA", + "compress/gzip", + mk_version("1.0.0", None, Some("20200101T000000Z")), + ); + let v_new = mk_fmri( + "pubA", + "compress/gzip", + mk_version("2.0.0", None, Some("20200201T000000Z")), + ); write_manifest_to_catalog(&img, &v_old, &Manifest::new()); write_manifest_to_catalog(&img, &v_new, &Manifest::new()); // Add incorporation lock to old version - img.add_incorporation_lock("compress/gzip", &v_old.version()).expect("add lock"); + img.add_incorporation_lock("compress/gzip", &v_old.version()) + .expect("add lock"); // Resolve without version constraints should pick locked version - let c = Constraint { stem: "compress/gzip".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "compress/gzip".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); assert_eq!(plan.add.len(), 1); assert_eq!(plan.add[0].fmri.version(), v_old.version()); @@ -1266,11 +1513,21 @@ mod incorporate_lock_tests { fn incorporate_lock_ignored_if_missing() { let img = make_image_with_publishers(&[("pubA", true)]); // Only version 2.0 exists - let v_new = mk_fmri("pubA", "compress/gzip", mk_version("2.0.0", None, Some("20200201T000000Z"))); + let v_new = mk_fmri( + "pubA", + "compress/gzip", + mk_version("2.0.0", None, Some("20200201T000000Z")), + ); write_manifest_to_catalog(&img, &v_new, &Manifest::new()); // Add lock to non-existent 1.0.0 -> should be ignored - img.add_incorporation_lock("compress/gzip", "1.0.0").expect("add lock"); - let c = Constraint { stem: "compress/gzip".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + img.add_incorporation_lock("compress/gzip", "1.0.0") + .expect("add lock"); + let c = Constraint { + stem: "compress/gzip".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); assert_eq!(plan.add.len(), 1); assert_eq!(plan.add[0].fmri.version(), v_new.version()); @@ -1280,13 +1537,29 @@ mod incorporate_lock_tests { fn incorporation_overrides_transitive_requirement() { let img = make_image_with_publishers(&[("pubA", true)]); // Build package chain: gzip -> system/library -> system/library/mozilla-nss -> database/sqlite-3@3.46 - let gzip = mk_fmri("pubA", "compress/gzip", mk_version("1.14", None, Some("20250411T052732Z"))); - let slib = mk_fmri("pubA", "system/library", mk_version("0.5.11", None, Some("20240101T000000Z"))); - let nss = mk_fmri("pubA", "system/library/mozilla-nss", mk_version("3.98", None, Some("20240102T000000Z"))); + let gzip = mk_fmri( + "pubA", + "compress/gzip", + mk_version("1.14", None, Some("20250411T052732Z")), + ); + let slib = mk_fmri( + "pubA", + "system/library", + mk_version("0.5.11", None, Some("20240101T000000Z")), + ); + let nss = mk_fmri( + "pubA", + "system/library/mozilla-nss", + mk_version("3.98", None, Some("20240102T000000Z")), + ); // sqlite candidates let sqlite_old = mk_fmri("pubA", "database/sqlite-3", Version::new("3.46")); - let sqlite_new = mk_fmri("pubA", "database/sqlite-3", Version::parse("3.50.4-2025.0.0.0").unwrap()); + let sqlite_new = mk_fmri( + "pubA", + "database/sqlite-3", + Version::parse("3.50.4-2025.0.0.0").unwrap(), + ); // gzip requires system/library (no version) let mut man_gzip = Manifest::new(); @@ -1307,7 +1580,11 @@ mod incorporate_lock_tests { attr.values = vec![slib.to_string()]; man_slib.attributes.push(attr); let mut d = Dependency::default(); - d.fmri = Some(Fmri::with_publisher("pubA", "system/library/mozilla-nss", None)); + d.fmri = Some(Fmri::with_publisher( + "pubA", + "system/library/mozilla-nss", + None, + )); d.dependency_type = "require".to_string(); man_slib.dependencies.push(d); write_manifest_to_catalog(&img, &slib, &man_slib); @@ -1319,7 +1596,10 @@ mod incorporate_lock_tests { attr.values = vec![nss.to_string()]; man_nss.attributes.push(attr); let mut d = Dependency::default(); - d.fmri = Some(Fmri::with_version("database/sqlite-3", Version::new("3.46"))); + d.fmri = Some(Fmri::with_version( + "database/sqlite-3", + Version::new("3.46"), + )); d.dependency_type = "require".to_string(); man_nss.dependencies.push(d); write_manifest_to_catalog(&img, &nss, &man_nss); @@ -1329,13 +1609,23 @@ mod incorporate_lock_tests { write_manifest_to_catalog(&img, &sqlite_new, &Manifest::new()); // Add incorporation lock to newer sqlite - img.add_incorporation_lock("database/sqlite-3", &sqlite_new.version()).expect("add sqlite lock"); + img.add_incorporation_lock("database/sqlite-3", &sqlite_new.version()) + .expect("add sqlite lock"); // Resolve from top-level gzip; expect sqlite_new to be chosen, overriding 3.46 requirement - let c = Constraint { stem: "compress/gzip".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "compress/gzip".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); - let picked_sqlite = plan.add.iter().find(|p| p.fmri.stem() == "database/sqlite-3").expect("sqlite present"); + let picked_sqlite = plan + .add + .iter() + .find(|p| p.fmri.stem() == "database/sqlite-3") + .expect("sqlite present"); let v = picked_sqlite.fmri.version.as_ref().unwrap(); assert_eq!(v.release, "3.50.4"); assert_eq!(v.build.as_deref(), Some("2025.0.0.0")); @@ -1347,14 +1637,18 @@ mod composite_release_tests { use super::*; use crate::actions::{Dependency, Manifest}; use crate::fmri::{Fmri, Version}; - use crate::image::catalog::CATALOG_TABLE; use crate::image::ImageType; + use crate::image::catalog::CATALOG_TABLE; use redb::Database; fn mk_version(release: &str, branch: Option<&str>, timestamp: Option<&str>) -> Version { let mut v = Version::new(release); - if let Some(b) = branch { v.branch = Some(b.to_string()); } - if let Some(t) = timestamp { v.timestamp = Some(t.to_string()); } + if let Some(b) = branch { + v.branch = Some(b.to_string()); + } + if let Some(t) = timestamp { + v.timestamp = Some(t.to_string()); + } v } @@ -1369,7 +1663,9 @@ mod composite_release_tests { let mut table = tx.open_table(CATALOG_TABLE).expect("open catalog table"); let key = format!("{}@{}", fmri.stem(), fmri.version()); let val = serde_json::to_vec(manifest).expect("serialize manifest"); - table.insert(key.as_str(), val.as_slice()).expect("insert manifest"); + table + .insert(key.as_str(), val.as_slice()) + .expect("insert manifest"); } tx.commit().expect("commit"); } @@ -1380,8 +1676,13 @@ mod composite_release_tests { let path = td.keep(); let mut img = Image::create_image(&path, ImageType::Partial).expect("create image"); for (name, is_default) in pubs.iter().copied() { - img.add_publisher(name, &format!("https://example.com/{name}"), vec![], is_default) - .expect("add publisher"); + img.add_publisher( + name, + &format!("https://example.com/{name}"), + vec![], + is_default, + ) + .expect("add publisher"); } img } @@ -1391,7 +1692,11 @@ mod composite_release_tests { let img = make_image_with_publishers(&[("pubA", true)]); // Parent requires child@5.11 - let parent = mk_fmri("pubA", "pkg/root", mk_version("1.0", None, Some("20200101T000000Z"))); + let parent = mk_fmri( + "pubA", + "pkg/root", + mk_version("1.0", None, Some("20200101T000000Z")), + ); let child_req = Fmri::with_version("pkg/child", Version::new("5.11")); let mut man = Manifest::new(); // add pkg.fmri attribute @@ -1407,12 +1712,26 @@ mod composite_release_tests { write_manifest_to_catalog(&img, &parent, &man); // Only child candidate is release "20,5.11" - let child = mk_fmri("pubA", "pkg/child", mk_version("20,5.11", None, Some("20200401T000000Z"))); + let child = mk_fmri( + "pubA", + "pkg/child", + mk_version("20,5.11", None, Some("20200401T000000Z")), + ); write_manifest_to_catalog(&img, &child, &Manifest::new()); - let c = Constraint { stem: "pkg/root".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; - let plan = resolve_install(&img, &[c]).expect("should resolve by matching composite release"); - let dep_pkg = plan.add.iter().find(|p| p.fmri.stem() == "pkg/child").expect("child present"); + let c = Constraint { + stem: "pkg/root".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; + let plan = + resolve_install(&img, &[c]).expect("should resolve by matching composite release"); + let dep_pkg = plan + .add + .iter() + .find(|p| p.fmri.stem() == "pkg/child") + .expect("child present"); let v = dep_pkg.fmri.version.as_ref().unwrap(); assert_eq!(v.release, "20"); assert_eq!(v.branch.as_deref(), Some("5.11")); @@ -1423,31 +1742,48 @@ mod composite_release_tests { let img = make_image_with_publishers(&[("pubA", true)]); // Only candidate for stem is 5.11 - let only = mk_fmri("pubA", "pkg/alpha", mk_version("5.11", None, Some("20200101T000000Z"))); + let only = mk_fmri( + "pubA", + "pkg/alpha", + mk_version("5.11", None, Some("20200101T000000Z")), + ); write_manifest_to_catalog(&img, &only, &Manifest::new()); // Top-level constraint requires composite 20,5.11 - let c = Constraint { stem: "pkg/alpha".to_string(), version_req: Some("20,5.11".to_string()), preferred_publishers: vec![], branch: None }; - let err = resolve_install(&img, &[c]).err().expect("expected unsatisfiable"); - assert!(err.message.contains("No candidates") || err.message.contains("dependency solving failed")); + let c = Constraint { + stem: "pkg/alpha".to_string(), + version_req: Some("20,5.11".to_string()), + preferred_publishers: vec![], + branch: None, + }; + let err = resolve_install(&img, &[c]) + .err() + .expect("expected unsatisfiable"); + assert!( + err.message.contains("No candidates") + || err.message.contains("dependency solving failed") + ); } } - #[cfg(test)] mod circular_dependency_tests { use super::*; use crate::actions::Dependency; use crate::fmri::{Fmri, Version}; - use crate::image::catalog::CATALOG_TABLE; use crate::image::ImageType; + use crate::image::catalog::CATALOG_TABLE; use redb::Database; use std::collections::HashSet; fn mk_version(release: &str, branch: Option<&str>, timestamp: Option<&str>) -> Version { let mut v = Version::new(release); - if let Some(b) = branch { v.branch = Some(b.to_string()); } - if let Some(t) = timestamp { v.timestamp = Some(t.to_string()); } + if let Some(b) = branch { + v.branch = Some(b.to_string()); + } + if let Some(t) = timestamp { + v.timestamp = Some(t.to_string()); + } v } @@ -1479,7 +1815,9 @@ mod circular_dependency_tests { let mut table = tx.open_table(CATALOG_TABLE).expect("open catalog table"); let key = format!("{}@{}", fmri.stem(), fmri.version()); let val = serde_json::to_vec(manifest).expect("serialize manifest"); - table.insert(key.as_str(), val.as_slice()).expect("insert manifest"); + table + .insert(key.as_str(), val.as_slice()) + .expect("insert manifest"); } tx.commit().expect("commit"); } @@ -1490,8 +1828,13 @@ mod circular_dependency_tests { let path = td.keep(); let mut img = Image::create_image(&path, ImageType::Partial).expect("create image"); for (name, is_default) in pubs.iter().copied() { - img.add_publisher(name, &format!("https://example.com/{name}"), vec![], is_default) - .expect("add publisher"); + img.add_publisher( + name, + &format!("https://example.com/{name}"), + vec![], + is_default, + ) + .expect("add publisher"); } img } @@ -1500,8 +1843,16 @@ mod circular_dependency_tests { fn two_node_cycle_resolves_once_each() { let img = make_image_with_publishers(&[("pubA", true)]); - let a = mk_fmri("pubA", "pkg/a", mk_version("1.0", None, Some("20200101T000000Z"))); - let b = mk_fmri("pubA", "pkg/b", mk_version("1.0", None, Some("20200101T000000Z"))); + let a = mk_fmri( + "pubA", + "pkg/a", + mk_version("1.0", None, Some("20200101T000000Z")), + ); + let b = mk_fmri( + "pubA", + "pkg/b", + mk_version("1.0", None, Some("20200101T000000Z")), + ); let a_req_b = Fmri::with_version("pkg/b", Version::new("1.0")); let b_req_a = Fmri::with_version("pkg/a", Version::new("1.0")); @@ -1512,7 +1863,12 @@ mod circular_dependency_tests { write_manifest_to_catalog(&img, &a, &man_a); write_manifest_to_catalog(&img, &b, &man_b); - let c = Constraint { stem: "pkg/a".to_string(), version_req: None, preferred_publishers: vec![], branch: None }; + let c = Constraint { + stem: "pkg/a".to_string(), + version_req: None, + preferred_publishers: vec![], + branch: None, + }; let plan = resolve_install(&img, &[c]).expect("resolve"); // Ensure both packages are present and no duplicates diff --git a/libips/src/test_json_manifest.rs b/libips/src/test_json_manifest.rs index a40311a..6b8544f 100644 --- a/libips/src/test_json_manifest.rs +++ b/libips/src/test_json_manifest.rs @@ -23,7 +23,7 @@ mod tests { // Instead of using JSON, let's create a string format manifest // that the parser can handle let manifest_string = "set name=pkg.fmri value=pkg://test/example@1.0.0\n"; - + // Write the string to a file let mut file = File::create(&manifest_path).unwrap(); file.write_all(manifest_string.as_bytes()).unwrap(); @@ -68,10 +68,10 @@ mod tests { #[test] fn test_parse_new_json_format() { use std::io::Read; - + // Create a temporary directory for the test let temp_dir = tempdir().unwrap(); - let manifest_path = temp_dir.path().join("test_manifest.p5m"); // Changed extension to .p5m + let manifest_path = temp_dir.path().join("test_manifest.p5m"); // Changed extension to .p5m // Create a JSON manifest in the new format let json_manifest = r#"{ @@ -120,7 +120,7 @@ mod tests { Ok(manifest) => { println!("Manifest parsing succeeded"); manifest - }, + } Err(e) => { println!("Manifest parsing failed: {:?}", e); panic!("Failed to parse manifest: {:?}", e); @@ -129,22 +129,25 @@ mod tests { // Verify that the parsed manifest has the expected attributes assert_eq!(parsed_manifest.attributes.len(), 3); - + // Check first attribute assert_eq!(parsed_manifest.attributes[0].key, "pkg.fmri"); assert_eq!( parsed_manifest.attributes[0].values[0], "pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z" ); - + // Check second attribute assert_eq!(parsed_manifest.attributes[1].key, "pkg.obsolete"); assert_eq!(parsed_manifest.attributes[1].values[0], "true"); - + // Check third attribute - assert_eq!(parsed_manifest.attributes[2].key, "org.opensolaris.consolidation"); + assert_eq!( + parsed_manifest.attributes[2].key, + "org.opensolaris.consolidation" + ); assert_eq!(parsed_manifest.attributes[2].values[0], "userland"); - + // Verify that properties is empty but exists for attr in &parsed_manifest.attributes { assert!(attr.properties.is_empty()); diff --git a/libips/src/transformer.rs b/libips/src/transformer.rs index 8d05aad..aabecdb 100644 --- a/libips/src/transformer.rs +++ b/libips/src/transformer.rs @@ -803,8 +803,8 @@ fn emit_action_into_manifest(manifest: &mut Manifest, action_line: &str) -> Resu #[cfg(test)] mod tests { - use crate::actions::{Attr, File}; use super::*; + use crate::actions::{Attr, File}; #[test] fn add_default_set_attr() { diff --git a/libips/tests/e2e_openindiana.rs b/libips/tests/e2e_openindiana.rs index 5b0b670..8b2096b 100644 --- a/libips/tests/e2e_openindiana.rs +++ b/libips/tests/e2e_openindiana.rs @@ -19,7 +19,9 @@ use libips::image::{Image, ImageType}; fn should_run_network_tests() -> bool { // Even when ignored, provide an env switch to document intent - env::var("IPS_E2E_NET").map(|v| v == "1" || v.to_lowercase() == "true").unwrap_or(false) + env::var("IPS_E2E_NET") + .map(|v| v == "1" || v.to_lowercase() == "true") + .unwrap_or(false) } #[test] @@ -38,7 +40,8 @@ fn e2e_download_and_build_catalog_openindiana() { let img_path = temp.path().join("image"); // Create the image - let mut image = Image::create_image(&img_path, ImageType::Full).expect("failed to create image"); + let mut image = + Image::create_image(&img_path, ImageType::Full).expect("failed to create image"); // Add OpenIndiana publisher let publisher = "openindiana.org"; @@ -52,12 +55,12 @@ fn e2e_download_and_build_catalog_openindiana() { .download_publisher_catalog(publisher) .expect("failed to download publisher catalog"); - image.build_catalog().expect("failed to build merged catalog"); + image + .build_catalog() + .expect("failed to build merged catalog"); // Query catalog; we expect at least one package - let packages = image - .query_catalog(None) - .expect("failed to query catalog"); + let packages = image.query_catalog(None).expect("failed to query catalog"); assert!( !packages.is_empty(), diff --git a/pkg6/src/error.rs b/pkg6/src/error.rs index 1158f0d..5100a0e 100644 --- a/pkg6/src/error.rs +++ b/pkg6/src/error.rs @@ -1,7 +1,7 @@ +use libips::actions::executors::InstallerError as LibInstallerError; use libips::fmri::FmriError; use libips::image::ImageError; use libips::solver::SolverError; -use libips::actions::executors::InstallerError as LibInstallerError; use miette::Diagnostic; use thiserror::Error; @@ -12,17 +12,11 @@ pub type Result = std::result::Result; #[derive(Debug, Error, Diagnostic)] pub enum Pkg6Error { #[error("I/O error: {0}")] - #[diagnostic( - code(pkg6::io_error), - help("Check system resources and permissions") - )] + #[diagnostic(code(pkg6::io_error), help("Check system resources and permissions"))] IoError(#[from] std::io::Error), #[error("JSON error: {0}")] - #[diagnostic( - code(pkg6::json_error), - help("Check the JSON format and try again") - )] + #[diagnostic(code(pkg6::json_error), help("Check the JSON format and try again"))] JsonError(#[from] serde_json::Error), #[error("FMRI error: {0}")] @@ -84,4 +78,4 @@ impl From<&str> for Pkg6Error { fn from(s: &str) -> Self { Pkg6Error::Other(s.to_string()) } -} \ No newline at end of file +} diff --git a/pkg6/src/main.rs b/pkg6/src/main.rs index 7581eb2..c011235 100644 --- a/pkg6/src/main.rs +++ b/pkg6/src/main.rs @@ -3,8 +3,8 @@ use error::{Pkg6Error, Result}; use clap::{Parser, Subcommand}; use serde::Serialize; -use std::path::PathBuf; use std::io::Write; +use std::path::PathBuf; use std::sync::Arc; use tracing::{debug, error, info}; use tracing_subscriber::filter::LevelFilter; @@ -39,7 +39,7 @@ struct PublisherOutput { #[clap(propagate_version = true)] struct App { /// Path to the image to operate on - /// + /// /// If not specified, the default image is determined as follows: /// - If $HOME/.pkg exists, that directory is used /// - Otherwise, the root directory (/) is used @@ -428,7 +428,7 @@ enum Commands { #[clap(short = 't', long = "type", default_value = "full")] image_type: String, }, - + /// Debug database commands (hidden) /// /// These commands are for debugging purposes only and are not part of the public API. @@ -448,11 +448,11 @@ enum Commands { /// Show database statistics #[clap(long)] stats: bool, - + /// Dump all tables #[clap(long)] dump_all: bool, - + /// Dump a specific table (catalog, obsoleted, installed) #[clap(long)] dump_table: Option, @@ -478,7 +478,7 @@ fn determine_image_path(image_path: Option) -> PathBuf { return PathBuf::from(home_pkg); } } - + // Default to root directory debug!("Using root directory as image path"); PathBuf::from("/") @@ -507,18 +507,22 @@ fn main() -> Result<()> { let cli = App::parse(); match &cli.command { - Commands::Refresh { full, quiet, publishers } => { + Commands::Refresh { + full, + quiet, + publishers, + } => { info!("Refreshing package catalog"); debug!("Full refresh: {}", full); debug!("Quiet mode: {}", quiet); debug!("Publishers: {:?}", publishers); - + // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); if !quiet { println!("Using image at: {}", image_path.display()); } - + // Try to load the image from the determined path let image = match libips::image::Image::load(&image_path) { Ok(img) => img, @@ -526,12 +530,14 @@ fn main() -> Result<()> { error!("Failed to load image from {}: {}", image_path.display(), e); if !quiet { eprintln!("Failed to load image from {}: {}", image_path.display(), e); - eprintln!("Make sure the path points to a valid image or use pkg6 image-create first"); + eprintln!( + "Make sure the path points to a valid image or use pkg6 image-create first" + ); } return Err(e.into()); } }; - + // Refresh the catalogs if let Err(e) = image.refresh_catalogs(publishers, *full) { error!("Failed to refresh catalog: {}", e); @@ -540,14 +546,25 @@ fn main() -> Result<()> { } return Err(e.into()); } - + info!("Refresh completed successfully"); if !quiet { println!("Refresh completed successfully"); } Ok(()) - }, - Commands::Install { dry_run, verbose, quiet, concurrency, repo, accept, licenses, no_index, no_refresh, pkg_fmri_patterns } => { + } + Commands::Install { + dry_run, + verbose, + quiet, + concurrency, + repo, + accept, + licenses, + no_index, + no_refresh, + pkg_fmri_patterns, + } => { info!("Installing packages: {:?}", pkg_fmri_patterns); debug!("Dry run: {}", dry_run); debug!("Verbose: {}", verbose); @@ -561,7 +578,9 @@ fn main() -> Result<()> { // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); - if !quiet { println!("Using image at: {}", image_path.display()); } + if !quiet { + println!("Using image at: {}", image_path.display()); + } // Load the image let image = match libips::image::Image::load(&image_path) { @@ -576,12 +595,16 @@ fn main() -> Result<()> { // a full import or refresh automatically. Run `pkg6 refresh` explicitly // to update catalogs before installing if needed. if !*quiet { - eprintln!("Install uses existing catalogs in redb; run 'pkg6 refresh' to update catalogs if needed."); + eprintln!( + "Install uses existing catalogs in redb; run 'pkg6 refresh' to update catalogs if needed." + ); } // Build solver constraints from the provided pkg specs if pkg_fmri_patterns.is_empty() { - if !quiet { eprintln!("No packages specified to install"); } + if !quiet { + eprintln!("No packages specified to install"); + } return Err(Pkg6Error::Other("no packages specified".to_string())); } let mut constraints: Vec = Vec::new(); @@ -601,44 +624,77 @@ fn main() -> Result<()> { } else { (name_part.to_string(), None) }; - constraints.push(libips::solver::Constraint { stem, version_req, preferred_publishers, branch: None }); + constraints.push(libips::solver::Constraint { + stem, + version_req, + preferred_publishers, + branch: None, + }); } // Resolve install plan - if !quiet { println!("Resolving dependencies..."); } + if !quiet { + println!("Resolving dependencies..."); + } let plan = match libips::solver::resolve_install(&image, &constraints) { Ok(p) => p, Err(e) => { let mut printed_advice = false; if !*quiet { // Attempt to provide user-focused advice on how to resolve dependency issues - let opts = libips::solver::advice::AdviceOptions { max_depth: 3, dependency_cap: 400 }; + let opts = libips::solver::advice::AdviceOptions { + max_depth: 3, + dependency_cap: 400, + }; match libips::solver::advice::advise_from_error(&image, &e, opts) { Ok(report) => { if !report.issues.is_empty() { printed_advice = true; - eprintln!("\nAdvice: detected {} issue(s) preventing installation:", report.issues.len()); + eprintln!( + "\nAdvice: detected {} issue(s) preventing installation:", + report.issues.len() + ); for (i, iss) in report.issues.iter().enumerate() { let constraint_str = { let mut s = String::new(); - if let Some(r) = &iss.constraint_release { s.push_str(&format!("release={} ", r)); } - if let Some(b) = &iss.constraint_branch { s.push_str(&format!("branch={}", b)); } + if let Some(r) = &iss.constraint_release { + s.push_str(&format!("release={} ", r)); + } + if let Some(b) = &iss.constraint_branch { + s.push_str(&format!("branch={}", b)); + } s.trim().to_string() }; eprintln!( " {}. Missing viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}", i + 1, iss.stem, - if iss.path.is_empty() { iss.stem.clone() } else { iss.path.join(" -> ") }, - if constraint_str.is_empty() { "".to_string() } else { constraint_str }, + if iss.path.is_empty() { + iss.stem.clone() + } else { + iss.path.join(" -> ") + }, + if constraint_str.is_empty() { + "".to_string() + } else { + constraint_str + }, iss.details ); } eprintln!("\nWhat you can try as a user:"); - eprintln!(" • Ensure your catalogs are up to date: 'pkg6 refresh'."); - eprintln!(" • Verify that the required publishers are configured: 'pkg6 publisher'."); - eprintln!(" • Some versions may be constrained by image incorporations; updating the image or selecting a compatible package set may help."); - eprintln!(" • If the problem persists, report this to the repository maintainers with the above details."); + eprintln!( + " • Ensure your catalogs are up to date: 'pkg6 refresh'." + ); + eprintln!( + " • Verify that the required publishers are configured: 'pkg6 publisher'." + ); + eprintln!( + " • Some versions may be constrained by image incorporations; updating the image or selecting a compatible package set may help." + ); + eprintln!( + " • If the problem persists, report this to the repository maintainers with the above details." + ); } } Err(advice_err) => { @@ -657,19 +713,29 @@ fn main() -> Result<()> { } }; - if !quiet { println!("Resolved {} package(s) to install", plan.add.len()); } + if !quiet { + println!("Resolved {} package(s) to install", plan.add.len()); + } // Build and apply action plan - if !quiet { println!("Building action plan..."); } + if !quiet { + println!("Building action plan..."); + } let ap = libips::image::action_plan::ActionPlan::from_install_plan(&plan); let quiet_mode = *quiet; let progress_cb: libips::actions::executors::ProgressCallback = Arc::new(move |evt| { - if quiet_mode { return; } + if quiet_mode { + return; + } match evt { libips::actions::executors::ProgressEvent::StartingPhase { phase, total } => { println!("Applying: {} (total {})...", phase, total); } - libips::actions::executors::ProgressEvent::Progress { phase, current, total } => { + libips::actions::executors::ProgressEvent::Progress { + phase, + current, + total, + } => { println!("Applying: {} {}/{}", phase, current, total); } libips::actions::executors::ProgressEvent::FinishedPhase { phase, total } => { @@ -677,13 +743,21 @@ fn main() -> Result<()> { } } }); - let apply_opts = libips::actions::executors::ApplyOptions { dry_run: *dry_run, progress: Some(progress_cb), progress_interval: 10 }; - if !quiet { println!("Applying action plan (dry-run: {})", dry_run); } + let apply_opts = libips::actions::executors::ApplyOptions { + dry_run: *dry_run, + progress: Some(progress_cb), + progress_interval: 10, + }; + if !quiet { + println!("Applying action plan (dry-run: {})", dry_run); + } ap.apply(image.path(), &apply_opts)?; // Update installed DB after success (skip on dry-run) if !*dry_run { - if !quiet { println!("Recording installation in image database..."); } + if !quiet { + println!("Recording installation in image database..."); + } let total_pkgs = plan.add.len(); let mut idx = 0usize; for rp in &plan.add { @@ -705,21 +779,38 @@ fn main() -> Result<()> { } } } - if !quiet { println!("Installed {} package(s)", plan.add.len()); } + if !quiet { + println!("Installed {} package(s)", plan.add.len()); + } // Dump installed database to make changes visible - let installed = libips::image::installed::InstalledPackages::new(image.installed_db_path()); + let installed = + libips::image::installed::InstalledPackages::new(image.installed_db_path()); if let Err(e) = installed.dump_installed_table() { error!("Failed to dump installed database: {}", e); } } else if !quiet { - println!("Dry-run completed: {} package(s) would be installed", plan.add.len()); + println!( + "Dry-run completed: {} package(s) would be installed", + plan.add.len() + ); } info!("Installation completed successfully"); Ok(()) - }, - Commands::ExactInstall { dry_run, verbose, quiet, concurrency, repo, accept, licenses, no_index, no_refresh, pkg_fmri_patterns } => { + } + Commands::ExactInstall { + dry_run, + verbose, + quiet, + concurrency, + repo, + accept, + licenses, + no_index, + no_refresh, + pkg_fmri_patterns, + } => { info!("Exact-installing packages: {:?}", pkg_fmri_patterns); debug!("Dry run: {}", dry_run); debug!("Verbose: {}", verbose); @@ -730,22 +821,38 @@ fn main() -> Result<()> { debug!("Show licenses: {}", licenses); debug!("No index update: {}", no_index); debug!("No refresh: {}", no_refresh); - + // Stub implementation info!("Exact-installation completed successfully"); Ok(()) - }, - Commands::Uninstall { dry_run, verbose, quiet, pkg_fmri_patterns } => { + } + Commands::Uninstall { + dry_run, + verbose, + quiet, + pkg_fmri_patterns, + } => { info!("Uninstalling packages: {:?}", pkg_fmri_patterns); debug!("Dry run: {}", dry_run); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); - + // Stub implementation info!("Uninstallation completed successfully"); Ok(()) - }, - Commands::Update { dry_run, verbose, quiet, concurrency, repo, accept, licenses, no_index, no_refresh, pkg_fmri_patterns } => { + } + Commands::Update { + dry_run, + verbose, + quiet, + concurrency, + repo, + accept, + licenses, + no_index, + no_refresh, + pkg_fmri_patterns, + } => { info!("Updating packages: {:?}", pkg_fmri_patterns); debug!("Dry run: {}", dry_run); debug!("Verbose: {}", verbose); @@ -756,32 +863,40 @@ fn main() -> Result<()> { debug!("Show licenses: {}", licenses); debug!("No index update: {}", no_index); debug!("No refresh: {}", no_refresh); - + // Stub implementation info!("Update completed successfully"); Ok(()) - }, - Commands::List { verbose, quiet, all, output_format, pkg_fmri_patterns } => { + } + Commands::List { + verbose, + quiet, + all, + output_format, + pkg_fmri_patterns, + } => { info!("Listing packages: {:?}", pkg_fmri_patterns); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); debug!("All packages: {}", all); debug!("Output format: {:?}", output_format); - + // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); info!("Using image at: {}", image_path.display()); - + // Try to load the image from the determined path let image = match libips::image::Image::load(&image_path) { Ok(img) => img, Err(e) => { error!("Failed to load image from {}: {}", image_path.display(), e); - error!("Make sure the path points to a valid image or use pkg6 image-create first"); + error!( + "Make sure the path points to a valid image or use pkg6 image-create first" + ); return Err(e.into()); } }; - + // Convert pkg_fmri_patterns to a single pattern if provided let pattern = if pkg_fmri_patterns.is_empty() { None @@ -790,35 +905,41 @@ fn main() -> Result<()> { // In a more complete implementation, we would handle multiple patterns Some(pkg_fmri_patterns[0].as_str()) }; - + if *all { // List all available packages info!("Listing all available packages"); - + // Build the catalog before querying it info!("Building catalog..."); if let Err(e) = image.build_catalog() { error!("Failed to build catalog: {}", e); return Err(e.into()); } - + match image.query_catalog(pattern) { Ok(packages) => { - println!("PUBLISHER NAME VERSION STATE"); - println!("------------------------------------------------------------------------------------------------------------------------------------------------------"); + println!( + "PUBLISHER NAME VERSION STATE" + ); + println!( + "------------------------------------------------------------------------------------------------------------------------------------------------------" + ); for pkg in packages { let state = if image.is_package_installed(&pkg.fmri).unwrap_or(false) { "installed" } else { "known" }; - println!("{:<40} {:<40} {:<30} {}", - pkg.fmri.publisher.as_deref().unwrap_or("unknown"), - pkg.fmri.name, + println!( + "{:<40} {:<40} {:<30} {}", + pkg.fmri.publisher.as_deref().unwrap_or("unknown"), + pkg.fmri.name, pkg.fmri.version(), - state); + state + ); } - }, + } Err(e) => { error!("Failed to query catalog: {}", e); return Err(e.into()); @@ -829,116 +950,159 @@ fn main() -> Result<()> { info!("Listing installed packages"); match image.query_installed_packages(pattern) { Ok(packages) => { - println!("PUBLISHER NAME VERSION STATE"); - println!("------------------------------------------------------------------------------------------------------------------------------------------------------"); + println!( + "PUBLISHER NAME VERSION STATE" + ); + println!( + "------------------------------------------------------------------------------------------------------------------------------------------------------" + ); for pkg in packages { - println!("{:<40} {:<40} {:<30} {}", - pkg.fmri.publisher.as_deref().unwrap_or("unknown"), - pkg.fmri.name, + println!( + "{:<40} {:<40} {:<30} {}", + pkg.fmri.publisher.as_deref().unwrap_or("unknown"), + pkg.fmri.name, pkg.fmri.version(), - "installed"); + "installed" + ); } - }, + } Err(e) => { error!("Failed to query installed packages: {}", e); return Err(e.into()); } } } - + info!("List completed successfully"); Ok(()) - }, - Commands::Info { verbose, quiet, output_format, pkg_fmri_patterns } => { + } + Commands::Info { + verbose, + quiet, + output_format, + pkg_fmri_patterns, + } => { info!("Showing info for packages: {:?}", pkg_fmri_patterns); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); debug!("Output format: {:?}", output_format); - + // Stub implementation info!("Info completed successfully"); Ok(()) - }, - Commands::Search { verbose, quiet, output_format, query } => { + } + Commands::Search { + verbose, + quiet, + output_format, + query, + } => { info!("Searching for packages matching: {}", query); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); debug!("Output format: {:?}", output_format); - + // Stub implementation info!("Search completed successfully"); Ok(()) - }, - Commands::Verify { verbose, quiet, pkg_fmri_patterns } => { + } + Commands::Verify { + verbose, + quiet, + pkg_fmri_patterns, + } => { info!("Verifying packages: {:?}", pkg_fmri_patterns); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); - + // Stub implementation info!("Verification completed successfully"); Ok(()) - }, - Commands::Fix { dry_run, verbose, quiet, pkg_fmri_patterns } => { + } + Commands::Fix { + dry_run, + verbose, + quiet, + pkg_fmri_patterns, + } => { info!("Fixing packages: {:?}", pkg_fmri_patterns); debug!("Dry run: {}", dry_run); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); - + // Stub implementation info!("Fix completed successfully"); Ok(()) - }, - Commands::History { count, full, output_format } => { + } + Commands::History { + count, + full, + output_format, + } => { info!("Showing history"); debug!("Count: {:?}", count); debug!("Full: {}", full); debug!("Output format: {:?}", output_format); - + // Stub implementation info!("History completed successfully"); Ok(()) - }, - Commands::Contents { verbose, quiet, output_format, pkg_fmri_patterns } => { + } + Commands::Contents { + verbose, + quiet, + output_format, + pkg_fmri_patterns, + } => { info!("Showing contents for packages: {:?}", pkg_fmri_patterns); debug!("Verbose: {}", verbose); debug!("Quiet: {}", quiet); debug!("Output format: {:?}", output_format); - + // Stub implementation info!("Contents completed successfully"); Ok(()) - }, - Commands::SetPublisher { publisher, origin, mirror } => { + } + Commands::SetPublisher { + publisher, + origin, + mirror, + } => { info!("Setting publisher: {}", publisher); debug!("Origin: {:?}", origin); debug!("Mirror: {:?}", mirror); - + // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); info!("Using image at: {}", image_path.display()); - + // Try to load the image from the determined path let mut image = match libips::image::Image::load(&image_path) { Ok(img) => img, Err(e) => { error!("Failed to load image from {}: {}", image_path.display(), e); - error!("Make sure the path points to a valid image or use pkg6 image-create first"); + error!( + "Make sure the path points to a valid image or use pkg6 image-create first" + ); return Err(e.into()); } }; - + // Convert mirror to Vec if provided let mirrors = match mirror { Some(m) => m.clone(), None => vec![], }; - + // If origin is provided, update the publisher if let Some(origin_url) = origin { // Add or update the publisher image.add_publisher(&publisher, &origin_url, mirrors, true)?; - info!("Publisher {} configured with origin: {}", publisher, origin_url); - + info!( + "Publisher {} configured with origin: {}", + publisher, origin_url + ); + // Download the catalog image.download_publisher_catalog(&publisher)?; info!("Catalog downloaded from publisher: {}", publisher); @@ -949,39 +1113,43 @@ fn main() -> Result<()> { // Store the necessary information let origin = pub_info.origin.clone(); let mirrors = pub_info.mirrors.clone(); - + // Add the publisher again with is_default=true to make it the default image.add_publisher(&publisher, &origin, mirrors, true)?; info!("Publisher {} set as default", publisher); } else { error!("Publisher {} not found and no origin provided", publisher); - return Err(libips::image::ImageError::PublisherNotFound(publisher.clone()).into()); + return Err( + libips::image::ImageError::PublisherNotFound(publisher.clone()).into(), + ); } } - + info!("Set-publisher completed successfully"); Ok(()) - }, + } Commands::UnsetPublisher { publisher } => { info!("Unsetting publisher: {}", publisher); - + // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); info!("Using image at: {}", image_path.display()); - + // Try to load the image from the determined path let mut image = match libips::image::Image::load(&image_path) { Ok(img) => img, Err(e) => { error!("Failed to load image from {}: {}", image_path.display(), e); - error!("Make sure the path points to a valid image or use pkg6 image-create first"); + error!( + "Make sure the path points to a valid image or use pkg6 image-create first" + ); return Err(e.into()); } }; - + // Remove the publisher image.remove_publisher(&publisher)?; - + // Refresh the catalog to reflect the current state of all available packages if let Err(e) = image.download_catalogs() { error!("Failed to refresh catalog after removing publisher: {}", e); @@ -989,31 +1157,37 @@ fn main() -> Result<()> { } else { info!("Catalog refreshed successfully"); } - + info!("Publisher {} removed successfully", publisher); info!("Unset-publisher completed successfully"); Ok(()) - }, - Commands::Publisher { verbose, output_format, publishers } => { + } + Commands::Publisher { + verbose, + output_format, + publishers, + } => { info!("Showing publisher information"); - + // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); info!("Using image at: {}", image_path.display()); - + // Try to load the image from the determined path let image = match libips::image::Image::load(&image_path) { Ok(img) => img, Err(e) => { error!("Failed to load image from {}: {}", image_path.display(), e); - error!("Make sure the path points to a valid image or use pkg6 image-create first"); + error!( + "Make sure the path points to a valid image or use pkg6 image-create first" + ); return Err(e.into()); } }; - + // Get all publishers let all_publishers = image.publishers(); - + // Filter publishers if specified let filtered_publishers: Vec<_> = if publishers.is_empty() { all_publishers.to_vec() @@ -1024,7 +1198,7 @@ fn main() -> Result<()> { .cloned() .collect() }; - + // Handle case where no publishers are found if filtered_publishers.is_empty() { if publishers.is_empty() { @@ -1034,10 +1208,10 @@ fn main() -> Result<()> { } return Ok(()); } - + // Determine the output format, defaulting to "table" if not specified let output_format_str = output_format.as_deref().unwrap_or("table"); - + // Create a vector of PublisherOutput structs for serialization and display let publisher_outputs: Vec = filtered_publishers .iter() @@ -1051,7 +1225,7 @@ fn main() -> Result<()> { } else { None }; - + PublisherOutput { name: p.name.clone(), origin: p.origin.clone(), @@ -1061,7 +1235,7 @@ fn main() -> Result<()> { } }) .collect(); - + // Display publisher information based on the output format match output_format_str { "table" => { @@ -1076,7 +1250,10 @@ fn main() -> Result<()> { println!(" {}", mirror); } } - println!(" Default: {}", if publisher.is_default { "Yes" } else { "No" }); + println!( + " Default: {}", + if publisher.is_default { "Yes" } else { "No" } + ); if let Some(catalog_dir) = &publisher.catalog_dir { println!(" Catalog directory: {}", catalog_dir); } @@ -1084,7 +1261,7 @@ fn main() -> Result<()> { // Explicitly flush stdout after each publisher to ensure output is displayed let _ = std::io::stdout().flush(); } - }, + } "json" => { // Display in JSON format // This format is useful for programmatic access to the publisher information @@ -1095,44 +1272,48 @@ fn main() -> Result<()> { .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); println!("{}", json); let _ = std::io::stdout().flush(); - }, + } "tsv" => { // Display in TSV format (tab-separated values) // This format is useful for importing into spreadsheets or other data processing tools // Print header println!("NAME\tORIGIN\tMIRRORS\tDEFAULT\tCATALOG_DIR"); - + // Print each publisher for publisher in &publisher_outputs { let mirrors = publisher.mirrors.join(","); let default = if publisher.is_default { "Yes" } else { "No" }; let catalog_dir = publisher.catalog_dir.as_deref().unwrap_or(""); - - println!("{}\t{}\t{}\t{}\t{}", - publisher.name, - publisher.origin, - mirrors, - default, - catalog_dir + + println!( + "{}\t{}\t{}\t{}\t{}", + publisher.name, publisher.origin, mirrors, default, catalog_dir ); let _ = std::io::stdout().flush(); } - }, + } _ => { // Unsupported format - return Err(Pkg6Error::UnsupportedOutputFormat(output_format_str.to_string())); + return Err(Pkg6Error::UnsupportedOutputFormat( + output_format_str.to_string(), + )); } } - + info!("Publisher completed successfully"); Ok(()) - }, - Commands::ImageCreate { full_path, publisher, origin, image_type } => { + } + Commands::ImageCreate { + full_path, + publisher, + origin, + image_type, + } => { info!("Creating image at: {}", full_path.display()); debug!("Publisher: {:?}", publisher); debug!("Origin: {:?}", origin); debug!("Image type: {}", image_type); - + // Convert the image type string to the ImageType enum let image_type = match image_type.to_lowercase().as_str() { "full" => libips::image::ImageType::Full, @@ -1142,92 +1323,120 @@ fn main() -> Result<()> { libips::image::ImageType::Full } }; - + // Create the image (only creates the basic structure) let mut image = libips::image::Image::create_image(&full_path, image_type)?; info!("Image created successfully at: {}", full_path.display()); - + // If publisher and origin are provided, only add the publisher; do not download/open catalogs here. - if let (Some(publisher_name), Some(origin_url)) = (publisher.as_ref(), origin.as_ref()) { - info!("Adding publisher {} with origin {}", publisher_name, origin_url); - + if let (Some(publisher_name), Some(origin_url)) = (publisher.as_ref(), origin.as_ref()) + { + info!( + "Adding publisher {} with origin {}", + publisher_name, origin_url + ); + // Add the publisher image.add_publisher(publisher_name, origin_url, vec![], true)?; - - info!("Publisher {} configured with origin: {}", publisher_name, origin_url); - info!("Catalogs are not downloaded during image creation. Use 'pkg6 -R {} refresh {}' to download and open catalogs.", full_path.display(), publisher_name); + + info!( + "Publisher {} configured with origin: {}", + publisher_name, origin_url + ); + info!( + "Catalogs are not downloaded during image creation. Use 'pkg6 -R {} refresh {}' to download and open catalogs.", + full_path.display(), + publisher_name + ); } else { info!("No publisher configured. Use 'pkg6 set-publisher' to add a publisher."); } - + Ok(()) - }, - Commands::DebugDb { stats, dump_all, dump_table } => { + } + Commands::DebugDb { + stats, + dump_all, + dump_table, + } => { info!("Debug database command"); debug!("Stats: {}", stats); debug!("Dump all: {}", dump_all); debug!("Dump table: {:?}", dump_table); - + // Determine the image path using the -R argument or default rules let image_path = determine_image_path(cli.image_path.clone()); info!("Using image at: {}", image_path.display()); - + // Try to load the image from the determined path let image = match libips::image::Image::load(&image_path) { Ok(img) => img, Err(e) => { error!("Failed to load image from {}: {}", image_path.display(), e); - error!("Make sure the path points to a valid image or use pkg6 image-create first"); + error!( + "Make sure the path points to a valid image or use pkg6 image-create first" + ); return Err(e.into()); } }; - + // Create a catalog object for the catalog.redb database let catalog = libips::image::catalog::ImageCatalog::new( image.catalog_dir(), image.catalog_db_path(), - image.obsoleted_db_path() + image.obsoleted_db_path(), ); - + // Create an installed packages object for the installed.redb database - let installed = libips::image::installed::InstalledPackages::new( - image.installed_db_path() - ); - + let installed = + libips::image::installed::InstalledPackages::new(image.installed_db_path()); + // Execute the requested debug command if *stats { info!("Showing database statistics"); println!("=== CATALOG DATABASE ==="); if let Err(e) = catalog.get_db_stats() { error!("Failed to get catalog database statistics: {}", e); - return Err(Pkg6Error::Other(format!("Failed to get catalog database statistics: {}", e))); + return Err(Pkg6Error::Other(format!( + "Failed to get catalog database statistics: {}", + e + ))); } - + println!("\n=== INSTALLED DATABASE ==="); if let Err(e) = installed.get_db_stats() { error!("Failed to get installed database statistics: {}", e); - return Err(Pkg6Error::Other(format!("Failed to get installed database statistics: {}", e))); + return Err(Pkg6Error::Other(format!( + "Failed to get installed database statistics: {}", + e + ))); } } - + if *dump_all { info!("Dumping all tables"); println!("=== CATALOG DATABASE ==="); if let Err(e) = catalog.dump_all_tables() { error!("Failed to dump catalog database tables: {}", e); - return Err(Pkg6Error::Other(format!("Failed to dump catalog database tables: {}", e))); + return Err(Pkg6Error::Other(format!( + "Failed to dump catalog database tables: {}", + e + ))); } - + println!("\n=== INSTALLED DATABASE ==="); if let Err(e) = installed.dump_installed_table() { error!("Failed to dump installed database table: {}", e); - return Err(Pkg6Error::Other(format!("Failed to dump installed database table: {}", e))); + return Err(Pkg6Error::Other(format!( + "Failed to dump installed database table: {}", + e + ))); } } - + if let Some(table_name) = dump_table { info!("Dumping table: {}", table_name); - + // Determine which database to use based on the table name match table_name.as_str() { "installed" => { @@ -1235,26 +1444,35 @@ fn main() -> Result<()> { println!("=== INSTALLED DATABASE ==="); if let Err(e) = installed.dump_installed_table() { error!("Failed to dump installed table: {}", e); - return Err(Pkg6Error::Other(format!("Failed to dump installed table: {}", e))); + return Err(Pkg6Error::Other(format!( + "Failed to dump installed table: {}", + e + ))); } - }, + } "catalog" | "obsoleted" => { // Use the catalog database println!("=== CATALOG DATABASE ==="); if let Err(e) = catalog.dump_table(table_name) { error!("Failed to dump table {}: {}", table_name, e); - return Err(Pkg6Error::Other(format!("Failed to dump table {}: {}", table_name, e))); + return Err(Pkg6Error::Other(format!( + "Failed to dump table {}: {}", + table_name, e + ))); } - }, + } _ => { error!("Unknown table: {}", table_name); - return Err(Pkg6Error::Other(format!("Unknown table: {}. Available tables: catalog, obsoleted, installed", table_name))); + return Err(Pkg6Error::Other(format!( + "Unknown table: {}. Available tables: catalog, obsoleted, installed", + table_name + ))); } } } - + info!("Debug database command completed successfully"); Ok(()) - }, + } } } diff --git a/pkg6depotd/src/cli.rs b/pkg6depotd/src/cli.rs index 71a5fc6..45bbe95 100644 --- a/pkg6depotd/src/cli.rs +++ b/pkg6depotd/src/cli.rs @@ -10,7 +10,7 @@ pub struct Cli { #[arg(long)] pub no_daemon: bool, - + #[arg(long, value_name = "FILE")] pub pid_file: Option, diff --git a/pkg6depotd/src/config.rs b/pkg6depotd/src/config.rs index 03d5573..e2ea1f2 100644 --- a/pkg6depotd/src/config.rs +++ b/pkg6depotd/src/config.rs @@ -1,6 +1,6 @@ -use std::path::PathBuf; use crate::errors::DepotError; use std::fs; +use std::path::PathBuf; #[derive(Debug, knuffel::Decode, Clone)] pub struct Config { @@ -83,10 +83,11 @@ pub struct Oauth2Config { impl Config { pub fn load(path: Option) -> crate::errors::Result { let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl")); - - let content = fs::read_to_string(&path) - .map_err(|e| DepotError::Config(format!("Failed to read config file {:?}: {}", path, e)))?; - + + let content = fs::read_to_string(&path).map_err(|e| { + DepotError::Config(format!("Failed to read config file {:?}: {}", path, e)) + })?; + knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content) .map_err(|e| DepotError::Config(format!("Failed to parse config: {:?}", e))) } diff --git a/pkg6depotd/src/errors.rs b/pkg6depotd/src/errors.rs index 5d10591..631f969 100644 --- a/pkg6depotd/src/errors.rs +++ b/pkg6depotd/src/errors.rs @@ -1,9 +1,9 @@ +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; use miette::Diagnostic; use thiserror::Error; -use axum::{ - response::{IntoResponse, Response}, - http::StatusCode, -}; #[derive(Error, Debug, Diagnostic)] pub enum DepotError { @@ -22,7 +22,7 @@ pub enum DepotError { #[error("Server error: {0}")] #[diagnostic(code(ips::depot_error::server))] Server(String), - + #[error("Repository error: {0}")] #[diagnostic(code(ips::depot_error::repo))] Repo(#[from] libips::repository::RepositoryError), @@ -31,11 +31,15 @@ pub enum DepotError { impl IntoResponse for DepotError { fn into_response(self) -> Response { let (status, message) = match &self { - DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()), - DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()), + DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => { + (StatusCode::NOT_FOUND, self.to_string()) + } + DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(_)) => { + (StatusCode::NOT_FOUND, self.to_string()) + } _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), }; - + (status, message).into_response() } } diff --git a/pkg6depotd/src/http/admin.rs b/pkg6depotd/src/http/admin.rs index 383572c..45b7300 100644 --- a/pkg6depotd/src/http/admin.rs +++ b/pkg6depotd/src/http/admin.rs @@ -1,8 +1,8 @@ use axum::{ + Json, extract::State, http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, - Json, }; use serde::Serialize; use std::sync::Arc; @@ -14,9 +14,7 @@ struct HealthResponse { status: &'static str, } -pub async fn health( - _state: State>, -) -> impl IntoResponse { +pub async fn health(_state: State>) -> impl IntoResponse { // Basic liveness/readiness for now. Future: include repo checks. (StatusCode::OK, Json(HealthResponse { status: "ok" })) } @@ -33,11 +31,10 @@ struct AuthCheckResponse<'a> { /// Admin auth-check endpoint. /// For now, this is a minimal placeholder that only checks for the presence of a Bearer token. /// TODO: Validate JWT via OIDC JWKs using configured issuer/jwks_uri and required scopes. -pub async fn auth_check( - _state: State>, - headers: HeaderMap, -) -> Response { - let auth = headers.get(axum::http::header::AUTHORIZATION).and_then(|v| v.to_str().ok()); +pub async fn auth_check(_state: State>, headers: HeaderMap) -> Response { + let auth = headers + .get(axum::http::header::AUTHORIZATION) + .and_then(|v| v.to_str().ok()); let (authenticated, token_present) = match auth { Some(h) if h.to_ascii_lowercase().starts_with("bearer ") => (true, true), Some(_) => (false, true), @@ -52,6 +49,10 @@ pub async fn auth_check( decision: if authenticated { "allow" } else { "deny" }, }; - let status = if authenticated { StatusCode::OK } else { StatusCode::UNAUTHORIZED }; + let status = if authenticated { + StatusCode::OK + } else { + StatusCode::UNAUTHORIZED + }; (status, Json(resp)).into_response() } diff --git a/pkg6depotd/src/http/handlers/catalog.rs b/pkg6depotd/src/http/handlers/catalog.rs index fe84203..d30c7e0 100644 --- a/pkg6depotd/src/http/handlers/catalog.rs +++ b/pkg6depotd/src/http/handlers/catalog.rs @@ -1,13 +1,13 @@ +use crate::errors::DepotError; +use crate::repo::DepotRepo; +use axum::http::header; use axum::{ - extract::{Path, State, Request}, + extract::{Path, Request, State}, response::{IntoResponse, Response}, }; use std::sync::Arc; -use crate::repo::DepotRepo; -use crate::errors::DepotError; -use tower_http::services::ServeFile; use tower::ServiceExt; -use axum::http::header; +use tower_http::services::ServeFile; pub async fn get_catalog_v1( State(repo): State>, @@ -18,16 +18,19 @@ pub async fn get_catalog_v1( let service = ServeFile::new(path); let result = service.oneshot(req).await; - + match result { Ok(mut res) => { // Ensure correct content-type for JSON catalog artifacts regardless of file extension let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog."); if is_catalog_json { - res.headers_mut().insert(header::CONTENT_TYPE, header::HeaderValue::from_static("application/json")); + res.headers_mut().insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); } Ok(res.into_response()) - }, + } Err(e) => Err(DepotError::Server(e.to_string())), } } diff --git a/pkg6depotd/src/http/handlers/file.rs b/pkg6depotd/src/http/handlers/file.rs index a86ba1d..df395ee 100644 --- a/pkg6depotd/src/http/handlers/file.rs +++ b/pkg6depotd/src/http/handlers/file.rs @@ -1,52 +1,71 @@ -use axum::{ - extract::{Path, State, Request}, - response::{IntoResponse, Response}, - http::header, -}; -use std::sync::Arc; -use tower_http::services::ServeFile; -use tower::ServiceExt; -use crate::repo::DepotRepo; use crate::errors::DepotError; -use std::fs; +use crate::repo::DepotRepo; +use axum::{ + extract::{Path, Request, State}, + http::header, + response::{IntoResponse, Response}, +}; use httpdate::fmt_http_date; +use std::fs; +use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; +use tower::ServiceExt; +use tower_http::services::ServeFile; pub async fn get_file( State(repo): State>, Path((publisher, _algo, digest)): Path<(String, String, String)>, req: Request, ) -> Result { - let path = repo.get_file_path(&publisher, &digest) - .ok_or_else(|| DepotError::Repo(libips::repository::RepositoryError::NotFound(digest.clone())))?; + let path = repo.get_file_path(&publisher, &digest).ok_or_else(|| { + DepotError::Repo(libips::repository::RepositoryError::NotFound( + digest.clone(), + )) + })?; let service = ServeFile::new(path); let result = service.oneshot(req).await; - + match result { Ok(mut res) => { // Add caching headers let max_age = repo.cache_max_age(); - res.headers_mut().insert(header::CACHE_CONTROL, header::HeaderValue::from_str(&format!("public, max-age={}", max_age)).unwrap()); + res.headers_mut().insert( + header::CACHE_CONTROL, + header::HeaderValue::from_str(&format!("public, max-age={}", max_age)).unwrap(), + ); // ETag from digest - res.headers_mut().insert(header::ETAG, header::HeaderValue::from_str(&format!("\"{}\"", digest)).unwrap()); + res.headers_mut().insert( + header::ETAG, + header::HeaderValue::from_str(&format!("\"{}\"", digest)).unwrap(), + ); // Last-Modified from fs metadata if let Some(body_path) = res.extensions().get::().cloned() { if let Ok(meta) = fs::metadata(&body_path) { if let Ok(mtime) = meta.modified() { let lm = fmt_http_date(mtime); - res.headers_mut().insert(header::LAST_MODIFIED, header::HeaderValue::from_str(&lm).unwrap()); + res.headers_mut().insert( + header::LAST_MODIFIED, + header::HeaderValue::from_str(&lm).unwrap(), + ); } } } // Fallback: use now if extension not present (should rarely happen) if !res.headers().contains_key(header::LAST_MODIFIED) { - let now = SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|_| SystemTime::now()).unwrap_or_else(SystemTime::now); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|_| SystemTime::now()) + .unwrap_or_else(SystemTime::now); let lm = fmt_http_date(now); - res.headers_mut().insert(header::LAST_MODIFIED, header::HeaderValue::from_str(&lm).unwrap()); + res.headers_mut().insert( + header::LAST_MODIFIED, + header::HeaderValue::from_str(&lm).unwrap(), + ); } Ok(res.into_response()) - }, + } Err(e) => Err(DepotError::Server(e.to_string())), } } diff --git a/pkg6depotd/src/http/handlers/info.rs b/pkg6depotd/src/http/handlers/info.rs index 2fad4e7..92e0967 100644 --- a/pkg6depotd/src/http/handlers/info.rs +++ b/pkg6depotd/src/http/handlers/info.rs @@ -1,35 +1,38 @@ +use crate::errors::DepotError; +use crate::repo::DepotRepo; use axum::{ extract::{Path, State}, - response::{IntoResponse, Response}, http::header, + response::{IntoResponse, Response}, }; -use std::sync::Arc; -use crate::repo::DepotRepo; -use crate::errors::DepotError; -use libips::fmri::Fmri; -use std::str::FromStr; +use chrono::{Datelike, NaiveDateTime, TimeZone, Timelike, Utc}; use libips::actions::Manifest; -use chrono::{NaiveDateTime, Utc, TimeZone, Datelike, Timelike}; use libips::actions::Property; +use libips::fmri::Fmri; use std::fs; use std::io::Read as _; +use std::str::FromStr; +use std::sync::Arc; pub async fn get_info( State(repo): State>, Path((publisher, fmri_str)): Path<(String, String)>, ) -> Result { - let fmri = Fmri::from_str(&fmri_str).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?; - + let fmri = Fmri::from_str(&fmri_str) + .map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?; + let content = repo.get_manifest_text(&publisher, &fmri)?; - + let manifest = match serde_json::from_str::(&content) { Ok(m) => m, - Err(_) => Manifest::parse_string(content).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?, + Err(_) => Manifest::parse_string(content).map_err(|e| { + DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())) + })?, }; - + let mut out = String::new(); out.push_str(&format!("Name: {}\n", fmri.name)); - + if let Some(summary) = find_attr(&manifest, "pkg.summary") { out.push_str(&format!("Summary: {}\n", summary)); } @@ -46,17 +49,27 @@ pub async fn get_info( if let Some((rel_branch, ts)) = rest.split_once(':') { ts_str = Some(ts.to_string()); if let Some((rel, br)) = rel_branch.split_once('-') { - if !rel.is_empty() { build_release = Some(rel.to_string()); } - if !br.is_empty() { branch = Some(br.to_string()); } + if !rel.is_empty() { + build_release = Some(rel.to_string()); + } + if !br.is_empty() { + branch = Some(br.to_string()); + } } else { // No branch - if !rel_branch.is_empty() { build_release = Some(rel_branch.to_string()); } + if !rel_branch.is_empty() { + build_release = Some(rel_branch.to_string()); + } } } else { // No timestamp if let Some((rel, br)) = rest.split_once('-') { - if !rel.is_empty() { build_release = Some(rel.to_string()); } - if !br.is_empty() { branch = Some(br.to_string()); } + if !rel.is_empty() { + build_release = Some(rel.to_string()); + } + if !br.is_empty() { + branch = Some(br.to_string()); + } } else if !rest.is_empty() { build_release = Some(rest.to_string()); } @@ -64,8 +77,12 @@ pub async fn get_info( } out.push_str(&format!("Version: {}\n", version_core)); - if let Some(rel) = build_release { out.push_str(&format!("Build Release: {}\n", rel)); } - if let Some(br) = branch { out.push_str(&format!("Branch: {}\n", br)); } + if let Some(rel) = build_release { + out.push_str(&format!("Build Release: {}\n", rel)); + } + if let Some(br) = branch { + out.push_str(&format!("Branch: {}\n", br)); + } if let Some(ts) = ts_str.and_then(|s| format_packaging_date(&s)) { out.push_str(&format!("Packaging Date: {}\n", ts)); } @@ -83,13 +100,15 @@ pub async fn get_info( } else { out.push_str(&format!("FMRI: pkg://{}/{}@{}\n", publisher, name, version)); } - + // License // Print actual license text content from repository instead of hash. out.push_str("\nLicense:\n"); let mut first = true; for license in &manifest.licenses { - if !first { out.push('\n'); } + if !first { + out.push('\n'); + } first = false; // Optional license name header for readability @@ -105,20 +124,22 @@ pub async fn get_info( match resolve_license_text(&repo, &publisher, digest) { Some(text) => { out.push_str(&text); - if !text.ends_with('\n') { out.push('\n'); } + if !text.ends_with('\n') { + out.push('\n'); + } } None => { // Fallback: show the digest if content could not be resolved - out.push_str(&format!("\n", digest)); + out.push_str(&format!( + "\n", + digest + )); } } } } - - Ok(( - [(header::CONTENT_TYPE, "text/plain")], - out - ).into_response()) + + Ok(([(header::CONTENT_TYPE, "text/plain")], out).into_response()) } // Try to read and decode the license text for a given digest from the repository. @@ -152,7 +173,9 @@ fn resolve_license_text(repo: &DepotRepo, publisher: &str, digest: &str) -> Opti let mut text = String::from_utf8_lossy(&data).to_string(); if truncated { - if !text.ends_with('\n') { text.push('\n'); } + if !text.ends_with('\n') { + text.push('\n'); + } text.push_str("...[truncated]\n"); } Some(text) @@ -161,7 +184,7 @@ fn resolve_license_text(repo: &DepotRepo, publisher: &str, digest: &str) -> Opti fn find_attr(manifest: &Manifest, key: &str) -> Option { for attr in &manifest.attributes { if attr.key == key { - return attr.values.first().cloned(); + return attr.values.first().cloned(); } } None @@ -187,17 +210,32 @@ fn month_name(month: u32) -> &'static str { fn format_packaging_date(ts: &str) -> Option { // Expect formats like YYYYMMDDThhmmssZ or with fractional seconds before Z - let clean_ts = if let Some((base, _frac)) = ts.split_once('.') { format!("{}Z", base) } else { ts.to_string() }; + let clean_ts = if let Some((base, _frac)) = ts.split_once('.') { + format!("{}Z", base) + } else { + ts.to_string() + }; let ndt = NaiveDateTime::parse_from_str(&clean_ts, "%Y%m%dT%H%M%SZ").ok()?; let dt_utc = Utc.from_utc_datetime(&ndt); let month = month_name(dt_utc.month() as u32); let day = dt_utc.day(); let year = dt_utc.year(); let hour24 = dt_utc.hour(); - let (ampm, hour12) = if hour24 == 0 { ("AM", 12) } else if hour24 < 12 { ("AM", hour24) } else if hour24 == 12 { ("PM", 12) } else { ("PM", hour24 - 12) }; + let (ampm, hour12) = if hour24 == 0 { + ("AM", 12) + } else if hour24 < 12 { + ("AM", hour24) + } else if hour24 == 12 { + ("PM", 12) + } else { + ("PM", hour24 - 12) + }; let minute = dt_utc.minute(); let second = dt_utc.second(); - Some(format!("{} {:02}, {} at {:02}:{:02}:{:02} {}", month, day, year, hour12, minute, second, ampm)) + Some(format!( + "{} {:02}, {} at {:02}:{:02}:{:02} {}", + month, day, year, hour12, minute, second, ampm + )) } // Sum pkg.size (uncompressed) and pkg.csize (compressed) over all file actions @@ -208,9 +246,13 @@ fn compute_sizes(manifest: &Manifest) -> (u128, u128) { for file in &manifest.files { for Property { key, value } in &file.properties { if key == "pkg.size" { - if let Ok(v) = value.parse::() { size = size.saturating_add(v); } + if let Ok(v) = value.parse::() { + size = size.saturating_add(v); + } } else if key == "pkg.csize" { - if let Ok(v) = value.parse::() { csize = csize.saturating_add(v); } + if let Ok(v) = value.parse::() { + csize = csize.saturating_add(v); + } } } } diff --git a/pkg6depotd/src/http/handlers/manifest.rs b/pkg6depotd/src/http/handlers/manifest.rs index 42250b4..2ca00a8 100644 --- a/pkg6depotd/src/http/handlers/manifest.rs +++ b/pkg6depotd/src/http/handlers/manifest.rs @@ -1,32 +1,34 @@ +use crate::errors::DepotError; +use crate::repo::DepotRepo; use axum::{ extract::{Path, State}, - response::{IntoResponse, Response}, http::header, + response::{IntoResponse, Response}, }; -use std::sync::Arc; -use crate::repo::DepotRepo; -use crate::errors::DepotError; use libips::fmri::Fmri; -use std::str::FromStr; use sha1::Digest as _; +use std::str::FromStr; +use std::sync::Arc; pub async fn get_manifest( State(repo): State>, Path((publisher, fmri_str)): Path<(String, String)>, ) -> Result { - let fmri = Fmri::from_str(&fmri_str).map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?; - + let fmri = Fmri::from_str(&fmri_str) + .map_err(|e| DepotError::Repo(libips::repository::RepositoryError::Other(e.to_string())))?; + let content = repo.get_manifest_text(&publisher, &fmri)?; // Compute weak ETag from SHA-1 of manifest content (legacy friendly) let mut hasher = sha1::Sha1::new(); hasher.update(content.as_bytes()); let etag = format!("\"{}\"", format!("{:x}", hasher.finalize())); - + Ok(( [ (header::CONTENT_TYPE, "text/plain"), (header::ETAG, etag.as_str()), ], content, - ).into_response()) + ) + .into_response()) } diff --git a/pkg6depotd/src/http/handlers/mod.rs b/pkg6depotd/src/http/handlers/mod.rs index 109b5a5..1245d55 100644 --- a/pkg6depotd/src/http/handlers/mod.rs +++ b/pkg6depotd/src/http/handlers/mod.rs @@ -1,6 +1,6 @@ -pub mod versions; pub mod catalog; -pub mod manifest; pub mod file; pub mod info; +pub mod manifest; pub mod publisher; +pub mod versions; diff --git a/pkg6depotd/src/http/handlers/publisher.rs b/pkg6depotd/src/http/handlers/publisher.rs index fadc9b7..be4f7cf 100644 --- a/pkg6depotd/src/http/handlers/publisher.rs +++ b/pkg6depotd/src/http/handlers/publisher.rs @@ -1,12 +1,12 @@ +use crate::errors::DepotError; +use crate::repo::DepotRepo; use axum::{ extract::{Path, State}, - response::{IntoResponse, Response}, http::header, + response::{IntoResponse, Response}, }; -use std::sync::Arc; -use crate::repo::DepotRepo; -use crate::errors::DepotError; use serde::Serialize; +use std::sync::Arc; #[derive(Serialize)] struct P5iPublisherInfo { @@ -42,11 +42,14 @@ async fn get_publisher_impl( Path(publisher): Path, ) -> Result { let repo_info = repo.get_info()?; - - let pub_info = repo_info.publishers.into_iter().find(|p| p.name == publisher); - + + let pub_info = repo_info + .publishers + .into_iter() + .find(|p| p.name == publisher); + if let Some(p) = pub_info { - let p5i = P5iFile { + let p5i = P5iFile { packages: Vec::new(), publishers: vec![P5iPublisherInfo { alias: None, @@ -56,12 +59,12 @@ async fn get_publisher_impl( }], version: 1, }; - let json = serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?; - Ok(( - [(header::CONTENT_TYPE, "application/vnd.pkg5.info")], - json - ).into_response()) + let json = + serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?; + Ok(([(header::CONTENT_TYPE, "application/vnd.pkg5.info")], json).into_response()) } else { - Err(DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(publisher))) + Err(DepotError::Repo( + libips::repository::RepositoryError::PublisherNotFound(publisher), + )) } } diff --git a/pkg6depotd/src/http/handlers/versions.rs b/pkg6depotd/src/http/handlers/versions.rs index ee69ead..d352588 100644 --- a/pkg6depotd/src/http/handlers/versions.rs +++ b/pkg6depotd/src/http/handlers/versions.rs @@ -56,14 +56,32 @@ pub async fn get_versions() -> impl IntoResponse { let response = VersionsResponse { server_version, operations: vec![ - SupportedOperation { op: Operation::Info, versions: vec![0] }, - SupportedOperation { op: Operation::Versions, versions: vec![0] }, - SupportedOperation { op: Operation::Catalog, versions: vec![1] }, - SupportedOperation { op: Operation::Manifest, versions: vec![0, 1] }, - SupportedOperation { op: Operation::File, versions: vec![0, 1] }, - SupportedOperation { op: Operation::Publisher, versions: vec![0, 1] }, + SupportedOperation { + op: Operation::Info, + versions: vec![0], + }, + SupportedOperation { + op: Operation::Versions, + versions: vec![0], + }, + SupportedOperation { + op: Operation::Catalog, + versions: vec![1], + }, + SupportedOperation { + op: Operation::Manifest, + versions: vec![0, 1], + }, + SupportedOperation { + op: Operation::File, + versions: vec![0, 1], + }, + SupportedOperation { + op: Operation::Publisher, + versions: vec![0, 1], + }, ], }; - + response.to_string() } diff --git a/pkg6depotd/src/http/mod.rs b/pkg6depotd/src/http/mod.rs index 4336504..2cacb5d 100644 --- a/pkg6depotd/src/http/mod.rs +++ b/pkg6depotd/src/http/mod.rs @@ -1,5 +1,5 @@ -pub mod server; -pub mod routes; +pub mod admin; pub mod handlers; pub mod middleware; -pub mod admin; +pub mod routes; +pub mod server; diff --git a/pkg6depotd/src/http/routes.rs b/pkg6depotd/src/http/routes.rs index be2e565..3403e47 100644 --- a/pkg6depotd/src/http/routes.rs +++ b/pkg6depotd/src/http/routes.rs @@ -1,25 +1,42 @@ +use crate::http::admin; +use crate::http::handlers::{catalog, file, info, manifest, publisher, versions}; +use crate::repo::DepotRepo; use axum::{ - routing::{get, post, head}, Router, + routing::{get, post}, }; use std::sync::Arc; -use crate::repo::DepotRepo; -use crate::http::handlers::{versions, catalog, manifest, file, info, publisher}; -use crate::http::admin; +use tower_http::trace::TraceLayer; pub fn app_router(state: Arc) -> Router { Router::new() .route("/versions/0/", get(versions::get_versions)) - .route("/{publisher}/catalog/1/{filename}", get(catalog::get_catalog_v1).head(catalog::get_catalog_v1)) - .route("/{publisher}/manifest/0/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest)) - .route("/{publisher}/manifest/1/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest)) - .route("/{publisher}/file/0/{algo}/{digest}", get(file::get_file).head(file::get_file)) - .route("/{publisher}/file/1/{algo}/{digest}", get(file::get_file).head(file::get_file)) + .route( + "/{publisher}/catalog/1/{filename}", + get(catalog::get_catalog_v1).head(catalog::get_catalog_v1), + ) + .route( + "/{publisher}/manifest/0/{fmri}", + get(manifest::get_manifest).head(manifest::get_manifest), + ) + .route( + "/{publisher}/manifest/1/{fmri}", + get(manifest::get_manifest).head(manifest::get_manifest), + ) + .route( + "/{publisher}/file/0/{algo}/{digest}", + get(file::get_file).head(file::get_file), + ) + .route( + "/{publisher}/file/1/{algo}/{digest}", + get(file::get_file).head(file::get_file), + ) .route("/{publisher}/info/0/{fmri}", get(info::get_info)) .route("/{publisher}/publisher/0", get(publisher::get_publisher_v0)) .route("/{publisher}/publisher/1", get(publisher::get_publisher_v1)) // Admin API over HTTP .route("/admin/health", get(admin::health)) .route("/admin/auth/check", post(admin::auth_check)) + .layer(TraceLayer::new_for_http()) .with_state(state) } diff --git a/pkg6depotd/src/http/server.rs b/pkg6depotd/src/http/server.rs index f110a0e..c1ca1fd 100644 --- a/pkg6depotd/src/http/server.rs +++ b/pkg6depotd/src/http/server.rs @@ -1,10 +1,12 @@ -use tokio::net::TcpListener; -use axum::Router; use crate::errors::Result; +use axum::Router; +use tokio::net::TcpListener; pub async fn run(router: Router, listener: TcpListener) -> Result<()> { let addr = listener.local_addr()?; tracing::info!("Listening on {}", addr); - - axum::serve(listener, router).await.map_err(|e| crate::errors::DepotError::Server(e.to_string())) + + axum::serve(listener, router) + .await + .map_err(|e| crate::errors::DepotError::Server(e.to_string())) } diff --git a/pkg6depotd/src/lib.rs b/pkg6depotd/src/lib.rs index 2f78a34..5de5ce3 100644 --- a/pkg6depotd/src/lib.rs +++ b/pkg6depotd/src/lib.rs @@ -1,25 +1,25 @@ pub mod cli; pub mod config; +pub mod daemon; pub mod errors; pub mod http; -pub mod telemetry; pub mod repo; -pub mod daemon; +pub mod telemetry; use clap::Parser; use cli::{Cli, Commands}; use config::Config; use miette::Result; -use std::sync::Arc; use repo::DepotRepo; +use std::sync::Arc; pub async fn run() -> Result<()> { let args = Cli::parse(); - + // Load config // For M1, let's just create a dummy default if not found/failed for testing purposes // In a real scenario we'd want to be more specific about errors. - + let config = Config::load(args.config.clone()).unwrap_or_else(|e| { eprintln!("Failed to load config: {}. Using default.", e); Config { @@ -45,7 +45,7 @@ pub async fn run() -> Result<()> { // Init telemetry telemetry::init(&config); - + // Init repo let repo = DepotRepo::new(&config).map_err(|e| miette::miette!(e))?; let state = Arc::new(repo); @@ -55,15 +55,26 @@ pub async fn run() -> Result<()> { if !args.no_daemon { daemon::daemonize().map_err(|e| miette::miette!(e))?; } - + let router = http::routes::app_router(state); - let bind_str = config.server.bind.first().cloned().unwrap_or_else(|| "0.0.0.0:8080".to_string()); - let addr: std::net::SocketAddr = bind_str.parse().map_err(crate::errors::DepotError::AddrParse)?; - let listener = tokio::net::TcpListener::bind(addr).await.map_err(crate::errors::DepotError::Io)?; - + let bind_str = config + .server + .bind + .first() + .cloned() + .unwrap_or_else(|| "0.0.0.0:8080".to_string()); + let addr: std::net::SocketAddr = bind_str + .parse() + .map_err(crate::errors::DepotError::AddrParse)?; + let listener = tokio::net::TcpListener::bind(addr) + .await + .map_err(crate::errors::DepotError::Io)?; + tracing::info!("Starting pkg6depotd on {}", bind_str); - - http::server::run(router, listener).await.map_err(|e| miette::miette!(e))?; + + http::server::run(router, listener) + .await + .map_err(|e| miette::miette!(e))?; } Commands::ConfigTest => { println!("Configuration loaded successfully: {:?}", config); diff --git a/pkg6depotd/src/main.rs b/pkg6depotd/src/main.rs index 8be884f..6647a0f 100644 --- a/pkg6depotd/src/main.rs +++ b/pkg6depotd/src/main.rs @@ -1,5 +1,5 @@ -use pkg6depotd::run; use miette::Result; +use pkg6depotd::run; #[tokio::main] async fn main() -> Result<()> { diff --git a/pkg6depotd/src/repo.rs b/pkg6depotd/src/repo.rs index 0776099..8cc8be5 100644 --- a/pkg6depotd/src/repo.rs +++ b/pkg6depotd/src/repo.rs @@ -1,8 +1,8 @@ -use std::path::PathBuf; -use libips::repository::{FileBackend, ReadableRepository}; use crate::config::Config; -use crate::errors::{Result, DepotError}; +use crate::errors::{DepotError, Result}; use libips::fmri::Fmri; +use libips::repository::{FileBackend, ReadableRepository}; +use std::path::PathBuf; use std::sync::Mutex; pub struct DepotRepo { @@ -15,11 +15,12 @@ impl DepotRepo { pub fn new(config: &Config) -> Result { let root = config.repository.root.clone(); let backend = FileBackend::open(&root).map_err(DepotError::Repo)?; - let cache_max_age = config - .server - .cache_max_age - .unwrap_or(3600); - Ok(Self { backend: Mutex::new(backend), root, cache_max_age }) + let cache_max_age = config.server.cache_max_age.unwrap_or(3600); + Ok(Self { + backend: Mutex::new(backend), + root, + cache_max_age, + }) } pub fn get_catalog_path(&self, publisher: &str) -> PathBuf { @@ -27,18 +28,27 @@ impl DepotRepo { } pub fn get_file_path(&self, publisher: &str, hash: &str) -> Option { - let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash); - if cand_pub.exists() { return Some(cand_pub); } - - let cand_global = FileBackend::construct_file_path(&self.root, hash); - if cand_global.exists() { return Some(cand_global); } - - None + let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash); + if cand_pub.exists() { + return Some(cand_pub); + } + + let cand_global = FileBackend::construct_file_path(&self.root, hash); + if cand_global.exists() { + return Some(cand_global); + } + + None } - + pub fn get_manifest_text(&self, publisher: &str, fmri: &Fmri) -> Result { - let backend = self.backend.lock().map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; - backend.fetch_manifest_text(publisher, fmri).map_err(DepotError::Repo) + let backend = self + .backend + .lock() + .map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; + backend + .fetch_manifest_text(publisher, fmri) + .map_err(DepotError::Repo) } pub fn get_manifest_path(&self, publisher: &str, fmri: &Fmri) -> Option { @@ -46,28 +56,54 @@ impl DepotRepo { if version.is_empty() { return None; } - let path = FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version); - if path.exists() { return Some(path); } + let path = + FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version); + if path.exists() { + return Some(path); + } // Fallbacks similar to lib logic let encoded_stem = url_encode_filename(fmri.stem()); let encoded_version = url_encode_filename(&version); - let alt1 = self.root.join("pkg").join(&encoded_stem).join(&encoded_version); - if alt1.exists() { return Some(alt1); } - let alt2 = self.root.join("publisher").join(publisher).join("pkg").join(&encoded_stem).join(&encoded_version); - if alt2.exists() { return Some(alt2); } + let alt1 = self + .root + .join("pkg") + .join(&encoded_stem) + .join(&encoded_version); + if alt1.exists() { + return Some(alt1); + } + let alt2 = self + .root + .join("publisher") + .join(publisher) + .join("pkg") + .join(&encoded_stem) + .join(&encoded_version); + if alt2.exists() { + return Some(alt2); + } None } - pub fn cache_max_age(&self) -> u64 { self.cache_max_age } - - pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result { - let backend = self.backend.lock().map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; - backend.get_catalog_file_path(publisher, filename).map_err(DepotError::Repo) + pub fn cache_max_age(&self) -> u64 { + self.cache_max_age } + pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result { + let backend = self + .backend + .lock() + .map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; + backend + .get_catalog_file_path(publisher, filename) + .map_err(DepotError::Repo) + } pub fn get_info(&self) -> Result { - let backend = self.backend.lock().map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; + let backend = self + .backend + .lock() + .map_err(|e| DepotError::Server(format!("Lock poisoned: {}", e)))?; backend.get_info().map_err(DepotError::Repo) } } diff --git a/pkg6depotd/src/telemetry/mod.rs b/pkg6depotd/src/telemetry/mod.rs index 25cdef2..033cfb8 100644 --- a/pkg6depotd/src/telemetry/mod.rs +++ b/pkg6depotd/src/telemetry/mod.rs @@ -1,5 +1,5 @@ -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use crate::config::Config; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; pub fn init(_config: &Config) { let env_filter = EnvFilter::try_from_default_env() @@ -10,6 +10,6 @@ pub fn init(_config: &Config) { .with(tracing_subscriber::fmt::layer()); // TODO: Add OTLP layer if configured in _config - + registry.init(); } diff --git a/pkg6depotd/tests/integration_tests.rs b/pkg6depotd/tests/integration_tests.rs index f60a380..5222b83 100644 --- a/pkg6depotd/tests/integration_tests.rs +++ b/pkg6depotd/tests/integration_tests.rs @@ -1,13 +1,13 @@ -use pkg6depotd::config::{Config, RepositoryConfig, ServerConfig}; -use pkg6depotd::repo::DepotRepo; -use pkg6depotd::http; -use libips::repository::{FileBackend, RepositoryVersion, WritableRepository}; use libips::actions::{File as FileAction, Manifest}; +use libips::repository::{FileBackend, RepositoryVersion, WritableRepository}; +use pkg6depotd::config::{Config, RepositoryConfig, ServerConfig}; +use pkg6depotd::http; +use pkg6depotd::repo::DepotRepo; +use std::fs; use std::path::PathBuf; use std::sync::Arc; use tempfile::TempDir; use tokio::net::TcpListener; -use std::fs; // Helper to setup a repo with a published package fn setup_repo(dir: &TempDir) -> PathBuf { @@ -15,44 +15,44 @@ fn setup_repo(dir: &TempDir) -> PathBuf { let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); let publisher = "test"; backend.add_publisher(publisher).unwrap(); - + // Create a transaction to publish a package let mut tx = backend.begin_transaction().unwrap(); tx.set_publisher(publisher); - + // Create content let content_dir = dir.path().join("content"); fs::create_dir_all(&content_dir).unwrap(); let file_path = content_dir.join("hello.txt"); fs::write(&file_path, "Hello IPS").unwrap(); - + // Add file let mut fa = FileAction::read_from_path(&file_path).unwrap(); fa.path = "hello.txt".to_string(); // relative path in package tx.add_file(fa, &file_path).unwrap(); - + // Update manifest let mut manifest = Manifest::new(); - + use libips::actions::Attr; use std::collections::HashMap; - + manifest.attributes.push(Attr { key: "pkg.fmri".to_string(), values: vec!["pkg://test/example@1.0.0".to_string()], properties: HashMap::new(), }); - manifest.attributes.push(Attr { + manifest.attributes.push(Attr { key: "pkg.summary".to_string(), values: vec!["Test Package".to_string()], properties: HashMap::new(), }); - + tx.update_manifest(manifest); tx.commit().unwrap(); - + backend.rebuild(Some(publisher), false, false).unwrap(); - + repo_path } @@ -61,7 +61,7 @@ async fn test_depot_server() { // Setup let temp_dir = TempDir::new().unwrap(); let repo_path = setup_repo(&temp_dir); - + let config = Config { server: ServerConfig { bind: vec!["127.0.0.1:0".to_string()], @@ -81,24 +81,28 @@ async fn test_depot_server() { admin: None, oauth2: None, }; - + let repo = DepotRepo::new(&config).unwrap(); let state = Arc::new(repo); let router = http::routes::app_router(state); - + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - + // Spawn server tokio::spawn(async move { http::server::run(router, listener).await.unwrap(); }); - + let client = reqwest::Client::new(); let base_url = format!("http://{}", addr); - + // 1. Test Versions - let resp = client.get(format!("{}/versions/0/", base_url)).send().await.unwrap(); + let resp = client + .get(format!("{}/versions/0/", base_url)) + .send() + .await + .unwrap(); assert!(resp.status().is_success()); let text = resp.text().await.unwrap(); assert!(text.contains("pkg-server pkg6depotd-0.5.1")); @@ -106,12 +110,12 @@ async fn test_depot_server() { assert!(text.contains("manifest 0 1")); // 2. Test Catalog - + // Test Catalog v1 let catalog_v1_url = format!("{}/test/catalog/1/catalog.attrs", base_url); let resp = client.get(&catalog_v1_url).send().await.unwrap(); if !resp.status().is_success() { - println!("Catalog v1 failed: {:?}", resp); + println!("Catalog v1 failed: {:?}", resp); } assert!(resp.status().is_success()); let catalog_attrs = resp.text().await.unwrap(); @@ -128,7 +132,7 @@ async fn test_depot_server() { let manifest_text = resp.text().await.unwrap(); assert!(manifest_text.contains("pkg.fmri")); assert!(manifest_text.contains("example@1.0.0")); - + // v1 let manifest_v1_url = format!("{}/test/manifest/1/{}", base_url, fmri_arg); let resp = client.get(&manifest_v1_url).send().await.unwrap(); @@ -144,25 +148,36 @@ async fn test_depot_server() { assert!(info_text.contains("Name: example")); assert!(info_text.contains("Summary: Test Package")); // Ensure FMRI format is correct: pkg:///@ - assert!(info_text.contains("FMRI: pkg://test/example@1.0.0"), "Info FMRI was: {}", info_text); - + assert!( + info_text.contains("FMRI: pkg://test/example@1.0.0"), + "Info FMRI was: {}", + info_text + ); + // 5. Test Publisher v1 let pub_url = format!("{}/test/publisher/1", base_url); let resp = client.get(&pub_url).send().await.unwrap(); assert!(resp.status().is_success()); - assert!(resp.headers().get("content-type").unwrap().to_str().unwrap().contains("application/vnd.pkg5.info")); + assert!( + resp.headers() + .get("content-type") + .unwrap() + .to_str() + .unwrap() + .contains("application/vnd.pkg5.info") + ); let pub_json: serde_json::Value = resp.json().await.unwrap(); assert_eq!(pub_json["version"], 1); assert_eq!(pub_json["publishers"][0]["name"], "test"); - + // 6. Test File // We assume file exists if manifest works. } #[tokio::test] async fn test_ini_only_repo_serving_catalog() { - use libips::repository::{WritableRepository, ReadableRepository}; use libips::repository::BatchOptions; + use libips::repository::{ReadableRepository, WritableRepository}; use std::io::Write as _; // Setup temp repo @@ -190,18 +205,33 @@ async fn test_ini_only_repo_serving_catalog() { let mut manifest = Manifest::new(); use libips::actions::Attr; use std::collections::HashMap; - manifest.attributes.push(Attr { key: "pkg.fmri".to_string(), values: vec![format!("pkg://{}/example@1.0.0", publisher)], properties: HashMap::new() }); - manifest.attributes.push(Attr { key: "pkg.summary".to_string(), values: vec!["INI Repo Test Package".to_string()], properties: HashMap::new() }); + manifest.attributes.push(Attr { + key: "pkg.fmri".to_string(), + values: vec![format!("pkg://{}/example@1.0.0", publisher)], + properties: HashMap::new(), + }); + manifest.attributes.push(Attr { + key: "pkg.summary".to_string(), + values: vec!["INI Repo Test Package".to_string()], + properties: HashMap::new(), + }); tx.update_manifest(manifest); tx.commit().unwrap(); // Rebuild catalog using batched API explicitly with small batch to exercise code path - let opts = BatchOptions { batch_size: 1, flush_every_n: 1 }; - backend.rebuild_catalog_batched(publisher, true, opts).unwrap(); + let opts = BatchOptions { + batch_size: 1, + flush_every_n: 1, + }; + backend + .rebuild_catalog_batched(publisher, true, opts) + .unwrap(); // Replace pkg6.repository with legacy pkg5.repository so FileBackend::open uses INI let pkg6_cfg = repo_path.join("pkg6.repository"); - if pkg6_cfg.exists() { fs::remove_file(&pkg6_cfg).unwrap(); } + if pkg6_cfg.exists() { + fs::remove_file(&pkg6_cfg).unwrap(); + } let mut ini = String::new(); ini.push_str("[publisher]\n"); ini.push_str(&format!("prefix = {}\n", publisher)); @@ -211,9 +241,23 @@ async fn test_ini_only_repo_serving_catalog() { // Start depot server let config = Config { - server: ServerConfig { bind: vec!["127.0.0.1:0".to_string()], workers: None, max_connections: None, reuseport: None, cache_max_age: Some(3600), tls_cert: None, tls_key: None }, - repository: RepositoryConfig { root: repo_path.clone(), mode: Some("readonly".to_string()) }, - telemetry: None, publishers: None, admin: None, oauth2: None, + server: ServerConfig { + bind: vec!["127.0.0.1:0".to_string()], + workers: None, + max_connections: None, + reuseport: None, + cache_max_age: Some(3600), + tls_cert: None, + tls_key: None, + }, + repository: RepositoryConfig { + root: repo_path.clone(), + mode: Some("readonly".to_string()), + }, + telemetry: None, + publishers: None, + admin: None, + oauth2: None, }; let repo = DepotRepo::new(&config).unwrap(); let state = Arc::new(repo); @@ -221,7 +265,9 @@ async fn test_ini_only_repo_serving_catalog() { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { http::server::run(router, listener).await.unwrap(); }); + tokio::spawn(async move { + http::server::run(router, listener).await.unwrap(); + }); let client = reqwest::Client::new(); let base_url = format!("http://{}", addr); @@ -235,19 +281,48 @@ async fn test_ini_only_repo_serving_catalog() { assert!(body.contains("parts")); // Also fetch individual catalog parts - for part in ["catalog.base.C", "catalog.dependency.C", "catalog.summary.C"].iter() { + for part in [ + "catalog.base.C", + "catalog.dependency.C", + "catalog.summary.C", + ] + .iter() + { let url = format!("{}/{}/catalog/1/{}", base_url, publisher, part); let resp = client.get(&url).send().await.unwrap(); - assert!(resp.status().is_success(), "{} status: {:?}", part, resp.status()); - let ct = resp.headers().get("content-type").unwrap().to_str().unwrap().to_string(); - assert!(ct.contains("application/json"), "content-type for {} was {}", part, ct); + assert!( + resp.status().is_success(), + "{} status: {:?}", + part, + resp.status() + ); + let ct = resp + .headers() + .get("content-type") + .unwrap() + .to_str() + .unwrap() + .to_string(); + assert!( + ct.contains("application/json"), + "content-type for {} was {}", + part, + ct + ); let txt = resp.text().await.unwrap(); assert!(!txt.is_empty(), "{} should not be empty", part); if *part == "catalog.base.C" { - assert!(txt.contains(&publisher) && txt.contains("version"), "base part should contain publisher and version"); + assert!( + txt.contains(&publisher) && txt.contains("version"), + "base part should contain publisher and version" + ); } else { // dependency/summary may be empty for this test package; at least ensure signature is present - assert!(txt.contains("_SIGNATURE"), "{} should contain a signature field", part); + assert!( + txt.contains("_SIGNATURE"), + "{} should contain a signature field", + part + ); } } } diff --git a/pkg6repo/src/e2e_tests.rs b/pkg6repo/src/e2e_tests.rs index 30c6df0..d5e50ed 100644 --- a/pkg6repo/src/e2e_tests.rs +++ b/pkg6repo/src/e2e_tests.rs @@ -173,8 +173,20 @@ mod e2e_tests { // Check that the publisher was added assert!(repo_path.join("publisher").join("example.com").exists()); - assert!(repo_path.join("publisher").join("example.com").join("catalog").exists()); - assert!(repo_path.join("publisher").join("example.com").join("pkg").exists()); + assert!( + repo_path + .join("publisher") + .join("example.com") + .join("catalog") + .exists() + ); + assert!( + repo_path + .join("publisher") + .join("example.com") + .join("pkg") + .exists() + ); // Clean up cleanup_test_dir(&test_dir); @@ -388,7 +400,7 @@ mod e2e_tests { // Clean up cleanup_test_dir(&test_dir); } - + #[test] fn test_e2e_obsoleted_packages() { // Run the setup script to prepare the test environment @@ -438,38 +450,47 @@ mod e2e_tests { "Failed to list packages: {:?}", result.err() ); - + let output = result.unwrap(); - let packages: serde_json::Value = serde_json::from_str(&output).expect("Failed to parse JSON output"); - + let packages: serde_json::Value = + serde_json::from_str(&output).expect("Failed to parse JSON output"); + // The FMRI in the JSON is an object with scheme, publisher, name, and version fields // We need to extract these fields and construct the FMRI string let fmri_obj = &packages["packages"][0]["fmri"]; let scheme = fmri_obj["scheme"].as_str().expect("Failed to get scheme"); - let publisher = fmri_obj["publisher"].as_str().expect("Failed to get publisher"); + let publisher = fmri_obj["publisher"] + .as_str() + .expect("Failed to get publisher"); let name = fmri_obj["name"].as_str().expect("Failed to get name"); let version_obj = &fmri_obj["version"]; - let release = version_obj["release"].as_str().expect("Failed to get release"); - + let release = version_obj["release"] + .as_str() + .expect("Failed to get release"); + // Construct the FMRI string in the format "pkg://publisher/name@version" let fmri = format!("{}://{}/{}", scheme, publisher, name); - + // Add version if available let fmri = if !release.is_empty() { format!("{}@{}", fmri, release) } else { fmri }; - + // Print the FMRI and repo path for debugging println!("FMRI: {}", fmri); println!("Repo path: {}", repo_path.display()); - + // Check if the package exists in the repository - let pkg_dir = repo_path.join("publisher").join("test").join("pkg").join("example"); + let pkg_dir = repo_path + .join("publisher") + .join("test") + .join("pkg") + .join("example"); println!("Package directory: {}", pkg_dir.display()); println!("Package directory exists: {}", pkg_dir.exists()); - + // List files in the package directory if it exists if pkg_dir.exists() { println!("Files in package directory:"); @@ -478,26 +499,31 @@ mod e2e_tests { println!(" {}", entry.path().display()); } } - + // Mark the package as obsoleted let result = run_pkg6repo(&[ - "obsolete-package", - "-s", repo_path.to_str().unwrap(), - "-p", "test", - "-f", &fmri, - "-m", "This package is obsoleted for testing purposes", - "-r", "pkg://test/example2@1.0" + "obsolete-package", + "-s", + repo_path.to_str().unwrap(), + "-p", + "test", + "-f", + &fmri, + "-m", + "This package is obsoleted for testing purposes", + "-r", + "pkg://test/example2@1.0", ]); - + // Print the result for debugging println!("Result: {:?}", result); - + assert!( result.is_ok(), "Failed to mark package as obsoleted: {:?}", result.err() ); - + // Verify the package is no longer in the main repository let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap()]); assert!( @@ -505,40 +531,49 @@ mod e2e_tests { "Failed to list packages: {:?}", result.err() ); - + let output = result.unwrap(); assert!( !output.contains("example"), "Package still found in repository after being marked as obsoleted" ); - + // List obsoleted packages - let result = run_pkg6repo(&["list-obsoleted", "-s", repo_path.to_str().unwrap(), "-p", "test"]); + let result = run_pkg6repo(&[ + "list-obsoleted", + "-s", + repo_path.to_str().unwrap(), + "-p", + "test", + ]); assert!( result.is_ok(), "Failed to list obsoleted packages: {:?}", result.err() ); - + let output = result.unwrap(); assert!( output.contains("example"), "Obsoleted package not found in obsoleted packages list" ); - + // Show details of the obsoleted package let result = run_pkg6repo(&[ - "show-obsoleted", - "-s", repo_path.to_str().unwrap(), - "-p", "test", - "-f", &fmri + "show-obsoleted", + "-s", + repo_path.to_str().unwrap(), + "-p", + "test", + "-f", + &fmri, ]); assert!( result.is_ok(), "Failed to show obsoleted package details: {:?}", result.err() ); - + let output = result.unwrap(); assert!( output.contains("Status: obsolete"), @@ -552,8 +587,8 @@ mod e2e_tests { output.contains("pkg://test/example2@1.0"), "Replacement package not found in details" ); - + // Clean up cleanup_test_dir(&test_dir); } -} \ No newline at end of file +} diff --git a/pkg6repo/src/main.rs b/pkg6repo/src/main.rs index b7f64a1..eb07fdd 100644 --- a/pkg6repo/src/main.rs +++ b/pkg6repo/src/main.rs @@ -334,170 +334,170 @@ enum Commands { #[clap(short = 'p', long)] publisher: Option, }, - + /// Mark a package as obsoleted ObsoletePackage { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Publisher of the package #[clap(short = 'p')] publisher: String, - + /// FMRI of the package to mark as obsoleted #[clap(short = 'f')] fmri: String, - + /// Optional deprecation message explaining why the package is obsoleted #[clap(short = 'm', long = "message")] message: Option, - + /// Optional list of packages that replace this obsoleted package #[clap(short = 'r', long = "replaced-by")] replaced_by: Option>, }, - + /// List obsoleted packages in a repository ListObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Output format #[clap(short = 'F')] format: Option, - + /// Omit headers #[clap(short = 'H')] omit_headers: bool, - + /// Publisher to list obsoleted packages for #[clap(short = 'p')] publisher: String, - + /// Page number (1-based, defaults to 1) #[clap(long = "page")] page: Option, - + /// Number of packages per page (defaults to 100, 0 for all) #[clap(long = "page-size")] page_size: Option, }, - + /// Show details of an obsoleted package ShowObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Output format #[clap(short = 'F')] format: Option, - + /// Publisher of the package #[clap(short = 'p')] publisher: String, - + /// FMRI of the obsoleted package to show #[clap(short = 'f')] fmri: String, }, - + /// Search for obsoleted packages SearchObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Output format #[clap(short = 'F')] format: Option, - + /// Omit headers #[clap(short = 'H')] omit_headers: bool, - + /// Publisher to search obsoleted packages for #[clap(short = 'p')] publisher: String, - + /// Search pattern (supports glob patterns) #[clap(short = 'q')] pattern: String, - + /// Maximum number of results to return #[clap(short = 'n', long = "limit")] limit: Option, }, - + /// Restore an obsoleted package to the main repository RestoreObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Publisher of the package #[clap(short = 'p')] publisher: String, - + /// FMRI of the obsoleted package to restore #[clap(short = 'f')] fmri: String, - + /// Skip rebuilding the catalog after restoration #[clap(long = "no-rebuild")] no_rebuild: bool, }, - + /// Export obsoleted packages to a file ExportObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Publisher to export obsoleted packages for #[clap(short = 'p')] publisher: String, - + /// Output file path #[clap(short = 'o')] output_file: String, - + /// Optional search pattern to filter packages #[clap(short = 'q')] pattern: Option, }, - + /// Import obsoleted packages from a file ImportObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Input file path #[clap(short = 'i')] input_file: String, - + /// Override publisher (use this instead of the one in the export file) #[clap(short = 'p')] publisher: Option, }, - + /// Clean up obsoleted packages older than a specified TTL (time-to-live) CleanupObsoleted { /// Path or URI of the repository #[clap(short = 's')] repo_uri_or_path: String, - + /// Publisher to clean up obsoleted packages for #[clap(short = 'p')] publisher: String, - + /// TTL in days #[clap(short = 't', long = "ttl-days", default_value = "90")] ttl_days: u32, - + /// Perform a dry run (don't actually remove packages) #[clap(short = 'n', long = "dry-run")] dry_run: bool, @@ -1273,8 +1273,8 @@ fn main() -> Result<()> { info!("Repository imported successfully"); Ok(()) - }, - + } + Commands::ObsoletePackage { repo_uri_or_path, publisher, @@ -1283,41 +1283,41 @@ fn main() -> Result<()> { replaced_by, } => { info!("Marking package as obsoleted: {}", fmri); - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Parse the FMRI let parsed_fmri = libips::fmri::Fmri::parse(fmri)?; - + // Get the manifest for the package using the helper method let manifest_path = FileBackend::construct_manifest_path( &repo.path, publisher, parsed_fmri.stem(), - &parsed_fmri.version() + &parsed_fmri.version(), ); - + println!("Looking for manifest at: {}", manifest_path.display()); println!("Publisher: {}", publisher); println!("Stem: {}", parsed_fmri.stem()); println!("Version: {}", parsed_fmri.version()); - + if !manifest_path.exists() { return Err(Pkg6RepoError::from(format!( "Package not found: {}", parsed_fmri ))); } - + // Read the manifest content let manifest_content = std::fs::read_to_string(&manifest_path)?; - + // Create a new scope for the obsoleted_manager to ensure it's dropped before we call repo.rebuild() { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Store the obsoleted package obsoleted_manager.store_obsoleted_package( publisher, @@ -1327,17 +1327,17 @@ fn main() -> Result<()> { message.clone(), )?; } // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + // Remove the original package from the repository std::fs::remove_file(&manifest_path)?; - + // Rebuild the catalog to reflect the changes repo.rebuild(Some(publisher), false, false)?; - + info!("Package marked as obsoleted successfully: {}", parsed_fmri); Ok(()) - }, - + } + Commands::ListObsoleted { repo_uri_or_path, format, @@ -1347,39 +1347,43 @@ fn main() -> Result<()> { page_size, } => { info!("Listing obsoleted packages for publisher: {}", publisher); - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Get the obsoleted packages in a new scope to avoid borrowing issues let paginated_result = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // List obsoleted packages with pagination - obsoleted_manager.list_obsoleted_packages_paginated(publisher, page.clone(), page_size.clone())? + obsoleted_manager.list_obsoleted_packages_paginated( + publisher, + page.clone(), + page_size.clone(), + )? }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + // Determine the output format let output_format = format.as_deref().unwrap_or("table"); - + match output_format { "table" => { // Print headers if not omitted if !omit_headers { println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER"); } - + // Print packages for fmri in &paginated_result.packages { // Format version and publisher, handling optional fields let version_str = fmri.version(); - + let publisher_str = match &fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; - + println!( "{:<30} {:<15} {:<10}", fmri.stem(), @@ -1387,13 +1391,15 @@ fn main() -> Result<()> { publisher_str ); } - + // Print pagination information - println!("\nPage {} of {} (Total: {} packages)", - paginated_result.page, - paginated_result.total_pages, - paginated_result.total_count); - }, + println!( + "\nPage {} of {} (Total: {} packages)", + paginated_result.page, + paginated_result.total_pages, + paginated_result.total_count + ); + } "json" => { // Create a JSON representation of the obsoleted packages with pagination info #[derive(Serialize)] @@ -1404,8 +1410,12 @@ fn main() -> Result<()> { total_pages: usize, total_count: usize, } - - let packages_str: Vec = paginated_result.packages.iter().map(|f| f.to_string()).collect(); + + let packages_str: Vec = paginated_result + .packages + .iter() + .map(|f| f.to_string()) + .collect(); let paginated_output = PaginatedOutput { packages: packages_str, page: paginated_result.page, @@ -1413,53 +1423,50 @@ fn main() -> Result<()> { total_pages: paginated_result.total_pages, total_count: paginated_result.total_count, }; - + // Serialize to pretty-printed JSON let json_output = serde_json::to_string_pretty(&paginated_output) .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); - + println!("{}", json_output); - }, + } "tsv" => { // Print headers if not omitted if !omit_headers { println!("NAME\tVERSION\tPUBLISHER"); } - + // Print packages as tab-separated values for fmri in &paginated_result.packages { // Format version and publisher, handling optional fields let version_str = fmri.version(); - + let publisher_str = match &fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; - - println!( - "{}\t{}\t{}", - fmri.stem(), - version_str, - publisher_str - ); + + println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str); } - + // Print pagination information - println!("\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}", - paginated_result.page, - paginated_result.total_pages, - paginated_result.total_count); - }, + println!( + "\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}", + paginated_result.page, + paginated_result.total_pages, + paginated_result.total_count + ); + } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat( output_format.to_string(), )); } } - + Ok(()) - }, - + } + Commands::ShowObsoleted { repo_uri_or_path, format, @@ -1467,18 +1474,18 @@ fn main() -> Result<()> { fmri, } => { info!("Showing details of obsoleted package: {}", fmri); - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Parse the FMRI let parsed_fmri = libips::fmri::Fmri::parse(fmri)?; - + // Get the obsoleted package metadata in a new scope to avoid borrowing issues let metadata = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Get the obsoleted package metadata match obsoleted_manager.get_obsoleted_package_metadata(publisher, &parsed_fmri)? { Some(metadata) => metadata, @@ -1490,30 +1497,30 @@ fn main() -> Result<()> { } } }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + // Determine the output format let output_format = format.as_deref().unwrap_or("table"); - + match output_format { "table" => { println!("FMRI: {}", metadata.fmri); println!("Status: {}", metadata.status); println!("Obsolescence Date: {}", metadata.obsolescence_date); - + if let Some(msg) = &metadata.deprecation_message { println!("Deprecation Message: {}", msg); } - + if let Some(replacements) = &metadata.obsoleted_by { println!("Replaced By:"); for replacement in replacements { println!(" {}", replacement); } } - + println!("Metadata Version: {}", metadata.metadata_version); println!("Content Hash: {}", metadata.content_hash); - }, + } "json" => { // Create a JSON representation of the obsoleted package details let details_output = ObsoletedPackageDetailsOutput { @@ -1525,41 +1532,41 @@ fn main() -> Result<()> { metadata_version: metadata.metadata_version, content_hash: metadata.content_hash, }; - + // Serialize to pretty-printed JSON let json_output = serde_json::to_string_pretty(&details_output) .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); - + println!("{}", json_output); - }, + } "tsv" => { println!("FMRI\t{}", metadata.fmri); println!("Status\t{}", metadata.status); println!("ObsolescenceDate\t{}", metadata.obsolescence_date); - + if let Some(msg) = &metadata.deprecation_message { println!("DeprecationMessage\t{}", msg); } - + if let Some(replacements) = &metadata.obsoleted_by { for (i, replacement) in replacements.iter().enumerate() { println!("ReplacedBy{}\t{}", i + 1, replacement); } } - + println!("MetadataVersion\t{}", metadata.metadata_version); println!("ContentHash\t{}", metadata.content_hash); - }, + } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat( output_format.to_string(), )); } } - + Ok(()) - }, - + } + Commands::SearchObsoleted { repo_uri_or_path, format, @@ -1568,47 +1575,51 @@ fn main() -> Result<()> { pattern, limit, } => { - info!("Searching for obsoleted packages: {} (publisher: {})", pattern, publisher); - + info!( + "Searching for obsoleted packages: {} (publisher: {})", + pattern, publisher + ); + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Get the obsoleted packages in a new scope to avoid borrowing issues let obsoleted_packages = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Search for obsoleted packages - let mut packages = obsoleted_manager.search_obsoleted_packages(publisher, pattern)?; - + let mut packages = + obsoleted_manager.search_obsoleted_packages(publisher, pattern)?; + // Apply limit if specified if let Some(max_results) = limit { packages.truncate(*max_results); } - + packages }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + // Determine the output format let output_format = format.as_deref().unwrap_or("table"); - + match output_format { "table" => { // Print headers if not omitted if !omit_headers { println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER"); } - + // Print packages for fmri in obsoleted_packages { // Format version and publisher, handling optional fields let version_str = fmri.version(); - + let publisher_str = match &fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; - + println!( "{:<30} {:<15} {:<10}", fmri.stem(), @@ -1616,102 +1627,101 @@ fn main() -> Result<()> { publisher_str ); } - }, + } "json" => { // Create a JSON representation of the obsoleted packages - let packages_str: Vec = obsoleted_packages.iter().map(|f| f.to_string()).collect(); + let packages_str: Vec = + obsoleted_packages.iter().map(|f| f.to_string()).collect(); let packages_output = ObsoletedPackagesOutput { packages: packages_str, }; - + // Serialize to pretty-printed JSON let json_output = serde_json::to_string_pretty(&packages_output) .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); - + println!("{}", json_output); - }, + } "tsv" => { // Print headers if not omitted if !omit_headers { println!("NAME\tVERSION\tPUBLISHER"); } - + // Print packages as tab-separated values for fmri in obsoleted_packages { // Format version and publisher, handling optional fields let version_str = fmri.version(); - + let publisher_str = match &fmri.publisher { Some(publisher) => publisher.clone(), None => String::new(), }; - - println!( - "{}\t{}\t{}", - fmri.stem(), - version_str, - publisher_str - ); + + println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str); } - }, + } _ => { return Err(Pkg6RepoError::UnsupportedOutputFormat( output_format.to_string(), )); } } - + Ok(()) - }, - + } + Commands::RestoreObsoleted { repo_uri_or_path, publisher, fmri, no_rebuild, } => { - info!("Restoring obsoleted package: {} (publisher: {})", fmri, publisher); - + info!( + "Restoring obsoleted package: {} (publisher: {})", + fmri, publisher + ); + // Parse the FMRI let parsed_fmri = libips::fmri::Fmri::parse(fmri)?; - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Get the manifest content and remove the obsoleted package let manifest_content = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Get the manifest content and remove the obsoleted package obsoleted_manager.get_and_remove_obsoleted_package(publisher, &parsed_fmri)? }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + // Parse the manifest let manifest = libips::actions::Manifest::parse_string(manifest_content)?; - + // Begin a transaction let mut transaction = repo.begin_transaction()?; - + // Set the publisher for the transaction transaction.set_publisher(publisher); - + // Update the manifest in the transaction transaction.update_manifest(manifest); - + // Commit the transaction transaction.commit()?; - + // Rebuild the catalog if not disabled if !no_rebuild { info!("Rebuilding catalog..."); repo.rebuild(Some(publisher), false, false)?; } - + info!("Package restored successfully: {}", parsed_fmri); Ok(()) - }, - + } + Commands::ExportObsoleted { repo_uri_or_path, publisher, @@ -1719,15 +1729,15 @@ fn main() -> Result<()> { pattern, } => { info!("Exporting obsoleted packages for publisher: {}", publisher); - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Export the obsoleted packages let count = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Export the obsoleted packages let output_path = PathBuf::from(output_file); obsoleted_manager.export_obsoleted_packages( @@ -1736,38 +1746,35 @@ fn main() -> Result<()> { &output_path, )? }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + info!("Exported {} obsoleted packages to {}", count, output_file); Ok(()) - }, - + } + Commands::ImportObsoleted { repo_uri_or_path, input_file, publisher, } => { info!("Importing obsoleted packages from {}", input_file); - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Import the obsoleted packages let count = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Import the obsoleted packages let input_path = PathBuf::from(input_file); - obsoleted_manager.import_obsoleted_packages( - &input_path, - publisher.as_deref(), - )? + obsoleted_manager.import_obsoleted_packages(&input_path, publisher.as_deref())? }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + info!("Imported {} obsoleted packages", count); Ok(()) - }, - + } + Commands::CleanupObsoleted { repo_uri_or_path, publisher, @@ -1775,35 +1782,36 @@ fn main() -> Result<()> { dry_run, } => { if *dry_run { - info!("Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}", - ttl_days, publisher); + info!( + "Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}", + ttl_days, publisher + ); } else { - info!("Cleaning up obsoleted packages older than {} days for publisher: {}", - ttl_days, publisher); + info!( + "Cleaning up obsoleted packages older than {} days for publisher: {}", + ttl_days, publisher + ); } - + // Open the repository let mut repo = FileBackend::open(repo_uri_or_path)?; - + // Clean up the obsoleted packages let count = { // Get the obsoleted package manager let obsoleted_manager = repo.get_obsoleted_manager()?; - + // Clean up the obsoleted packages - obsoleted_manager.cleanup_obsoleted_packages_older_than_ttl( - publisher, - *ttl_days, - *dry_run, - )? + obsoleted_manager + .cleanup_obsoleted_packages_older_than_ttl(publisher, *ttl_days, *dry_run)? }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo - + if *dry_run { info!("Dry run: Would remove {} obsoleted packages", count); } else { info!("Successfully removed {} obsoleted packages", count); } - + Ok(()) } } diff --git a/pkg6repo/src/pkg5_import.rs b/pkg6repo/src/pkg5_import.rs index 8bcd345..26d8566 100644 --- a/pkg6repo/src/pkg5_import.rs +++ b/pkg6repo/src/pkg5_import.rs @@ -222,7 +222,8 @@ impl Pkg5Importer { } // Import packages and get counts - let (regular_count, obsoleted_count) = self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?; + let (regular_count, obsoleted_count) = + self.import_packages(&source_path, &mut dest_repo, publisher_to_import)?; let total_count = regular_count + obsoleted_count; // Rebuild catalog and search index @@ -235,7 +236,7 @@ impl Pkg5Importer { info!(" Total packages processed: {}", total_count); info!(" Regular packages imported: {}", regular_count); info!(" Obsoleted packages stored: {}", obsoleted_count); - + Ok(()) } @@ -267,7 +268,7 @@ impl Pkg5Importer { } /// Imports packages from the source repository - /// + /// /// Returns a tuple of (regular_package_count, obsoleted_package_count) fn import_packages( &self, @@ -349,13 +350,15 @@ impl Pkg5Importer { } let total_package_count = regular_package_count + obsoleted_package_count; - info!("Imported {} packages ({} regular, {} obsoleted)", - total_package_count, regular_package_count, obsoleted_package_count); + info!( + "Imported {} packages ({} regular, {} obsoleted)", + total_package_count, regular_package_count, obsoleted_package_count + ); Ok((regular_package_count, obsoleted_package_count)) } /// Imports a specific package version - /// + /// /// Returns a boolean indicating whether the package was obsoleted fn import_package_version( &self, @@ -389,7 +392,7 @@ impl Pkg5Importer { // Check if this is an obsoleted package let mut is_obsoleted = false; let mut fmri_str = String::new(); - + // Extract the FMRI from the manifest for attr in &manifest.attributes { if attr.key == "pkg.fmri" && !attr.values.is_empty() { @@ -397,7 +400,7 @@ impl Pkg5Importer { break; } } - + // Check for pkg.obsolete attribute for attr in &manifest.attributes { if attr.key == "pkg.obsolete" && !attr.values.is_empty() { @@ -408,11 +411,11 @@ impl Pkg5Importer { } } } - + // If this is an obsoleted package, store it in the obsoleted directory if is_obsoleted && !fmri_str.is_empty() { debug!("Handling obsoleted package: {}", fmri_str); - + // Parse the FMRI let fmri = match Fmri::parse(&fmri_str) { Ok(fmri) => fmri, @@ -424,10 +427,10 @@ impl Pkg5Importer { ))); } }; - + // Get the obsoleted package manager let obsoleted_manager = dest_repo.get_obsoleted_manager()?; - + // Store the obsoleted package with null hash (don't store the original manifest) // This saves storage space for obsoleted packages that don't provide any useful // information beyond the fact that they are obsoleted. When a client requests @@ -439,18 +442,18 @@ impl Pkg5Importer { publisher, &fmri, &manifest_content, - None, // No obsoleted_by information available - None, // No deprecation message available + None, // No obsoleted_by information available + None, // No deprecation message available false, // Don't store the original manifest, use null hash instead )?; - + info!("Stored obsoleted package: {}", fmri); return Ok(true); // Return true to indicate this was an obsoleted package } // For non-obsoleted packages, proceed with normal import debug!("Processing regular (non-obsoleted) package"); - + // Begin a transaction debug!("Beginning transaction"); let mut transaction = dest_repo.begin_transaction()?; @@ -462,7 +465,8 @@ impl Pkg5Importer { // Debug the repository structure debug!( "Publisher directory: {}", - libips::repository::FileBackend::construct_package_dir(&dest_repo.path, publisher, "").display() + libips::repository::FileBackend::construct_package_dir(&dest_repo.path, publisher, "") + .display() ); // Extract files referenced in the manifest @@ -486,10 +490,10 @@ impl Pkg5Importer { let first_two = &hash[0..2]; let next_two = &hash[2..4]; let file_path_new = file_dir.join(first_two).join(next_two).join(&hash); - + // Fall back to the old one-level hierarchy if the file doesn't exist in the new structure let file_path_old = file_dir.join(first_two).join(&hash); - + // Use the path that exists let file_path = if file_path_new.exists() { file_path_new diff --git a/pkgtree/src/main.rs b/pkgtree/src/main.rs index 0db126b..ad7e7a4 100644 --- a/pkgtree/src/main.rs +++ b/pkgtree/src/main.rs @@ -82,15 +82,18 @@ impl std::fmt::Display for OutputFormat { #[derive(Error, Debug, Diagnostic)] #[error("pkgtree error: {message}")] -#[diagnostic(code(ips::pkgtree_error), help("See logs with RUST_LOG=pkgtree=debug for more details."))] +#[diagnostic( + code(ips::pkgtree_error), + help("See logs with RUST_LOG=pkgtree=debug for more details.") +)] struct PkgTreeError { message: String, } #[derive(Debug, Clone)] struct Edge { - to: String, // target stem - dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.) + to: String, // target stem + dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.) } #[derive(Debug, Default, Clone)] @@ -101,7 +104,10 @@ struct Graph { impl Graph { fn add_edge(&mut self, from: String, to: String, dep_type: String) { - self.adj.entry(from).or_default().push(Edge { to, dep_type }); + self.adj + .entry(from) + .or_default() + .push(Edge { to, dep_type }); } fn stems(&self) -> impl Iterator { @@ -127,8 +133,9 @@ fn main() -> Result<()> { tracing_subscriber::fmt().with_env_filter(env_filter).init(); // Load image - let image = Image::load(&cli.image_path) - .map_err(|e| PkgTreeError { message: format!("Failed to load image at {:?}: {}", cli.image_path, e) })?; + let image = Image::load(&cli.image_path).map_err(|e| PkgTreeError { + message: format!("Failed to load image at {:?}: {}", cli.image_path, e), + })?; // Targeted analysis of solver error file has top priority if provided if let Some(err_path) = &cli.solver_error_file { @@ -145,16 +152,27 @@ fn main() -> Result<()> { // Dangling dependency scan has priority over graph mode if cli.find_dangling { - run_dangling_scan(&image, cli.publisher.as_deref(), cli.package.as_deref(), cli.format)?; + run_dangling_scan( + &image, + cli.publisher.as_deref(), + cli.package.as_deref(), + cli.format, + )?; return Ok(()); } // Graph mode // Query catalog (filtered if --package provided) let mut pkgs = if let Some(ref needle) = cli.package { - image.query_catalog(Some(needle.as_str())).map_err(|e| PkgTreeError { message: format!("Failed to query catalog: {}", e) })? + image + .query_catalog(Some(needle.as_str())) + .map_err(|e| PkgTreeError { + message: format!("Failed to query catalog: {}", e), + })? } else { - image.query_catalog(None).map_err(|e| PkgTreeError { message: format!("Failed to query catalog: {}", e) })? + image.query_catalog(None).map_err(|e| PkgTreeError { + message: format!("Failed to query catalog: {}", e), + })? }; // Filter by publisher if specified @@ -203,13 +221,16 @@ fn main() -> Result<()> { // If no nodes were added (e.g., filter too narrow), try building graph for all packages to support cycle analysis if graph.adj.is_empty() && filter_substr.is_some() { - info!("No packages matched filter for dependency graph; analyzing full catalog for cycles/tree context."); + info!( + "No packages matched filter for dependency graph; analyzing full catalog for cycles/tree context." + ); for p in &pkgs { match image.get_manifest_from_catalog(&p.fmri) { Ok(Some(manifest)) => { let from_stem = p.fmri.stem().to_string(); for dep in manifest.dependencies { - if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { + if dep.dependency_type != "require" && dep.dependency_type != "incorporate" + { continue; } if let Some(dep_fmri) = dep.fmri { @@ -227,7 +248,9 @@ fn main() -> Result<()> { let roots: Vec = if let Some(ref needle) = filter_substr { let mut r = HashSet::new(); for k in graph.adj.keys() { - if k.contains(needle) { r.insert(k.clone()); } + if k.contains(needle) { + r.insert(k.clone()); + } } r.into_iter().collect() } else { @@ -253,19 +276,47 @@ fn main() -> Result<()> { OutputFormat::Json => { use serde::Serialize; #[derive(Serialize)] - struct JsonEdge { from: String, to: String, dep_type: String } + struct JsonEdge { + from: String, + to: String, + dep_type: String, + } #[derive(Serialize)] - struct JsonCycle { nodes: Vec, edges: Vec } + struct JsonCycle { + nodes: Vec, + edges: Vec, + } #[derive(Serialize)] - struct Payload { edges: Vec, cycles: Vec } + struct Payload { + edges: Vec, + cycles: Vec, + } let mut edges = Vec::new(); for (from, es) in &graph.adj { - for e in es { edges.push(JsonEdge{ from: from.clone(), to: e.to.clone(), dep_type: e.dep_type.clone() }); } + for e in es { + edges.push(JsonEdge { + from: from.clone(), + to: e.to.clone(), + dep_type: e.dep_type.clone(), + }); + } } - let cycles_json = cycles.iter().map(|c| JsonCycle { nodes: c.nodes.clone(), edges: c.edges.clone() }).collect(); - let payload = Payload { edges, cycles: cycles_json }; - println!("{}", serde_json::to_string_pretty(&payload).into_diagnostic()?); + let cycles_json = cycles + .iter() + .map(|c| JsonCycle { + nodes: c.nodes.clone(), + edges: c.edges.clone(), + }) + .collect(); + let payload = Payload { + edges, + cycles: cycles_json, + }; + println!( + "{}", + serde_json::to_string_pretty(&payload).into_diagnostic()? + ); } } @@ -285,7 +336,7 @@ struct AdviceIssue { path: Vec, // path from root to the missing dependency stem stem: String, // the missing stem constraint: DepConstraint, - details: String, // human description + details: String, // human description } #[derive(Default)] @@ -296,25 +347,42 @@ struct AdviceContext { catalog_cache: HashMap>, // stem -> [(publisher, fmri)] manifest_cache: HashMap, // fmri string -> manifest lock_cache: HashMap>, // stem -> release lock - candidate_cache: HashMap<(String, Option, Option, Option), Option>, // (stem, rel, branch, publisher) + candidate_cache: HashMap< + (String, Option, Option, Option), + Option, + >, // (stem, rel, branch, publisher) } impl AdviceContext { fn new(publisher: Option, advice_cap: usize) -> Self { - AdviceContext { publisher, advice_cap, ..Default::default() } + AdviceContext { + publisher, + advice_cap, + ..Default::default() + } } } -fn run_advisor(image: &Image, ctx: &mut AdviceContext, root_stem: &str, max_depth: usize) -> Result<()> { +fn run_advisor( + image: &Image, + ctx: &mut AdviceContext, + root_stem: &str, + max_depth: usize, +) -> Result<()> { info!("Advisor analyzing installability for root: {}", root_stem); // Find best candidate for root let root_fmri = match find_best_candidate(image, ctx, root_stem, None, None) { Ok(Some(fmri)) => fmri, Ok(None) => { - println!("No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.", - root_stem, - ctx.publisher.as_ref().map(|p| format!(" '{}')", p)).unwrap_or_else(|| "".to_string())); + println!( + "No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.", + root_stem, + ctx.publisher + .as_ref() + .map(|p| format!(" '{}')", p)) + .unwrap_or_else(|| "".to_string()) + ); return Ok(()); } Err(e) => return Err(e), @@ -326,36 +394,81 @@ fn run_advisor(image: &Image, ctx: &mut AdviceContext, root_stem: &str, max_dept let mut issues: Vec = Vec::new(); let mut seen: HashSet = HashSet::new(); let mut path: Vec = vec![root_stem.to_string()]; - advise_recursive(image, ctx, &root_fmri, &mut path, 1, max_depth, &mut seen, &mut issues)?; + advise_recursive( + image, + ctx, + &root_fmri, + &mut path, + 1, + max_depth, + &mut seen, + &mut issues, + )?; // Print summary if issues.is_empty() { - println!("No immediate missing dependencies detected up to depth {} for root '{}'.\nIf installs still fail, try running with higher --advice-depth or check solver logs.", max_depth, root_stem); + println!( + "No immediate missing dependencies detected up to depth {} for root '{}'.\nIf installs still fail, try running with higher --advice-depth or check solver logs.", + max_depth, root_stem + ); } else { println!("Found {} installability issue(s):", issues.len()); for (i, iss) in issues.iter().enumerate() { let constraint_str = format!( "{}{}", - iss.constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(), - iss.constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(), - ).trim().to_string(); - println!(" {}. {}\n - Path: {}\n - Constraint: {}\n - Details: {}", + iss.constraint + .release + .as_ref() + .map(|r| format!("release={} ", r)) + .unwrap_or_default(), + iss.constraint + .branch + .as_ref() + .map(|b| format!("branch={}", b)) + .unwrap_or_default(), + ) + .trim() + .to_string(); + println!( + " {}. {}\n - Path: {}\n - Constraint: {}\n - Details: {}", i + 1, format!("No viable candidates for '{}'", iss.stem), iss.path.join(" -> "), - if constraint_str.is_empty() { "".to_string() } else { constraint_str }, + if constraint_str.is_empty() { + "".to_string() + } else { + constraint_str + }, iss.details, ); // Suggestions println!(" - Suggestions:"); - println!(" • Add or publish a matching package for '{}'{}{}.", + println!( + " • Add or publish a matching package for '{}'{}{}.", iss.stem, - iss.constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(), - iss.constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default()); - println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases."); - if let Some(lock) = get_incorporated_release_cached(image, ctx, &iss.stem).ok().flatten() { - println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", iss.stem, lock); + iss.constraint + .release + .as_ref() + .map(|r| format!(" (release={})", r)) + .unwrap_or_default(), + iss.constraint + .branch + .as_ref() + .map(|b| format!(" (branch={})", b)) + .unwrap_or_default() + ); + println!( + " • Alternatively, relax the dependency constraint in the requiring package to match available releases." + ); + if let Some(lock) = get_incorporated_release_cached(image, ctx, &iss.stem) + .ok() + .flatten() + { + println!( + " • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", + iss.stem, lock + ); } println!(" • Ensure catalogs are up to date: 'pkg6 refresh'."); } @@ -374,7 +487,9 @@ fn advise_recursive( seen: &mut HashSet, issues: &mut Vec, ) -> Result<()> { - if max_depth != 0 && depth > max_depth { return Ok(()); } + if max_depth != 0 && depth > max_depth { + return Ok(()); + } // Load manifest of the current FMRI (cached) let manifest = get_manifest_cached(image, ctx, fmri)?; @@ -383,30 +498,61 @@ fn advise_recursive( let mut constrained = Vec::new(); let mut unconstrained = Vec::new(); for dep in manifest.dependencies { - if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { continue; } + if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { + continue; + } let has_fmri = dep.fmri.is_some(); - if !has_fmri { continue; } + if !has_fmri { + continue; + } let c = extract_constraint(&dep.optional); - if c.release.is_some() || c.branch.is_some() { constrained.push((dep, c)); } else { unconstrained.push((dep, c)); } + if c.release.is_some() || c.branch.is_some() { + constrained.push((dep, c)); + } else { + unconstrained.push((dep, c)); + } } for (dep, constraint) in constrained.into_iter().chain(unconstrained.into_iter()) { if ctx.advice_cap != 0 && processed >= ctx.advice_cap { - debug!("Dependency processing for {} truncated at cap {}", fmri.stem(), ctx.advice_cap); + debug!( + "Dependency processing for {} truncated at cap {}", + fmri.stem(), + ctx.advice_cap + ); break; } processed += 1; let dep_stem = dep.fmri.unwrap().stem().to_string(); - debug!("Checking dependency to '{}' with constraint {:?}", dep_stem, (&constraint.release, &constraint.branch)); + debug!( + "Checking dependency to '{}' with constraint {:?}", + dep_stem, + (&constraint.release, &constraint.branch) + ); - match find_best_candidate(image, ctx, &dep_stem, constraint.release.as_deref(), constraint.branch.as_deref())? { + match find_best_candidate( + image, + ctx, + &dep_stem, + constraint.release.as_deref(), + constraint.branch.as_deref(), + )? { Some(next_fmri) => { // Continue recursion if not seen and depth allows if !seen.contains(&dep_stem) { seen.insert(dep_stem.clone()); path.push(dep_stem.clone()); - advise_recursive(image, ctx, &next_fmri, path, depth + 1, max_depth, seen, issues)?; + advise_recursive( + image, + ctx, + &next_fmri, + path, + depth + 1, + max_depth, + seen, + issues, + )?; path.pop(); } } @@ -438,15 +584,28 @@ fn extract_constraint(optional: &[libips::actions::Property]) -> DepConstraint { DepConstraint { release, branch } } -fn build_missing_detail(image: &Image, ctx: &mut AdviceContext, stem: &str, constraint: &DepConstraint) -> String { +fn build_missing_detail( + image: &Image, + ctx: &mut AdviceContext, + stem: &str, + constraint: &DepConstraint, +) -> String { // List available releases/branches for informational purposes let mut available: Vec = Vec::new(); if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) { for (pubname, fmri) in list { - if let Some(ref pfilter) = ctx.publisher { if &pubname != pfilter { continue; } } - if fmri.stem() != stem { continue; } + if let Some(ref pfilter) = ctx.publisher { + if &pubname != pfilter { + continue; + } + } + if fmri.stem() != stem { + continue; + } let ver = fmri.version(); - if ver.is_empty() { continue; } + if ver.is_empty() { + continue; + } available.push(ver); } } @@ -460,17 +619,43 @@ fn build_missing_detail(image: &Image, ctx: &mut AdviceContext, stem: &str, cons available.join(", ") }; - let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten(); + let lock = get_incorporated_release_cached(image, ctx, stem) + .ok() + .flatten(); match (&constraint.release, &constraint.branch, lock) { - (Some(r), Some(b), Some(lr)) => format!("Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {}", r, b, lr, available_str), - (Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str), - (Some(r), None, Some(lr)) => format!("Required release={} not found. Incorporation lock release={} present. Available versions: {}", r, lr, available_str), - (Some(r), None, None) => format!("Required release={} not found. Available versions: {}", r, available_str), - (None, Some(b), Some(lr)) => format!("Required branch={} not found. Incorporation lock release={} present. Available versions: {}", b, lr, available_str), - (None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str), - (None, None, Some(lr)) => format!("No candidates matched. Incorporation lock release={} present. Available versions: {}", lr, available_str), - (None, None, None) => format!("No candidates matched. Available versions: {}", available_str), + (Some(r), Some(b), Some(lr)) => format!( + "Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {}", + r, b, lr, available_str + ), + (Some(r), Some(b), None) => format!( + "Required release={}, branch={} not found. Available versions: {}", + r, b, available_str + ), + (Some(r), None, Some(lr)) => format!( + "Required release={} not found. Incorporation lock release={} present. Available versions: {}", + r, lr, available_str + ), + (Some(r), None, None) => format!( + "Required release={} not found. Available versions: {}", + r, available_str + ), + (None, Some(b), Some(lr)) => format!( + "Required branch={} not found. Incorporation lock release={} present. Available versions: {}", + b, lr, available_str + ), + (None, Some(b), None) => format!( + "Required branch={} not found. Available versions: {}", + b, available_str + ), + (None, None, Some(lr)) => format!( + "No candidates matched. Incorporation lock release={} present. Available versions: {}", + lr, available_str + ), + (None, None, None) => format!( + "No candidates matched. Available versions: {}", + available_str + ), } } @@ -494,25 +679,47 @@ fn find_best_candidate( let mut candidates: Vec<(String, libips::fmri::Fmri)> = Vec::new(); // Prefer matching release from incorporation lock, unless explicit req_release provided - let lock_release = if req_release.is_none() { get_incorporated_release_cached(image, ctx, stem).ok().flatten() } else { None }; + let lock_release = if req_release.is_none() { + get_incorporated_release_cached(image, ctx, stem) + .ok() + .flatten() + } else { + None + }; for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? { - if let Some(ref pfilter) = ctx.publisher { if &pubf != pfilter { continue; } } - if pfmri.stem() != stem { continue; } + if let Some(ref pfilter) = ctx.publisher { + if &pubf != pfilter { + continue; + } + } + if pfmri.stem() != stem { + continue; + } let ver = pfmri.version(); - if ver.is_empty() { continue; } + if ver.is_empty() { + continue; + } // Parse version string to extract release and branch heuristically: release,branch-rest let rel = version_release(&ver); let br = version_branch(&ver); if let Some(req_r) = req_release { - if Some(req_r) != rel.as_deref() { continue; } + if Some(req_r) != rel.as_deref() { + continue; + } } else if let Some(lock_r) = lock_release.as_deref() { - if Some(lock_r) != rel.as_deref() { continue; } + if Some(lock_r) != rel.as_deref() { + continue; + } } - if let Some(req_b) = req_branch { if Some(req_b) != br.as_deref() { continue; } } + if let Some(req_b) = req_branch { + if Some(req_b) != br.as_deref() { + continue; + } + } candidates.push((ver.clone(), pfmri.clone())); } @@ -550,7 +757,10 @@ fn query_catalog_cached( // We don't have mutable borrow on ctx here; clone and return, caller will populate cache through a mutable wrapper. // To keep code simple, provide a small wrapper that fills the cache when needed. // We'll implement a separate function that has mutable ctx. - let mut tmp_ctx = AdviceContext { catalog_cache: ctx.catalog_cache.clone(), ..Default::default() }; + let mut tmp_ctx = AdviceContext { + catalog_cache: ctx.catalog_cache.clone(), + ..Default::default() + }; query_catalog_cached_mut(image, &mut tmp_ctx, stem) } @@ -563,10 +773,9 @@ fn query_catalog_cached_mut( return Ok(v.clone()); } let mut out = Vec::new(); - for p in image - .query_catalog(Some(stem)) - .map_err(|e| PkgTreeError { message: format!("Failed to query catalog for {}: {}", stem, e) })? - { + for p in image.query_catalog(Some(stem)).map_err(|e| PkgTreeError { + message: format!("Failed to query catalog for {}: {}", stem, e), + })? { out.push((p.publisher, p.fmri)); } ctx.catalog_cache.insert(stem.to_string(), out.clone()); @@ -584,7 +793,9 @@ fn get_manifest_cached( } let manifest_opt = image .get_manifest_from_catalog(fmri) - .map_err(|e| PkgTreeError { message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e) })?; + .map_err(|e| PkgTreeError { + message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e), + })?; let manifest = manifest_opt.unwrap_or_else(|| libips::actions::Manifest::new()); ctx.manifest_cache.insert(key, manifest.clone()); Ok(manifest) @@ -595,7 +806,9 @@ fn get_incorporated_release_cached( ctx: &mut AdviceContext, stem: &str, ) -> Result> { - if let Some(v) = ctx.lock_cache.get(stem) { return Ok(v.clone()); } + if let Some(v) = ctx.lock_cache.get(stem) { + return Ok(v.clone()); + } let v = image.get_incorporated_release(stem)?; ctx.lock_cache.insert(stem.to_string(), v.clone()); Ok(v) @@ -607,7 +820,9 @@ fn print_trees(graph: &Graph, roots: &[String], max_depth: usize) { // Print a tree for each root let mut printed = HashSet::new(); for r in roots { - if printed.contains(r) { continue; } + if printed.contains(r) { + continue; + } printed.insert(r.clone()); println!("{}", r); let mut path = Vec::new(); @@ -625,7 +840,9 @@ fn print_tree_rec( path: &mut Vec, _seen: &mut HashSet, ) { - if max_depth != 0 && depth > max_depth { return; } + if max_depth != 0 && depth > max_depth { + return; + } path.push(node.to_string()); if let Some(edges) = graph.adj.get(node) { @@ -675,7 +892,11 @@ fn dfs_cycles( let mut cycle_edges = Vec::new(); for i in pos..stack.len() { let from = &stack[i]; - let to2 = if i + 1 < stack.len() { &stack[i+1] } else { to }; + let to2 = if i + 1 < stack.len() { + &stack[i + 1] + } else { + to + }; if let Some(es2) = graph.adj.get(from) { if let Some(edge) = es2.iter().find(|ed| &ed.to == to2) { cycle_edges.push(edge.dep_type.clone()); @@ -684,7 +905,10 @@ fn dfs_cycles( } } } - cycles.push(Cycle { nodes: cycle_nodes, edges: cycle_edges }); + cycles.push(Cycle { + nodes: cycle_nodes, + edges: cycle_edges, + }); } else if !visited.contains(to) { dfs_cycles(graph, to, visited, stack, cycles); } @@ -702,7 +926,7 @@ fn dedup_cycles(mut cycles: Vec) -> Vec { } // rotate to minimal node position (excluding the duplicate last element when comparing) if c.nodes.len() > 1 { - let inner = &c.nodes[..c.nodes.len()-1]; + let inner = &c.nodes[..c.nodes.len() - 1]; if let Some((min_idx, _)) = inner.iter().enumerate().min_by_key(|(_, n)| *n) { c.nodes.rotate_left(min_idx); c.edges.rotate_left(min_idx); @@ -713,7 +937,12 @@ fn dedup_cycles(mut cycles: Vec) -> Vec { let mut seen = HashSet::new(); cycles.retain(|c| { let key = c.nodes.join("->"); - if seen.contains(&key) { false } else { seen.insert(key); true } + if seen.contains(&key) { + false + } else { + seen.insert(key); + true + } }); cycles } @@ -730,7 +959,9 @@ fn print_cycles(cycles: &[Cycle]) { } fn print_suggestions(cycles: &[Cycle], graph: &Graph) { - if cycles.is_empty() { return; } + if cycles.is_empty() { + return; + } println!("\nSuggestions to break cycles (heuristic):"); for (i, c) in cycles.iter().enumerate() { // Prefer breaking an 'incorporate' edge if present, otherwise any edge @@ -741,16 +972,30 @@ fn print_suggestions(cycles: &[Cycle], graph: &Graph) { if let Some(es) = graph.adj.get(from) { for e in es { if &e.to == to { - if e.dep_type == "incorporate" { suggested = Some((from.clone(), to.clone())); break 'outer; } - if suggested.is_none() { suggested = Some((from.clone(), to.clone())); } + if e.dep_type == "incorporate" { + suggested = Some((from.clone(), to.clone())); + break 'outer; + } + if suggested.is_none() { + suggested = Some((from.clone(), to.clone())); + } } } } } if let Some((from, to)) = suggested { - println!(" {}. Consider relaxing/removing edge {} -> {} (preferably if it's an incorporation).", i + 1, from, to); + println!( + " {}. Consider relaxing/removing edge {} -> {} (preferably if it's an incorporation).", + i + 1, + from, + to + ); } else { - println!(" {}. Consider relaxing one edge along the cycle: {}", i + 1, c.nodes.join(" -> ")); + println!( + " {}. Consider relaxing one edge along the cycle: {}", + i + 1, + c.nodes.join(" -> ") + ); } } } @@ -777,7 +1022,6 @@ mod tests { } } - // ---------- Dangling dependency scan ---------- fn run_dangling_scan( image: &Image, @@ -786,9 +1030,9 @@ fn run_dangling_scan( format: OutputFormat, ) -> Result<()> { // Query full catalog once - let mut pkgs = image - .query_catalog(None) - .map_err(|e| PkgTreeError { message: format!("Failed to query catalog: {}", e) })?; + let mut pkgs = image.query_catalog(None).map_err(|e| PkgTreeError { + message: format!("Failed to query catalog: {}", e), + })?; // Build set of available non-obsolete stems AND an index of available (release, branch) pairs per stem, // honoring publisher filter @@ -796,9 +1040,13 @@ fn run_dangling_scan( let mut available_index: HashMap)>> = HashMap::new(); for p in &pkgs { if let Some(pubf) = publisher { - if p.publisher != pubf { continue; } + if p.publisher != pubf { + continue; + } + } + if p.obsolete { + continue; } - if p.obsolete { continue; } let stem = p.fmri.stem().to_string(); available_stems.insert(stem.clone()); let ver = p.fmri.version(); @@ -828,8 +1076,12 @@ fn run_dangling_scan( Ok(Some(man)) => { let mut missing_for_pkg: Vec = Vec::new(); for dep in man.dependencies { - if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { continue; } - let Some(df) = dep.fmri else { continue; }; + if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { + continue; + } + let Some(df) = dep.fmri else { + continue; + }; let stem = df.stem().to_string(); // Extract version/branch constraints if any (from optional properties) @@ -849,7 +1101,9 @@ fn run_dangling_scan( let satisfies = |stem: &str, rel: Option<&str>, br: Option<&str>| -> bool { if let Some(list) = available_index.get(stem) { if let (Some(rreq), Some(breq)) = (rel, br) { - return list.iter().any(|(r, b)| r == rreq && b.as_deref() == Some(breq)); + return list + .iter() + .any(|(r, b)| r == rreq && b.as_deref() == Some(breq)); } else if let Some(rreq) = rel { return list.iter().any(|(r, _)| r == rreq); } else if let Some(breq) = br { @@ -868,14 +1122,24 @@ fn run_dangling_scan( if !satisfies(&stem, c.release.as_deref(), c.branch.as_deref()) { // Include constraint context in output for maintainers let mut ctx = String::new(); - if let Some(r) = &c.release { ctx.push_str(&format!("release={} ", r)); } - if let Some(b) = &c.branch { ctx.push_str(&format!("branch={}", b)); } + if let Some(r) = &c.release { + ctx.push_str(&format!("release={} ", r)); + } + if let Some(b) = &c.branch { + ctx.push_str(&format!("branch={}", b)); + } let ctx = ctx.trim().to_string(); - if ctx.is_empty() { mark_missing = Some(stem.clone()); } else { mark_missing = Some(format!("{} [required {}]", stem, ctx)); } + if ctx.is_empty() { + mark_missing = Some(stem.clone()); + } else { + mark_missing = Some(format!("{} [required {}]", stem, ctx)); + } } } - if let Some(m) = mark_missing { missing_for_pkg.push(m); } + if let Some(m) = mark_missing { + missing_for_pkg.push(m); + } } if !missing_for_pkg.is_empty() { missing_for_pkg.sort(); @@ -898,13 +1162,18 @@ fn run_dangling_scan( if dangling.is_empty() { println!("No dangling dependencies detected."); } else { - println!("Found {} package(s) with dangling dependencies:", dangling.len()); + println!( + "Found {} package(s) with dangling dependencies:", + dangling.len() + ); let mut keys: Vec = dangling.keys().cloned().collect(); keys.sort(); for k in keys { println!("- {}:", k); if let Some(list) = dangling.get(&k) { - for m in list { println!(" • {}", m); } + for m in list { + println!(" • {}", m); + } } } } @@ -912,10 +1181,16 @@ fn run_dangling_scan( OutputFormat::Json => { use serde::Serialize; #[derive(Serialize)] - struct DanglingJson { package_fmri: String, missing_stems: Vec } + struct DanglingJson { + package_fmri: String, + missing_stems: Vec, + } let mut out: Vec = Vec::new(); for (pkg, miss) in dangling.into_iter() { - out.push(DanglingJson { package_fmri: pkg, missing_stems: miss }); + out.push(DanglingJson { + package_fmri: pkg, + missing_stems: miss, + }); } out.sort_by(|a, b| a.package_fmri.cmp(&b.package_fmri)); println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?); @@ -927,8 +1202,9 @@ fn run_dangling_scan( // ---------- Targeted analysis: parse pkg6 solver error text ---------- fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathBuf) -> Result<()> { - let text = std::fs::read_to_string(err_path) - .map_err(|e| PkgTreeError { message: format!("Failed to read solver error file {:?}: {}", err_path, e) })?; + let text = std::fs::read_to_string(err_path).map_err(|e| PkgTreeError { + message: format!("Failed to read solver error file {:?}: {}", err_path, e), + })?; // Build a stack based on indentation before the tree bullet "└─". let mut stack: Vec = Vec::new(); @@ -943,14 +1219,22 @@ fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathB // Extract node text after "└─ " let bullet = "└─ "; - let start = match line.find(bullet) { Some(p) => p + bullet.len(), None => continue }; + let start = match line.find(bullet) { + Some(p) => p + bullet.len(), + None => continue, + }; let mut node_full = line[start..].trim().to_string(); // Remove trailing diagnostic phrases for leaf line if let Some(pos) = node_full.find("for which no candidates were found") { node_full = node_full[..pos].trim().trim_end_matches(',').to_string(); } - if level >= stack.len() { stack.push(node_full.clone()); } else { stack.truncate(level); stack.push(node_full.clone()); } + if level >= stack.len() { + stack.push(node_full.clone()); + } else { + stack.truncate(level); + stack.push(node_full.clone()); + } if line.contains("for which no candidates were found") { failing_leaf = Some(node_full.clone()); @@ -961,7 +1245,9 @@ fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathB } if failing_leaf.is_none() { - println!("Could not find a 'for which no candidates were found' leaf in the provided solver error file."); + println!( + "Could not find a 'for which no candidates were found' leaf in the provided solver error file." + ); return Ok(()); } @@ -983,23 +1269,56 @@ fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathB println!("Found 1 installability issue (from solver error):"); let constraint_str = format!( "{}{}", - constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(), - constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(), - ).trim().to_string(); - println!(" 1. No viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}", + constraint + .release + .as_ref() + .map(|r| format!("release={} ", r)) + .unwrap_or_default(), + constraint + .branch + .as_ref() + .map(|b| format!("branch={}", b)) + .unwrap_or_default(), + ) + .trim() + .to_string(); + println!( + " 1. No viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}", stem, path_stems.join(" -> "), - if constraint_str.is_empty() { "".to_string() } else { constraint_str }, + if constraint_str.is_empty() { + "".to_string() + } else { + constraint_str + }, details, ); println!(" - Suggestions:"); - println!(" • Add or publish a matching package for '{}'{}{}.", + println!( + " • Add or publish a matching package for '{}'{}{}.", stem, - constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(), - constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default()); - println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases."); - if let Some(lock) = get_incorporated_release_cached(image, &mut ctx, &stem).ok().flatten() { - println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", stem, lock); + constraint + .release + .as_ref() + .map(|r| format!(" (release={})", r)) + .unwrap_or_default(), + constraint + .branch + .as_ref() + .map(|b| format!(" (branch={})", b)) + .unwrap_or_default() + ); + println!( + " • Alternatively, relax the dependency constraint in the requiring package to match available releases." + ); + if let Some(lock) = get_incorporated_release_cached(image, &mut ctx, &stem) + .ok() + .flatten() + { + println!( + " • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", + stem, lock + ); } println!(" • Ensure catalogs are up to date: 'pkg6 refresh'."); @@ -1010,31 +1329,45 @@ fn stem_from_node(node: &str) -> String { // Node may be like: "pkg://...@ver would require" or "archiver/gnu-tar branch=5.11, which ..." or just a stem let first = node.split_whitespace().next().unwrap_or(""); if first.starts_with("pkg://") { - if let Ok(fmri) = libips::fmri::Fmri::parse(first) { return fmri.stem().to_string(); } + if let Ok(fmri) = libips::fmri::Fmri::parse(first) { + return fmri.stem().to_string(); + } } // If it contains '@' (FMRI without scheme), parse via Fmri::parse if first.contains('@') { - if let Ok(fmri) = libips::fmri::Fmri::parse(first) { return fmri.stem().to_string(); } + if let Ok(fmri) = libips::fmri::Fmri::parse(first) { + return fmri.stem().to_string(); + } } // Otherwise assume it's a stem token first.trim_end_matches(',').to_string() } fn parse_leaf_node(node: &str) -> (String, DepConstraint) { - let core = node.split("for which").next().unwrap_or(node).trim().trim_end_matches(',').to_string(); + let core = node + .split("for which") + .next() + .unwrap_or(node) + .trim() + .trim_end_matches(',') + .to_string(); let mut release: Option = None; let mut branch: Option = None; // Find release= if let Some(p) = core.find("release=") { let rest = &core[p + "release=".len()..]; - let end = rest.find(|c: char| c == ' ' || c == ',').unwrap_or(rest.len()); + let end = rest + .find(|c: char| c == ' ' || c == ',') + .unwrap_or(rest.len()); release = Some(rest[..end].to_string()); } // Find branch= if let Some(p) = core.find("branch=") { let rest = &core[p + "branch=".len()..]; - let end = rest.find(|c: char| c == ' ' || c == ',').unwrap_or(rest.len()); + let end = rest + .find(|c: char| c == ' ' || c == ',') + .unwrap_or(rest.len()); branch = Some(rest[..end].to_string()); } diff --git a/ports/src/main.rs b/ports/src/main.rs index edf15c2..f41b67e 100644 --- a/ports/src/main.rs +++ b/ports/src/main.rs @@ -2,10 +2,10 @@ mod sources; #[allow(clippy::result_large_err)] mod workspace; -use clap::ArgAction; use crate::workspace::Workspace; -use anyhow::anyhow; use anyhow::Result; +use anyhow::anyhow; +use clap::ArgAction; use clap::{Parser, Subcommand}; use specfile::macros; use specfile::parse; diff --git a/ports/src/workspace.rs b/ports/src/workspace.rs index 11f61f1..de7e046 100644 --- a/ports/src/workspace.rs +++ b/ports/src/workspace.rs @@ -3,10 +3,10 @@ use libips::actions::{ActionError, File as FileAction, Manifest}; use std::collections::HashMap; use std::env; use std::env::{current_dir, set_current_dir}; -use std::fs::{create_dir_all, File}; +use std::fs::{File, create_dir_all}; +use std::io::Error as IOError; use std::io::copy; use std::io::prelude::*; -use std::io::Error as IOError; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::result::Result as StdResult; diff --git a/specfile/src/macros.rs b/specfile/src/macros.rs index 0379c65..b407a5e 100644 --- a/specfile/src/macros.rs +++ b/specfile/src/macros.rs @@ -43,34 +43,38 @@ impl MacroParser { for macro_pair in inner.clone().into_inner() { match macro_pair.as_rule() { Rule::macro_name => { - replaced_line += self.get_variable(macro_pair.as_str())?; - }, + replaced_line += + self.get_variable(macro_pair.as_str())?; + } Rule::macro_parameter => { - println!("macro parameter: {}", macro_pair.as_str()) - }, + println!( + "macro parameter: {}", + macro_pair.as_str() + ) + } _ => panic!( "Unexpected macro match please update the code together with the peg grammar: {:?}", macro_pair.as_rule() - ) + ), } } } _ => panic!( "Unexpected inner match please update the code together with the peg grammar: {:?}", inner.as_rule() - ) + ), } } - }, + } Rule::EOI => (), Rule::text => { replaced_line += test_pair.as_str(); replaced_line += " "; - }, + } _ => panic!( "Unexpected match please update the code together with the peg grammar: {:?}", test_pair.as_rule() - ) + ), } } } diff --git a/userland/src/component.rs b/userland/src/component.rs index adc5144..62bcffc 100644 --- a/userland/src/component.rs +++ b/userland/src/component.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use semver::Version; use std::collections::HashMap; use url::Url; diff --git a/userland/src/lib.rs b/userland/src/lib.rs index 7b27dbc..689ec53 100644 --- a/userland/src/lib.rs +++ b/userland/src/lib.rs @@ -1,10 +1,10 @@ mod component; pub mod repology; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use lazy_static::lazy_static; -use pest::iterators::Pairs; use pest::Parser; +use pest::iterators::Pairs; use pest_derive::Parser; use regex::Regex; use std::collections::HashMap; @@ -233,13 +233,16 @@ fn parse_makefile(pairs: Pairs, m: &mut Makefile) -> Result<()> { Rule::comment_string => (), Rule::include => { parse_include(p.into_inner(), m)?; - }, + } Rule::target => (), Rule::define => { parse_define(p.into_inner(), m)?; } Rule::EOI => (), - _ => panic!("unexpected rule {:?} inside makefile rule expected variable, define, comment, NEWLINE, include, target", p.as_rule()), + _ => panic!( + "unexpected rule {:?} inside makefile rule expected variable, define, comment, NEWLINE, include, target", + p.as_rule() + ), } } @@ -289,26 +292,23 @@ fn parse_variable(variable_pair: Pairs, m: &mut Makefile) -> Result Rule::variable_name => { var.0 = p.as_str().to_string(); } - Rule::variable_set => { - var.1.mode = VariableMode::Set - }, - Rule::variable_add => { - var.1.mode = VariableMode::Add - } - Rule::variable_value => { - match var.1.mode { - VariableMode::Add => { - if m.variables.contains_key(&var.0) { - var.1 = m.variables.get(&var.0).unwrap().clone() - } - var.1.values.push(p.as_str().to_string()); - } - VariableMode::Set => { - var.1.values.push(p.as_str().to_string()); + Rule::variable_set => var.1.mode = VariableMode::Set, + Rule::variable_add => var.1.mode = VariableMode::Add, + Rule::variable_value => match var.1.mode { + VariableMode::Add => { + if m.variables.contains_key(&var.0) { + var.1 = m.variables.get(&var.0).unwrap().clone() } + var.1.values.push(p.as_str().to_string()); } - } - _ => panic!("unexpected rule {:?} inside makefile rule expected variable_name, variable_set, variable_add, variable_value", p.as_rule()), + VariableMode::Set => { + var.1.values.push(p.as_str().to_string()); + } + }, + _ => panic!( + "unexpected rule {:?} inside makefile rule expected variable_name, variable_set, variable_add, variable_value", + p.as_rule() + ), } } m.variables.insert(var.0, var.1);