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.
This commit is contained in:
Till Wegmueller 2025-12-22 20:10:17 +01:00
parent d0fcdbec20
commit d2d1c297cc
No known key found for this signature in database
56 changed files with 5356 additions and 2748 deletions

View file

@ -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<PathBuf, InstallerError
Component::Prefix(_) | Component::RootDir => {
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<dyn Fn(ProgressEvent) + Send + Sync + 'static>;
/// 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<ProgressCallback>| {
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")]

View file

@ -898,7 +898,10 @@ impl Manifest {
match serde_json::from_str::<Manifest>(&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);

View file

@ -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<Self, IpsError> {
// 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<Self, IpsError> {
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<bool, IpsError> {
@ -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<String>) -> Self {
Self { repo, publisher: publisher.into() }
Self {
repo,
publisher: publisher.into(),
}
}
/// Begin a new transaction
pub fn begin(&self) -> Result<Txn, IpsError> {
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<Vec<FileDep>, IpsError> {
pub fn file_deps(
proto: &Path,
manifest: &Manifest,
mut opts: DependGenerateOptions,
) -> Result<Vec<FileDep>, 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<Manifest, IpsError> {
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<Fmri> {
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<PathBuf>,
pub rulesets: Vec<String>,
// Rule configurability
pub disabled_rules: Vec<String>, // rule IDs to disable
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
pub disabled_rules: Vec<String>, // rule IDs to disable
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
pub severity_overrides: std::collections::HashMap<String, lint::LintSeverity>,
pub rule_params: std::collections::HashMap<String, std::collections::HashMap<String, String>>, // rule_id -> (key->val)
}
@ -639,30 +686,47 @@ 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"))]
#[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)"))]
#[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<miette::Report>;
@ -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<miette::Report> {
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<miette::Report> {
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<miette::Report> {
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<Vec<miette::Report>, IpsError> {
pub fn lint_manifest(
manifest: &Manifest,
config: &LintConfig,
) -> Result<Vec<miette::Report>, IpsError> {
let mut diags: Vec<miette::Report> = 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);

View file

@ -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<String>) -> Self {
Self { message: message.into(), source: None }
Self {
message: message.into(),
source: None,
}
}
fn with_source(message: impl Into<String>, source: Box<dyn StdError + Send + Sync>) -> 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<Vec<FileDep>> {
pub fn generate_file_dependencies_from_manifest(
manifest: &Manifest,
opts: &GenerateOptions,
) -> Result<Vec<FileDep>> {
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<Vec<String>, DependError> {
fn insert_default_runpath(
defaults: &[String],
provided: &[String],
) -> std::result::Result<Vec<String>, 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<Vec<Regex>> {
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<String> = 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<R: ReadableRepository>(
for fd in file_deps {
match &fd.kind {
FileDepKind::Elf { base_name, run_paths, .. } => {
FileDepKind::Elf {
base_name,
run_paths,
..
} => {
let mut providers: Vec<Fmri> = Vec::new();
for dir in run_paths {
let full = normalize_join(dir, base_name);
@ -330,7 +388,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
// unresolved -> skip for now; future: emit analysis warnings
}
}
FileDepKind::Script { base_name, run_paths, .. } => {
FileDepKind::Script {
base_name,
run_paths,
..
} => {
let mut providers: Vec<Fmri> = Vec::new();
for dir in run_paths {
let full = normalize_join(dir, base_name);
@ -366,7 +428,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
} else {
}
}
FileDepKind::Python { base_names, run_paths, .. } => {
FileDepKind::Python {
base_names,
run_paths,
..
} => {
let mut providers: Vec<Fmri> = 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<R: ReadableRepository>(repo: &R, publisher: Option<&str>) -> Result<HashMap<String, Vec<Fmri>>> {
fn build_path_provider_map<R: ReadableRepository>(
repo: &R,
publisher: Option<&str>,
) -> Result<HashMap<String, Vec<Fmri>>> {
// 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<R: ReadableRepository>(repo: &R, publisher: Option<&s
let fmri = match pc.package_id.parse::<Fmri>() {
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<R: ReadableRepository>(repo: &R, publisher: Option<&s
Ok(map)
}
// --- Helpers for script processing ---
fn parse_shebang(bytes: &[u8]) -> Option<String> {
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 <service_bundle ...> 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::<u8>(), mi.as_str().parse::<u8>()) {
if let (Ok(maj), Ok(min)) =
(ma.as_str().parse::<u8>(), mi.as_str().parse::<u8>())
{
return Some((maj, min));
}
}
@ -580,7 +659,12 @@ fn collect_python_imports(src: &str) -> Vec<String> {
mods
}
fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: &GenerateOptions) -> Vec<FileDep> {
fn process_python(
bytes: &[u8],
installed_path: &str,
version: (u8, u8),
opts: &GenerateOptions,
) -> Vec<FileDep> {
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<String> {
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);
}
}
}
}

View file

@ -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 },

View file

@ -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);

View file

@ -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<T> = std::result::Result<T, CatalogError>;
// 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'[')
}
@ -144,35 +147,56 @@ impl ImageCatalog {
// 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(())
@ -181,17 +205,21 @@ impl ImageCatalog {
/// 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);
@ -204,15 +232,24 @@ impl ImageCatalog {
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());
@ -223,7 +260,7 @@ impl ImageCatalog {
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,10 +270,13 @@ 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
)))
}
}
}
@ -246,8 +286,15 @@ impl ImageCatalog {
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);
@ -255,27 +302,33 @@ impl ImageCatalog {
}
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
@ -284,23 +337,37 @@ impl ImageCatalog {
// 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);
@ -313,30 +380,43 @@ impl ImageCatalog {
/// 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(())
}
@ -352,28 +432,37 @@ impl ImageCatalog {
}
// 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 {
@ -383,28 +472,46 @@ impl ImageCatalog {
// 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/<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);
@ -414,8 +521,12 @@ impl ImageCatalog {
// 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
@ -425,7 +536,12 @@ 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
@ -433,10 +549,12 @@ impl ImageCatalog {
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(())
@ -472,11 +590,18 @@ impl ImageCatalog {
// 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() {
@ -499,17 +624,18 @@ 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;
}
@ -523,11 +649,18 @@ impl ImageCatalog {
};
// 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)?;
@ -538,13 +671,23 @@ impl ImageCatalog {
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,11 +708,7 @@ 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);
@ -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,9 +848,7 @@ impl ImageCatalog {
info!(
"Finished merged import for publisher {}: {} versions processed ({} obsolete)",
publisher,
processed,
obsolete_count
publisher, processed, obsolete_count
);
Ok(())
@ -728,11 +883,12 @@ impl ImageCatalog {
// 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();
@ -792,9 +952,10 @@ 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
)),
));
}
}
@ -812,7 +973,10 @@ impl ImageCatalog {
/// 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 {
@ -833,30 +997,40 @@ impl ImageCatalog {
/// Query the catalog for packages matching a pattern
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
// 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
@ -880,7 +1054,9 @@ impl ImageCatalog {
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) {
@ -918,8 +1094,12 @@ impl ImageCatalog {
// 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
@ -933,7 +1113,10 @@ impl ImageCatalog {
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 {
@ -941,9 +1124,12 @@ impl ImageCatalog {
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;
}
}
@ -955,14 +1141,17 @@ impl ImageCatalog {
/// Get a manifest from the catalog
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
// 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)
@ -974,12 +1163,15 @@ impl ImageCatalog {
}
// 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();

View file

@ -83,22 +83,32 @@ impl InstalledPackages {
.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::<Manifest>(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());
@ -109,7 +119,7 @@ impl InstalledPackages {
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,10 +129,13 @@ 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
)))
}
}
}
@ -134,7 +147,8 @@ impl InstalledPackages {
.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
@ -142,8 +156,15 @@ impl InstalledPackages {
// 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;
}
}
@ -169,14 +190,17 @@ impl InstalledPackages {
.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.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.commit().map_err(|e| {
InstalledError::Database(format!("Failed to commit transaction: {}", e))
})?;
Ok(())
}
@ -188,7 +212,8 @@ impl InstalledPackages {
.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)
@ -200,19 +225,27 @@ impl InstalledPackages {
// 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(())
@ -225,7 +258,8 @@ impl InstalledPackages {
.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)
@ -234,8 +268,9 @@ impl InstalledPackages {
// 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()) {
@ -243,15 +278,17 @@ impl InstalledPackages {
}
// 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(())
@ -264,21 +301,30 @@ impl InstalledPackages {
.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
@ -292,13 +338,13 @@ impl InstalledPackages {
let fmri = Fmri::from_str(key_str)?;
// Get the publisher (handling the Option<String>)
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
@ -315,7 +361,8 @@ impl InstalledPackages {
.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)
@ -324,8 +371,9 @@ impl InstalledPackages {
// 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()) {
@ -346,7 +394,8 @@ impl InstalledPackages {
.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)
@ -355,8 +404,9 @@ impl InstalledPackages {
// 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()) {

View file

@ -50,7 +50,10 @@ fn test_installed_packages() {
// 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

View file

@ -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;
@ -150,7 +150,13 @@ impl Image {
}
/// Add a publisher to the image
pub fn add_publisher(&mut self, name: &str, origin: &str, mirrors: Vec<String>, is_default: bool) -> Result<()> {
pub fn add_publisher(
&mut self,
name: &str,
origin: &str,
mirrors: Vec<String>,
is_default: bool,
) -> Result<()> {
// Check if publisher already exists
if self.publishers.iter().any(|p| p.name == name) {
// Update existing publisher
@ -304,7 +310,10 @@ 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
),
))
})
}
@ -315,7 +324,10 @@ impl Image {
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
),
))
})
}
@ -326,7 +338,10 @@ impl Image {
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
),
))
})
}
@ -338,12 +353,19 @@ impl Image {
// 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
@ -385,23 +411,35 @@ impl Image {
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<Vec<InstalledPackageInfo>> {
pub fn query_installed_packages(
&self,
pattern: Option<&str>,
) -> Result<Vec<InstalledPackageInfo>> {
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<Option<crate::actions::Manifest>> {
pub fn get_manifest_from_installed(
&self,
fmri: &crate::fmri::Fmri,
) -> Result<Option<crate::actions::Manifest>> {
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
))
})
}
@ -419,7 +457,11 @@ impl Image {
/// and stored under a flattened path:
/// manifests/<publisher>/<encoded_stem>@<encoded_version>.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<std::path::PathBuf> {
pub fn save_manifest(
&self,
fmri: &crate::fmri::Fmri,
_manifest: &crate::actions::Manifest,
) -> Result<std::path::PathBuf> {
// 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('%');
@ -484,7 +528,11 @@ impl Image {
/// 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))
})
@ -526,7 +574,8 @@ impl Image {
self.publishers.iter().collect()
} else {
// Otherwise, filter publishers by name
self.publishers.iter()
self.publishers
.iter()
.filter(|p| publishers.contains(&p.name))
.collect()
};
@ -541,19 +590,25 @@ impl Image {
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
),
))
})?;
}
}
@ -574,23 +629,29 @@ impl Image {
self.init_catalog_db()?;
// Get publisher names
let publisher_names: Vec<String> = self.publishers.iter()
.map(|p| p.name.clone())
.collect();
let publisher_names: Vec<String> = 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<Vec<PackageInfo>> {
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<Option<String>> {
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,26 +680,39 @@ 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<Option<crate::actions::Manifest>> {
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<Option<crate::actions::Manifest>> {
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))
})
@ -647,7 +723,10 @@ impl Image {
/// 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<crate::actions::Manifest> {
pub fn get_manifest_from_repository(
&self,
fmri: &crate::fmri::Fmri,
) -> Result<crate::actions::Manifest> {
// Determine publisher: use FMRI's publisher if present, otherwise default publisher
let publisher_name = if let Some(p) = &fmri.publisher {
p.clone()
@ -671,13 +750,15 @@ impl Image {
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)
}
}

View file

@ -34,7 +34,9 @@ fn test_catalog_methods() {
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());
@ -59,7 +61,10 @@ 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();
@ -88,13 +93,22 @@ 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...");
@ -109,7 +123,7 @@ fn test_catalog_methods() {
Ok(pkgs) => {
println!("Found {} packages", pkgs.len());
pkgs
},
}
Err(e) => {
println!("Failed to query catalog: {:?}", e);
panic!("Failed to query catalog: {:?}", e);
@ -126,7 +140,10 @@ fn test_catalog_methods() {
// 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();
@ -164,8 +181,12 @@ fn test_refresh_catalogs_directory_clearing() {
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();
@ -177,8 +198,16 @@ fn test_refresh_catalogs_directory_clearing() {
// 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());
@ -195,7 +224,11 @@ fn test_refresh_catalogs_directory_clearing() {
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

View file

@ -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);

View file

@ -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<P: AsRef<Path>>(path: P, publisher: impl Into<String>) -> Result<Self> {
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()?;

View file

@ -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"
);
}
}

View file

@ -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<String> {
// 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<String> {
// 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<String> {
// 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())
}

View file

@ -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<std::cell::RefCell<crate::repository::catalog::CatalogManager>>,
/// Manager for obsoleted packages
obsoleted_manager: Option<std::cell::RefCell<crate::repository::obsoleted::ObsoletedPackageManager>>,
obsoleted_manager:
Option<std::cell::RefCell<crate::repository::obsoleted::ObsoletedPackageManager>>,
}
/// 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,
@ -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<Vec<PackageContents>> {
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);
}
@ -1744,7 +1801,10 @@ impl FileBackend {
locale: &str,
fmri: &crate::fmri::Fmri,
op_type: crate::repository::catalog::CatalogOperationType,
catalog_parts: std::collections::HashMap<String, std::collections::HashMap<String, Vec<String>>>,
catalog_parts: std::collections::HashMap<
String,
std::collections::HashMap<String, Vec<String>>,
>,
signature_sha1: Option<String>,
) -> 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<PathBuf> {
@ -1880,8 +1956,7 @@ impl FileBackend {
.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)?;
@ -1939,10 +2014,7 @@ 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")
}
@ -1963,23 +2035,20 @@ impl FileBackend {
/// 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,10 +2057,7 @@ 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)
}
}
@ -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;
}
@ -2113,8 +2186,12 @@ impl FileBackend {
};
// 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
};
@ -2122,9 +2199,7 @@ impl FileBackend {
// 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
@ -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,25 +2581,42 @@ 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(())
}
@ -2536,7 +2639,8 @@ impl FileBackend {
) -> Result<std::cell::RefMut<'_, crate::repository::catalog::CatalogManager>> {
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);
}
@ -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,7 +2847,10 @@ 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;

View file

@ -216,8 +216,8 @@ impl From<bincode::error::EncodeError> 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

File diff suppressed because it is too large Load diff

View file

@ -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.
///
@ -132,7 +132,10 @@ impl WritableRepository for RestBackend {
}
// 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");
@ -144,7 +147,10 @@ impl WritableRepository for RestBackend {
}
// 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 {
@ -259,7 +265,9 @@ impl WritableRepository for RestBackend {
// 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
@ -336,21 +344,26 @@ impl ReadableRepository for RestBackend {
match response.json::<Value>() {
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);
}
@ -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<crate::digest::DigestAlgorithm> = 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<String> = 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.
///
@ -729,7 +766,9 @@ 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
@ -743,7 +782,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(),
));
}
};
// The local cache path is expected to already point to the per-publisher directory
@ -753,7 +796,8 @@ 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())
@ -789,12 +833,18 @@ impl RestBackend {
// Prepare candidate URLs to support both modern and legacy pkg5 depotd layouts
let mut urls: Vec<String> = 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,10 +905,7 @@ 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),
}))
}
@ -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());
@ -969,26 +1024,29 @@ impl RestBackend {
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_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: 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(|| {
@ -1006,7 +1064,8 @@ impl RestBackend {
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);
@ -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)
}
}

View file

@ -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;
@ -210,8 +210,14 @@ mod tests {
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();
@ -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!(
@ -480,7 +497,10 @@ mod tests {
// 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();
@ -514,7 +534,10 @@ mod tests {
// 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();
@ -568,7 +591,10 @@ mod tests {
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());
@ -733,7 +759,11 @@ mod tests {
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();
}

View file

@ -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<String, Vec<(String, Fmri)>>, // stem -> [(publisher, fmri)]
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
lock_cache: HashMap<String, Option<String>>, // stem -> incorporated release
candidate_cache: HashMap<(String, Option<String>, Option<String>, Option<String>), Option<Fmri>>, // (stem, rel, branch, publisher)
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
lock_cache: HashMap<String, Option<String>>, // stem -> incorporated release
candidate_cache:
HashMap<(String, Option<String>, Option<String>, Option<String>), Option<Fmri>>, // (stem, rel, branch, publisher)
publisher_filter: Option<String>,
cap: usize,
}
impl Ctx {
fn new(publisher_filter: Option<String>, 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<AdviceReport, AdviceError> {
pub fn advise_from_error(
image: &Image,
err: &SolverError,
opts: AdviceOptions,
) -> Result<AdviceReport, AdviceError> {
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<String>,
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<String>, Option<String>)
(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<String> = 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() { "<none>".to_string() } else { available.join(", ") };
let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten();
let available_str = if available.is_empty() {
"<none>".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<String> {
}
fn version_branch(version: &str) -> Option<String> {
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<Vec<(String, Fmri)>, 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<Vec<(String, Fmri)>, 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<Manifest, AdviceError> {
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<Option<String>, 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<Option<String>, 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)
}

File diff suppressed because it is too large Load diff

View file

@ -71,7 +71,7 @@ mod tests {
// 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);
@ -142,7 +142,10 @@ mod tests {
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

View file

@ -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() {

View file

@ -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(),

View file

@ -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<T> = std::result::Result<T, Pkg6Error>;
#[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}")]

View file

@ -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;
@ -507,7 +507,11 @@ 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);
@ -526,7 +530,9 @@ 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());
}
@ -546,8 +552,19 @@ fn main() -> Result<()> {
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<libips::solver::Constraint> = 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() { "<none>".to_string() } else { constraint_str },
if iss.path.is_empty() {
iss.stem.clone()
} else {
iss.path.join(" -> ")
},
if constraint_str.is_empty() {
"<none>".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);
@ -734,8 +825,13 @@ fn main() -> Result<()> {
// 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);
@ -744,8 +840,19 @@ fn main() -> Result<()> {
// 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);
@ -760,8 +867,14 @@ fn main() -> Result<()> {
// 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);
@ -777,7 +890,9 @@ fn main() -> Result<()> {
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());
}
};
@ -804,21 +919,27 @@ fn main() -> Result<()> {
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} {}",
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,16 +950,22 @@ 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} {}",
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());
@ -848,8 +975,13 @@ fn main() -> Result<()> {
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);
@ -858,8 +990,13 @@ fn main() -> Result<()> {
// 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);
@ -868,8 +1005,12 @@ fn main() -> Result<()> {
// 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);
@ -877,8 +1018,13 @@ fn main() -> Result<()> {
// 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);
@ -887,8 +1033,12 @@ fn main() -> Result<()> {
// 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);
@ -897,8 +1047,13 @@ fn main() -> Result<()> {
// 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);
@ -907,8 +1062,12 @@ fn main() -> Result<()> {
// 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);
@ -922,7 +1081,9 @@ fn main() -> Result<()> {
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());
}
};
@ -937,7 +1098,10 @@ fn main() -> Result<()> {
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)?;
@ -955,13 +1119,15 @@ fn main() -> Result<()> {
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);
@ -974,7 +1140,9 @@ fn main() -> Result<()> {
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());
}
};
@ -993,8 +1161,12 @@ fn main() -> Result<()> {
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
@ -1006,7 +1178,9 @@ fn main() -> Result<()> {
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());
}
};
@ -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,7 +1272,7 @@ 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
@ -1108,26 +1285,30 @@ fn main() -> Result<()> {
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);
@ -1148,21 +1329,36 @@ fn main() -> Result<()> {
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);
@ -1177,7 +1373,9 @@ fn main() -> Result<()> {
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());
}
};
@ -1186,13 +1384,12 @@ fn main() -> Result<()> {
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 {
@ -1200,13 +1397,19 @@ fn main() -> Result<()> {
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
)));
}
}
@ -1215,13 +1418,19 @@ fn main() -> Result<()> {
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
)));
}
}
@ -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(())
},
}
}
}

View file

@ -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 {
@ -84,8 +84,9 @@ impl Config {
pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> {
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)))

View file

@ -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 {
@ -31,8 +31,12 @@ 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()),
};

View file

@ -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<Arc<DepotRepo>>,
) -> impl IntoResponse {
pub async fn health(_state: State<Arc<DepotRepo>>) -> 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<Arc<DepotRepo>>,
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<Arc<DepotRepo>>, 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()
}

View file

@ -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<Arc<DepotRepo>>,
@ -24,10 +24,13 @@ pub async fn get_catalog_v1(
// 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())),
}
}

View file

@ -1,24 +1,27 @@
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<Arc<DepotRepo>>,
Path((publisher, _algo, digest)): Path<(String, String, String)>,
req: Request,
) -> Result<Response, DepotError> {
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;
@ -27,26 +30,42 @@ pub async fn get_file(
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::<std::path::PathBuf>().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())),
}
}

View file

@ -1,30 +1,33 @@
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<Arc<DepotRepo>>,
Path((publisher, fmri_str)): Path<(String, String)>,
) -> Result<Response, DepotError> {
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::<Manifest>(&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();
@ -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));
}
@ -89,7 +106,9 @@ pub async fn get_info(
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!("<license content unavailable for digest {}>\n", digest));
out.push_str(&format!(
"<license content unavailable for digest {}>\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<String> {
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<String> {
// 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::<u128>() { size = size.saturating_add(v); }
if let Ok(v) = value.parse::<u128>() {
size = size.saturating_add(v);
}
} else if key == "pkg.csize" {
if let Ok(v) = value.parse::<u128>() { csize = csize.saturating_add(v); }
if let Ok(v) = value.parse::<u128>() {
csize = csize.saturating_add(v);
}
}
}
}

View file

@ -1,20 +1,21 @@
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<Arc<DepotRepo>>,
Path((publisher, fmri_str)): Path<(String, String)>,
) -> Result<Response, DepotError> {
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)
@ -28,5 +29,6 @@ pub async fn get_manifest(
(header::ETAG, etag.as_str()),
],
content,
).into_response())
)
.into_response())
}

View file

@ -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;

View file

@ -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 {
@ -43,10 +43,13 @@ async fn get_publisher_impl(
) -> Result<Response, DepotError> {
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),
))
}
}

View file

@ -56,12 +56,30 @@ 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],
},
],
};

View file

@ -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;

View file

@ -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<DepotRepo>) -> 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)
}

View file

@ -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()))
}

View file

@ -1,17 +1,17 @@
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();
@ -57,13 +57,24 @@ pub async fn run() -> Result<()> {
}
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);

View file

@ -1,5 +1,5 @@
use pkg6depotd::run;
use miette::Result;
use pkg6depotd::run;
#[tokio::main]
async fn main() -> Result<()> {

View file

@ -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<Self> {
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<PathBuf> {
let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash);
if cand_pub.exists() { return Some(cand_pub); }
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); }
let cand_global = FileBackend::construct_file_path(&self.root, hash);
if cand_global.exists() {
return Some(cand_global);
}
None
None
}
pub fn get_manifest_text(&self, publisher: &str, fmri: &Fmri) -> Result<String> {
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<PathBuf> {
@ -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<PathBuf> {
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<PathBuf> {
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<libips::repository::RepositoryInfo> {
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)
}
}

View file

@ -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()

View file

@ -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 {
@ -42,7 +42,7 @@ fn setup_repo(dir: &TempDir) -> PathBuf {
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(),
@ -98,7 +98,11 @@ async fn test_depot_server() {
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"));
@ -111,7 +115,7 @@ async fn test_depot_server() {
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();
@ -144,13 +148,24 @@ async fn test_depot_server() {
assert!(info_text.contains("Name: example"));
assert!(info_text.contains("Summary: Test Package"));
// Ensure FMRI format is correct: pkg://<publisher>/<name>@<version>
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");
@ -161,8 +176,8 @@ async fn test_depot_server() {
#[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
);
}
}
}

View file

@ -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);
@ -440,16 +452,21 @@ mod e2e_tests {
);
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);
@ -466,7 +483,11 @@ mod e2e_tests {
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());
@ -482,11 +503,16 @@ mod e2e_tests {
// 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"
"-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
@ -513,7 +539,13 @@ mod e2e_tests {
);
// 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: {:?}",
@ -529,9 +561,12 @@ mod e2e_tests {
// Show details of the obsoleted package
let result = run_pkg6repo(&[
"show-obsoleted",
"-s", repo_path.to_str().unwrap(),
"-p", "test",
"-f", &fmri
"-s",
repo_path.to_str().unwrap(),
"-p",
"test",
"-f",
&fmri,
]);
assert!(
result.is_ok(),

View file

@ -1273,7 +1273,7 @@ fn main() -> Result<()> {
info!("Repository imported successfully");
Ok(())
},
}
Commands::ObsoletePackage {
repo_uri_or_path,
@ -1295,7 +1295,7 @@ fn main() -> Result<()> {
&repo.path,
publisher,
parsed_fmri.stem(),
&parsed_fmri.version()
&parsed_fmri.version(),
);
println!("Looking for manifest at: {}", manifest_path.display());
@ -1336,7 +1336,7 @@ fn main() -> Result<()> {
info!("Package marked as obsoleted successfully: {}", parsed_fmri);
Ok(())
},
}
Commands::ListObsoleted {
repo_uri_or_path,
@ -1357,7 +1357,11 @@ fn main() -> Result<()> {
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
@ -1389,11 +1393,13 @@ fn main() -> Result<()> {
}
// Print pagination information
println!("\nPage {} of {} (Total: {} packages)",
println!(
"\nPage {} of {} (Total: {} packages)",
paginated_result.page,
paginated_result.total_pages,
paginated_result.total_count);
},
paginated_result.total_count
);
}
"json" => {
// Create a JSON representation of the obsoleted packages with pagination info
#[derive(Serialize)]
@ -1405,7 +1411,11 @@ fn main() -> Result<()> {
total_count: usize,
}
let packages_str: Vec<String> = paginated_result.packages.iter().map(|f| f.to_string()).collect();
let packages_str: Vec<String> = paginated_result
.packages
.iter()
.map(|f| f.to_string())
.collect();
let paginated_output = PaginatedOutput {
packages: packages_str,
page: paginated_result.page,
@ -1419,7 +1429,7 @@ fn main() -> Result<()> {
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output);
},
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
@ -1436,20 +1446,17 @@ fn main() -> Result<()> {
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{}",
println!(
"\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}",
paginated_result.page,
paginated_result.total_pages,
paginated_result.total_count);
},
paginated_result.total_count
);
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
@ -1458,7 +1465,7 @@ fn main() -> Result<()> {
}
Ok(())
},
}
Commands::ShowObsoleted {
repo_uri_or_path,
@ -1513,7 +1520,7 @@ fn main() -> Result<()> {
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 {
@ -1531,7 +1538,7 @@ fn main() -> Result<()> {
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output);
},
}
"tsv" => {
println!("FMRI\t{}", metadata.fmri);
println!("Status\t{}", metadata.status);
@ -1549,7 +1556,7 @@ fn main() -> Result<()> {
println!("MetadataVersion\t{}", metadata.metadata_version);
println!("ContentHash\t{}", metadata.content_hash);
},
}
_ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(),
@ -1558,7 +1565,7 @@ fn main() -> Result<()> {
}
Ok(())
},
}
Commands::SearchObsoleted {
repo_uri_or_path,
@ -1568,7 +1575,10 @@ 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)?;
@ -1579,7 +1589,8 @@ fn main() -> Result<()> {
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 {
@ -1616,10 +1627,11 @@ fn main() -> Result<()> {
publisher_str
);
}
},
}
"json" => {
// Create a JSON representation of the obsoleted packages
let packages_str: Vec<String> = obsoleted_packages.iter().map(|f| f.to_string()).collect();
let packages_str: Vec<String> =
obsoleted_packages.iter().map(|f| f.to_string()).collect();
let packages_output = ObsoletedPackagesOutput {
packages: packages_str,
};
@ -1629,7 +1641,7 @@ fn main() -> Result<()> {
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output);
},
}
"tsv" => {
// Print headers if not omitted
if !omit_headers {
@ -1646,14 +1658,9 @@ fn main() -> Result<()> {
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(),
@ -1662,7 +1669,7 @@ fn main() -> Result<()> {
}
Ok(())
},
}
Commands::RestoreObsoleted {
repo_uri_or_path,
@ -1670,7 +1677,10 @@ fn main() -> Result<()> {
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)?;
@ -1710,7 +1720,7 @@ fn main() -> Result<()> {
info!("Package restored successfully: {}", parsed_fmri);
Ok(())
},
}
Commands::ExportObsoleted {
repo_uri_or_path,
@ -1739,7 +1749,7 @@ fn main() -> Result<()> {
info!("Exported {} obsoleted packages to {}", count, output_file);
Ok(())
},
}
Commands::ImportObsoleted {
repo_uri_or_path,
@ -1758,15 +1768,12 @@ fn main() -> Result<()> {
// 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,
@ -1775,11 +1782,15 @@ 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
@ -1791,11 +1802,8 @@ fn main() -> Result<()> {
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 {

View file

@ -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
@ -349,8 +350,10 @@ 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))
}
@ -439,8 +442,8 @@ 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
)?;
@ -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

View file

@ -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<Item = &String> {
@ -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<String> = 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<String>, edges: Vec<String> }
struct JsonCycle {
nodes: Vec<String>,
edges: Vec<String>,
}
#[derive(Serialize)]
struct Payload { edges: Vec<JsonEdge>, cycles: Vec<JsonCycle> }
struct Payload {
edges: Vec<JsonEdge>,
cycles: Vec<JsonCycle>,
}
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<String>, // 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<String, Vec<(String, libips::fmri::Fmri)>>, // stem -> [(publisher, fmri)]
manifest_cache: HashMap<String, libips::actions::Manifest>, // fmri string -> manifest
lock_cache: HashMap<String, Option<String>>, // stem -> release lock
candidate_cache: HashMap<(String, Option<String>, Option<String>, Option<String>), Option<libips::fmri::Fmri>>, // (stem, rel, branch, publisher)
candidate_cache: HashMap<
(String, Option<String>, Option<String>, Option<String>),
Option<libips::fmri::Fmri>,
>, // (stem, rel, branch, publisher)
}
impl AdviceContext {
fn new(publisher: Option<String>, 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<AdviceIssue> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let mut path: Vec<String> = 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() { "<none>".to_string() } else { constraint_str },
if constraint_str.is_empty() {
"<none>".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<String>,
issues: &mut Vec<AdviceIssue>,
) -> 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<String> = 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<Option<String>> {
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<String>,
_seen: &mut HashSet<String>,
) {
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<Cycle>) -> Vec<Cycle> {
}
// 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<Cycle>) -> Vec<Cycle> {
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<String, Vec<(String, Option<String>)>> = 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<String> = 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<String> = 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<String> }
struct DanglingJson {
package_fmri: String,
missing_stems: Vec<String>,
}
let mut out: Vec<DanglingJson> = 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<String> = 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() { "<none>".to_string() } else { constraint_str },
if constraint_str.is_empty() {
"<none>".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<String> = None;
let mut branch: Option<String> = 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());
}

View file

@ -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;

View file

@ -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;

View file

@ -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()
)
),
}
}
}

View file

@ -1,4 +1,4 @@
use anyhow::{bail, Result};
use anyhow::{Result, bail};
use semver::Version;
use std::collections::HashMap;
use url::Url;

View file

@ -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<crate::Rule>, 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<crate::Rule>, 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);