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 thiserror::Error;
use tracing::info; use tracing::info;
use crate::actions::{Link as LinkAction, Manifest};
use crate::actions::{Dir as DirAction, File as FileAction}; use crate::actions::{Dir as DirAction, File as FileAction};
use crate::actions::{Link as LinkAction, Manifest};
#[derive(Error, Debug, Diagnostic)] #[derive(Error, Debug, Diagnostic)]
pub enum InstallerError { pub enum InstallerError {
@ -23,16 +23,25 @@ pub enum InstallerError {
}, },
#[error("Absolute paths are forbidden in actions: {path}")] #[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 }, AbsolutePathForbidden { path: String },
#[error("Path escapes image root via traversal: {rel}")] #[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 }, PathTraversalOutsideImage { rel: String },
#[error("Unsupported or not yet implemented action: {action} ({reason})")] #[error("Unsupported or not yet implemented action: {action} ({reason})")]
#[diagnostic(code(ips::installer_error::unsupported_action))] #[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 { 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 => { Component::Prefix(_) | Component::RootDir => {
return Err(InstallerError::AbsolutePathForbidden { return Err(InstallerError::AbsolutePathForbidden {
path: rel.to_string(), 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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ApplyOptions") f.debug_struct("ApplyOptions")
.field("dry_run", &self.dry_run) .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) .field("progress_interval", &self.progress_interval)
.finish() .finish()
} }
@ -115,65 +127,154 @@ impl std::fmt::Debug for ApplyOptions {
impl Default for ApplyOptions { impl Default for ApplyOptions {
fn default() -> Self { 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. /// Progress event emitted by apply_manifest when a callback is provided.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum ProgressEvent { pub enum ProgressEvent {
StartingPhase { phase: &'static str, total: usize }, StartingPhase {
Progress { phase: &'static str, current: usize, total: usize }, phase: &'static str,
FinishedPhase { phase: &'static str, total: usize }, 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>; pub type ProgressCallback = Arc<dyn Fn(ProgressEvent) + Send + Sync + 'static>;
/// Apply a manifest to the filesystem rooted at image_root. /// 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). /// 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>| { 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 // Directories first
let total_dirs = manifest.directories.len(); 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; let mut i = 0usize;
for d in &manifest.directories { for d in &manifest.directories {
apply_dir(image_root, d, opts)?; apply_dir(image_root, d, opts)?;
i += 1; i += 1;
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_dirs) { 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 // Files next
let total_files = manifest.files.len(); 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; i = 0;
for f_action in &manifest.files { for f_action in &manifest.files {
apply_file(image_root, f_action, opts)?; apply_file(image_root, f_action, opts)?;
i += 1; i += 1;
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_files) { 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 // Links
let total_links = manifest.links.len(); 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; i = 0;
for l in &manifest.links { for l in &manifest.links {
apply_link(image_root, l, opts)?; apply_link(image_root, l, opts)?;
i += 1; i += 1;
if opts.progress_interval > 0 && (i % opts.progress_interval == 0 || i == total_links) { 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. // Other action kinds are ignored for now and left for future extension.
Ok(()) Ok(())
@ -216,7 +317,11 @@ fn ensure_parent(image_root: &Path, p: &str, opts: &ApplyOptions) -> Result<(),
Ok(()) 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)?; let full = safe_join(image_root, &f.path)?;
// Ensure parent exists (directories should already be applied, but be robust) // 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(()) 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)?; let link_path = safe_join(image_root, &l.path)?;
// Determine link type (default to symlink). If properties contain type=hard, create hard link. // 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 { } else {
// Symlink: require non-absolute target to avoid embedding full host paths // Symlink: require non-absolute target to avoid embedding full host paths
if Path::new(&l.target).is_absolute() { 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) // Create relative symlink as provided (do not convert to absolute to avoid embedding full paths)
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]

View file

@ -898,7 +898,10 @@ impl Manifest {
match serde_json::from_str::<Manifest>(&content) { match serde_json::from_str::<Manifest>(&content) {
Ok(manifest) => Ok(manifest), Ok(manifest) => Ok(manifest),
Err(err) => { 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 // If JSON parsing fails, fall back to string format
Manifest::parse_string(content) Manifest::parse_string(content)
} }
@ -933,17 +936,24 @@ impl Manifest {
property.key = prop.as_str().to_owned(); property.key = prop.as_str().to_owned();
} }
Rule::property_value => { Rule::property_value => {
let str_val: String = prop.as_str().to_owned(); let str_val: String =
property.value = str_val prop.as_str().to_owned();
.replace(['\"', '\\'], ""); 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); act.properties.push(property);
} }
Rule::EOI => (), 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); m.add_action(act);

View file

@ -58,12 +58,17 @@ use walkdir::WalkDir;
pub use crate::actions::Manifest; pub use crate::actions::Manifest;
// Core typed 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::depend::{FileDep, GenerateOptions as DependGenerateOptions};
pub use crate::fmri::Fmri; pub use crate::fmri::Fmri;
// For BaseMeta // For BaseMeta
use crate::repository::file_backend::{FileBackend, Transaction}; use crate::repository::file_backend::{FileBackend, Transaction};
use crate::repository::{ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository}; use crate::repository::{
ReadableRepository, RepositoryError, RepositoryVersion, WritableRepository,
};
use crate::transformer; use crate::transformer;
pub use crate::transformer::TransformRule; pub use crate::transformer::TransformRule;
@ -87,7 +92,10 @@ pub enum IpsError {
Io(String), Io(String),
#[error("Unimplemented feature: {feature}")] #[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 }, Unimplemented { feature: &'static str },
} }
@ -183,19 +191,32 @@ impl ManifestBuilder {
let mut props = std::collections::HashMap::new(); let mut props = std::collections::HashMap::new();
props.insert( props.insert(
"path".to_string(), "path".to_string(),
Property { key: "path".to_string(), value: path.to_string() }, Property {
key: "path".to_string(),
value: path.to_string(),
},
); );
props.insert( props.insert(
"license".to_string(), "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 self
} }
/// Add a link action /// Add a link action
pub fn add_link(&mut self, path: &str, target: &str) -> &mut Self { 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 self
} }
@ -211,7 +232,9 @@ impl ManifestBuilder {
} }
/// Start a new empty builder /// Start a new empty builder
pub fn new() -> Self { pub fn new() -> Self {
Self { manifest: Manifest::new() } Self {
manifest: Manifest::new(),
}
} }
/// Convenience: construct a Manifest directly by scanning a prototype directory. /// Convenience: construct a Manifest directly by scanning a prototype directory.
@ -223,9 +246,9 @@ impl ManifestBuilder {
proto.display() proto.display()
))); )));
} }
let root = proto let root = proto.canonicalize().map_err(|e| {
.canonicalize() IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e))
.map_err(|e| IpsError::Io(format!("failed to canonicalize {}: {}", proto.display(), e)))?; })?;
let mut m = Manifest::new(); let mut m = Manifest::new();
for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) { 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 { if let Some(fmri) = meta.fmri {
push_attr("pkg.fmri", fmri.to_string()); push_attr("pkg.fmri", fmri.to_string());
} }
if let Some(s) = meta.summary { push_attr("pkg.summary", s); } if let Some(s) = meta.summary {
if let Some(c) = meta.classification { push_attr("info.classification", c); } push_attr("pkg.summary", s);
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(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 { if let Some(l) = meta.license {
// Represent base license via an attribute named 'license'; callers may add dedicated license actions separately // Represent base license via an attribute named 'license'; callers may add dedicated license actions separately
self.manifest.attributes.push(Attr { self.manifest.attributes.push(Attr {
@ -310,12 +341,16 @@ impl Repository {
pub fn open(path: &Path) -> Result<Self, IpsError> { pub fn open(path: &Path) -> Result<Self, IpsError> {
// Validate by opening backend // Validate by opening backend
let _ = FileBackend::open(path)?; 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> { pub fn create(path: &Path) -> Result<Self, IpsError> {
let _ = FileBackend::create(path, RepositoryVersion::default())?; 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> { pub fn has_publisher(&self, name: &str) -> Result<bool, IpsError> {
@ -330,7 +365,9 @@ impl Repository {
Ok(()) Ok(())
} }
pub fn path(&self) -> &Path { &self.path } pub fn path(&self) -> &Path {
&self.path
}
} }
/// High-level publishing client for starting repository transactions. /// High-level publishing client for starting repository transactions.
@ -352,14 +389,21 @@ pub struct PublisherClient {
impl PublisherClient { impl PublisherClient {
pub fn new(repo: Repository, publisher: impl Into<String>) -> Self { pub fn new(repo: Repository, publisher: impl Into<String>) -> Self {
Self { repo, publisher: publisher.into() } Self {
repo,
publisher: publisher.into(),
}
} }
/// Begin a new transaction /// Begin a new transaction
pub fn begin(&self) -> Result<Txn, IpsError> { pub fn begin(&self) -> Result<Txn, IpsError> {
let backend = FileBackend::open(self.repo.path())?; let backend = FileBackend::open(self.repo.path())?;
let tx = backend.begin_transaction()?; // returns Transaction bound to 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 { impl Txn {
/// Add all files from the given payload/prototype directory /// Add all files from the given payload/prototype directory
pub fn add_payload_dir(&mut self, dir: &Path) -> Result<(), IpsError> { pub fn add_payload_dir(&mut self, dir: &Path) -> Result<(), IpsError> {
let root = dir let root = dir.canonicalize().map_err(|e| {
.canonicalize() IpsError::Io(format!("failed to canonicalize {}: {}", dir.display(), e))
.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()) { for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) {
let p = entry.path(); let p = entry.path();
if p.is_file() { if p.is_file() {
@ -451,7 +495,11 @@ pub struct DependencyGenerator;
impl DependencyGenerator { impl DependencyGenerator {
/// Compute file-level dependencies for the given manifest, using `proto` as base for local file resolution. /// 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. /// 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() { if opts.proto_dir.is_none() {
opts.proto_dir = Some(proto.to_path_buf()); 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 /// Intentionally not implemented in this facade: mapping raw file dependencies to package FMRIs
/// requires repository/catalog context. Call `generate_with_repo` instead. /// requires repository/catalog context. Call `generate_with_repo` instead.
pub fn generate(_proto: &Path, _manifest: &Manifest) -> Result<Manifest, IpsError> { 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. /// Generate dependencies using a repository to resolve file-level deps into package FMRIs.
@ -562,10 +612,8 @@ impl Resolver {
if f.version.is_none() { if f.version.is_none() {
// Query repository for this package name // Query repository for this package name
let pkgs = repo.list_packages(publisher, Some(&f.name))?; let pkgs = repo.list_packages(publisher, Some(&f.name))?;
let matches: Vec<&crate::repository::PackageInfo> = pkgs let matches: Vec<&crate::repository::PackageInfo> =
.iter() pkgs.iter().filter(|pi| pi.fmri.name == f.name).collect();
.filter(|pi| pi.fmri.name == f.name)
.collect();
if matches.len() == 1 { if matches.len() == 1 {
let fmri = &matches[0].fmri; let fmri = &matches[0].fmri;
if f.publisher.is_none() { if f.publisher.is_none() {
@ -597,7 +645,6 @@ fn manifest_fmri(manifest: &Manifest) -> Option<Fmri> {
None None
} }
/// Lint facade providing a typed, extensible rule engine with enable/disable controls. /// Lint facade providing a typed, extensible rule engine with enable/disable controls.
/// ///
/// Configure which rules to run, override severities, and pass rule-specific parameters. /// 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 reference_repos: Vec<PathBuf>,
pub rulesets: Vec<String>, pub rulesets: Vec<String>,
// Rule configurability // Rule configurability
pub disabled_rules: Vec<String>, // rule IDs to disable pub disabled_rules: Vec<String>, // rule IDs to disable
pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run pub enabled_only: Option<Vec<String>>, // if Some, only these rule IDs run
pub severity_overrides: std::collections::HashMap<String, lint::LintSeverity>, 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) pub rule_params: std::collections::HashMap<String, std::collections::HashMap<String, String>>, // rule_id -> (key->val)
} }
@ -638,31 +685,48 @@ pub mod lint {
#[derive(Debug, Error, Diagnostic)] #[derive(Debug, Error, Diagnostic)]
pub enum LintIssue { pub enum LintIssue {
#[error("Manifest is missing pkg.fmri or it is invalid")] #[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, MissingOrInvalidFmri,
#[error("Manifest has multiple pkg.fmri attributes")] #[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, DuplicateFmri,
#[error("Manifest is missing pkg.summary")] #[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, MissingSummary,
#[error("Dependency is missing FMRI or name")] #[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, DependencyMissingFmri,
#[error("Dependency type is missing")] #[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, DependencyMissingType,
} }
pub trait LintRule { pub trait LintRule {
fn id(&self) -> &'static str; fn id(&self) -> &'static str;
fn description(&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. /// 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. /// The config carries enable/disable lists, severity overrides and rule-specific parameters for extensibility.
fn check(&self, manifest: &Manifest, config: &LintConfig) -> Vec<miette::Report>; fn check(&self, manifest: &Manifest, config: &LintConfig) -> Vec<miette::Report>;
@ -670,8 +734,12 @@ pub mod lint {
struct RuleManifestFmri; struct RuleManifestFmri;
impl LintRule for RuleManifestFmri { impl LintRule for RuleManifestFmri {
fn id(&self) -> &'static str { "manifest.fmri" } fn id(&self) -> &'static str {
fn description(&self) -> &'static str { "Validate pkg.fmri presence/uniqueness/parse" } "manifest.fmri"
}
fn description(&self) -> &'static str {
"Validate pkg.fmri presence/uniqueness/parse"
}
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> { fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
let mut diags = Vec::new(); let mut diags = Vec::new();
let mut fmri_attr_count = 0usize; let mut fmri_attr_count = 0usize;
@ -679,13 +747,21 @@ pub mod lint {
for attr in &manifest.attributes { for attr in &manifest.attributes {
if attr.key == "pkg.fmri" { if attr.key == "pkg.fmri" {
fmri_attr_count += 1; 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) { match (fmri_attr_count, fmri_text) {
(0, _) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)), (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)), (_, None) => diags.push(miette::Report::new(LintIssue::MissingOrInvalidFmri)),
} }
diags diags
@ -694,29 +770,47 @@ pub mod lint {
struct RuleManifestSummary; struct RuleManifestSummary;
impl LintRule for RuleManifestSummary { impl LintRule for RuleManifestSummary {
fn id(&self) -> &'static str { "manifest.summary" } fn id(&self) -> &'static str {
fn description(&self) -> &'static str { "Validate pkg.summary presence" } "manifest.summary"
}
fn description(&self) -> &'static str {
"Validate pkg.summary presence"
}
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> { fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
let mut diags = Vec::new(); let mut diags = Vec::new();
let has_summary = manifest let has_summary = manifest
.attributes .attributes
.iter() .iter()
.any(|a| a.key == "pkg.summary" && a.values.iter().any(|v| !v.trim().is_empty())); .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 diags
} }
} }
struct RuleDependencyFields; struct RuleDependencyFields;
impl LintRule for RuleDependencyFields { impl LintRule for RuleDependencyFields {
fn id(&self) -> &'static str { "depend.fields" } fn id(&self) -> &'static str {
fn description(&self) -> &'static str { "Validate basic dependency fields" } "depend.fields"
}
fn description(&self) -> &'static str {
"Validate basic dependency fields"
}
fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> { fn check(&self, manifest: &Manifest, _config: &LintConfig) -> Vec<miette::Report> {
let mut diags = Vec::new(); let mut diags = Vec::new();
for dep in &manifest.dependencies { for dep in &manifest.dependencies {
let fmri_ok = dep.fmri.as_ref().map(|f| !f.name.trim().is_empty()).unwrap_or(false); let fmri_ok = dep
if !fmri_ok { diags.push(miette::Report::new(LintIssue::DependencyMissingFmri)); } .fmri
if dep.dependency_type.trim().is_empty() { diags.push(miette::Report::new(LintIssue::DependencyMissingType)); } .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 diags
} }
@ -735,7 +829,8 @@ pub mod lint {
let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect(); let set: std::collections::HashSet<&str> = only.iter().map(|s| s.as_str()).collect();
return set.contains(rule_id); 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) !disabled.contains(rule_id)
} }
@ -751,7 +846,10 @@ pub mod lint {
/// assert!(diags.is_empty()); /// assert!(diags.is_empty());
/// # Ok::<(), ips::IpsError>(()) /// # 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(); let mut diags: Vec<miette::Report> = Vec::new();
for rule in default_rules().into_iter() { for rule in default_rules().into_iter() {
if rule_enabled(rule.id(), config) { if rule_enabled(rule.id(), config) {
@ -769,7 +867,11 @@ mod tests {
fn make_manifest_with_fmri(fmri_str: &str) -> Manifest { fn make_manifest_with_fmri(fmri_str: &str) -> Manifest {
let mut m = Manifest::new(); 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 m
} }
@ -799,14 +901,17 @@ mod tests {
let fmri = dep.fmri.as_ref().unwrap(); let fmri = dep.fmri.as_ref().unwrap();
assert_eq!(fmri.name, "pkgA"); assert_eq!(fmri.name, "pkgA");
assert_eq!(fmri.publisher.as_deref(), Some("pub")); 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"); assert_eq!(fmri.version.as_ref().unwrap().to_string(), "1.0");
} }
#[test] #[test]
fn resolver_uses_repository_for_provider() { fn resolver_uses_repository_for_provider() {
use crate::repository::file_backend::FileBackend;
use crate::repository::RepositoryVersion; use crate::repository::RepositoryVersion;
use crate::repository::file_backend::FileBackend;
// Create a temporary repository and add a publisher // Create a temporary repository and add a publisher
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@ -816,7 +921,11 @@ mod tests {
// Publish provider package pkgA@1.0 // Publish provider package pkgA@1.0
let mut provider = Manifest::new(); 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(); let mut tx = backend.begin_transaction().unwrap();
tx.update_manifest(provider); tx.update_manifest(provider);
tx.set_publisher("pub"); tx.set_publisher("pub");
@ -854,8 +963,16 @@ mod tests {
#[test] #[test]
fn lint_accepts_valid_manifest() { fn lint_accepts_valid_manifest() {
let mut m = Manifest::new(); 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 {
m.attributes.push(Attr { key: "pkg.summary".into(), values: vec!["A package".to_string()], properties: Default::default() }); 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 cfg = LintConfig::default();
let diags = lint::lint_manifest(&m, &cfg).unwrap(); let diags = lint::lint_manifest(&m, &cfg).unwrap();
assert!(diags.is_empty(), "unexpected diags: {:?}", diags); assert!(diags.is_empty(), "unexpected diags: {:?}", diags);
@ -865,14 +982,22 @@ mod tests {
fn lint_disable_summary_rule() { fn lint_disable_summary_rule() {
// Manifest with valid fmri but missing summary // Manifest with valid fmri but missing summary
let mut m = Manifest::new(); 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 // Disable the summary rule; expect no diagnostics
let mut cfg = LintConfig::default(); let mut cfg = LintConfig::default();
cfg.disabled_rules = vec!["manifest.summary".to_string()]; cfg.disabled_rules = vec!["manifest.summary".to_string()];
let diags = lint::lint_manifest(&m, &cfg).unwrap(); let diags = lint::lint_manifest(&m, &cfg).unwrap();
// fmri is valid, dependencies empty, summary rule disabled => no diags // 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] #[test]
@ -889,14 +1014,29 @@ mod tests {
let m = b.build(); let m = b.build();
// Validate attributes include fmri and summary // 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| {
assert!(m.attributes.iter().any(|a| a.key == "pkg.summary" && a.values.get(0).map(|v| v == "Summary").unwrap_or(false))); 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 // Validate license
assert_eq!(m.licenses.len(), 1); assert_eq!(m.licenses.len(), 1);
let lic = &m.licenses[0]; let lic = &m.licenses[0];
assert_eq!(lic.properties.get("path").map(|p| p.value.as_str()), Some("LICENSE")); assert_eq!(
assert_eq!(lic.properties.get("license").map(|p| p.value.as_str()), Some("MIT")); 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 // Validate link
assert_eq!(m.links.len(), 1); assert_eq!(m.links.len(), 1);

View file

@ -8,9 +8,9 @@ use miette::Diagnostic;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::error::Error as StdError;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::error::Error as StdError;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, warn}; use tracing::{debug, warn};
@ -27,10 +27,16 @@ pub struct DependError {
impl DependError { impl DependError {
fn new(message: impl Into<String>) -> Self { 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 { 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). /// 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 mut out = Vec::new();
let bypass = compile_bypass(&opts.bypass_patterns)?; let bypass = compile_bypass(&opts.bypass_patterns)?;
for f in &manifest.files { for f in &manifest.files {
// Determine installed path (manifests typically do not start with '/'). // 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) { 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; continue;
} }
@ -142,16 +158,28 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
// Normalize /bin -> /usr/bin // Normalize /bin -> /usr/bin
let interp_path = normalize_bin_path(&interp); let interp_path = normalize_bin_path(&interp);
if !interp_path.starts_with('/') { 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 { } else {
// Derive dir and base name // Derive dir and base name
let (dir, base) = split_dir_base(&interp_path); let (dir, base) = split_dir_base(&interp_path);
if let Some(dir) = dir { 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 Python interpreter, perform Python analysis
if interp_path.contains("python") { if interp_path.contains("python") {
if let Some((maj, min)) = infer_python_version_from_paths(&installed_path, Some(&interp_path)) { if let Some((maj, min)) =
let mut pydeps = process_python(&bytes, &installed_path, (maj, min), opts); 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); out.append(&mut pydeps);
} }
} }
@ -171,7 +199,13 @@ pub fn generate_file_dependencies_from_manifest(manifest: &Manifest, opts: &Gene
if exec_path.starts_with('/') { if exec_path.starts_with('/') {
let (dir, base) = split_dir_base(&exec_path); let (dir, base) = split_dir_base(&exec_path);
if let Some(dir) = dir { 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 /// 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 out = Vec::new();
let mut token_count = 0; let mut token_count = 0;
for p in provided { for p in provided {
if p == PD_DEFAULT_RUNPATH { if p == PD_DEFAULT_RUNPATH {
token_count += 1; token_count += 1;
if 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); out.extend_from_slice(defaults);
} else { } else {
@ -208,7 +247,9 @@ fn insert_default_runpath(defaults: &[String], provided: &[String]) -> std::resu
fn compile_bypass(patterns: &[String]) -> Result<Vec<Regex>> { fn compile_bypass(patterns: &[String]) -> Result<Vec<Regex>> {
let mut out = Vec::new(); let mut out = Vec::new();
for p in patterns { 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) Ok(out)
} }
@ -259,11 +300,18 @@ fn process_elf(bytes: &[u8], installed_path: &str, opts: &GenerateOptions) -> Ve
} }
} else { } else {
// If no override, prefer DT_RUNPATH if present else defaults // 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 // 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 let expanded: Vec<String> = effective
.into_iter() .into_iter()
.map(|p| p.replace("$ORIGIN", &origin)) .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 // Emit FileDep for each DT_NEEDED base name
for bn in needed.drain(..) { 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), 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 { for fd in file_deps {
match &fd.kind { match &fd.kind {
FileDepKind::Elf { base_name, run_paths, .. } => { FileDepKind::Elf {
base_name,
run_paths,
..
} => {
let mut providers: Vec<Fmri> = Vec::new(); let mut providers: Vec<Fmri> = Vec::new();
for dir in run_paths { for dir in run_paths {
let full = normalize_join(dir, base_name); 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 // 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(); let mut providers: Vec<Fmri> = Vec::new();
for dir in run_paths { for dir in run_paths {
let full = normalize_join(dir, base_name); let full = normalize_join(dir, base_name);
@ -366,7 +428,11 @@ pub fn resolve_dependencies<R: ReadableRepository>(
} else { } else {
} }
} }
FileDepKind::Python { base_names, run_paths, .. } => { FileDepKind::Python {
base_names,
run_paths,
..
} => {
let mut providers: Vec<Fmri> = Vec::new(); let mut providers: Vec<Fmri> = Vec::new();
for dir in run_paths { for dir in run_paths {
for base in base_names { 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) // Ask repo to show contents for all packages (files only)
let contents = repo let contents = repo
.show_contents(publisher, None, Some(&["file".to_string()])) .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>() { let fmri = match pc.package_id.parse::<Fmri>() {
Ok(f) => f, Ok(f) => f,
Err(e) => { Err(e) => {
warn!("Skipping package with invalid FMRI {}: {}", pc.package_id, e); warn!(
"Skipping package with invalid FMRI {}: {}",
pc.package_id, e
);
continue; continue;
} }
}; };
if let Some(files) = pc.files { if let Some(files) = pc.files {
for p in files { for p in files {
// Ensure leading slash // 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()); 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) Ok(map)
} }
// --- Helpers for script processing --- // --- Helpers for script processing ---
fn parse_shebang(bytes: &[u8]) -> Option<String> { fn parse_shebang(bytes: &[u8]) -> Option<String> {
if bytes.len() < 2 || bytes[0] != b'#' || bytes[1] != b'!' { 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 { fn looks_like_smf_manifest(bytes: &[u8]) -> bool {
// Very lightweight detection: SMF manifests are XML files with a <service_bundle ...> root // 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. // 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 --- // --- 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 // 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 Ok(re) = Regex::new(r"^/usr/lib/python(\d+)\.(\d+)(/|$)") {
if let Some(c) = re.captures(installed_path) { 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 Ok(re) = Regex::new(r"python(\d+)\.(\d+)") {
if let Some(c) = re.captures(sb) { if let Some(c) = re.captures(sb) {
if let (Some(ma), Some(mi)) = (c.get(1), c.get(2)) { 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)); return Some((maj, min));
} }
} }
@ -580,7 +659,12 @@ fn collect_python_imports(src: &str) -> Vec<String> {
mods 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 text = String::from_utf8_lossy(bytes);
let imports = collect_python_imports(&text); let imports = collect_python_imports(&text);
if imports.is_empty() { if imports.is_empty() {
@ -591,11 +675,21 @@ fn process_python(bytes: &[u8], installed_path: &str, version: (u8, u8), opts: &
for m in imports { for m in imports {
let py = format!("{}.py", m); let py = format!("{}.py", m);
let so = format!("{}.so", m); let so = format!("{}.so", m);
if !base_names.contains(&py) { base_names.push(py); } if !base_names.contains(&py) {
if !base_names.contains(&so) { base_names.push(so); } base_names.push(py);
}
if !base_names.contains(&so) {
base_names.push(so);
}
} }
let run_paths = compute_python_runpaths(version, opts); 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 --- // --- SMF helpers ---
@ -608,7 +702,9 @@ fn extract_smf_execs(bytes: &[u8]) -> Vec<String> {
let m = cap.get(1).or_else(|| cap.get(2)); let m = cap.get(1).or_else(|| cap.get(2));
if let Some(v) = m { if let Some(v) = m {
let val = v.as_str().to_string(); 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 => { x => {
return Err(DigestError::UnknownAlgorithm { return Err(DigestError::UnknownAlgorithm {
algorithm: x.to_string(), algorithm: x.to_string(),
}) });
} }
}; };
@ -152,7 +152,9 @@ pub enum DigestError {
#[error("hashing algorithm {algorithm:?} is not known by this library")] #[error("hashing algorithm {algorithm:?} is not known by this library")]
#[diagnostic( #[diagnostic(
code(ips::digest_error::unknown_algorithm), 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 }, UnknownAlgorithm { algorithm: String },

View file

@ -1,7 +1,7 @@
use std::path::Path; use std::path::Path;
use crate::actions::executors::{apply_manifest, ApplyOptions, InstallerError};
use crate::actions::Manifest; use crate::actions::Manifest;
use crate::actions::executors::{ApplyOptions, InstallerError, apply_manifest};
use crate::solver::InstallPlan; use crate::solver::InstallPlan;
/// ActionPlan represents a merged list of actions across all manifests /// ActionPlan represents a merged list of actions across all manifests
@ -50,12 +50,20 @@ mod tests {
#[test] #[test]
fn build_and_apply_empty_plan_dry_run() { fn build_and_apply_empty_plan_dry_run() {
// Empty install plan should produce empty action plan and apply should be no-op. // 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); let ap = ActionPlan::from_install_plan(&plan);
assert!(ap.manifest.directories.is_empty()); assert!(ap.manifest.directories.is_empty());
assert!(ap.manifest.files.is_empty()); assert!(ap.manifest.files.is_empty());
assert!(ap.manifest.links.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"); 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. // Even if root doesn't exist, dry_run should not perform any IO and succeed.
let res = ap.apply(root, &opts); let res = ap.apply(root, &opts);

File diff suppressed because it is too large Load diff

View file

@ -46,7 +46,7 @@ pub type Result<T> = std::result::Result<T, InstalledError>;
pub struct InstalledPackageInfo { pub struct InstalledPackageInfo {
/// The FMRI of the package /// The FMRI of the package
pub fmri: Fmri, pub fmri: Fmri,
/// The publisher of the package /// The publisher of the package
pub publisher: String, pub publisher: String,
} }
@ -68,48 +68,58 @@ impl InstalledPackages {
// To fix this issue, we use block scopes {} around table operations to ensure that the table // To fix this issue, we use block scopes {} around table operations to ensure that the table
// objects are dropped (and their borrows released) before committing the transaction. // objects are dropped (and their borrows released) before committing the transaction.
// This pattern is used in all methods that commit transactions after table operations. // This pattern is used in all methods that commit transactions after table operations.
/// Create a new installed packages database /// Create a new installed packages database
pub fn new<P: AsRef<Path>>(db_path: P) -> Self { pub fn new<P: AsRef<Path>>(db_path: P) -> Self {
InstalledPackages { InstalledPackages {
db_path: db_path.as_ref().to_path_buf(), db_path: db_path.as_ref().to_path_buf(),
} }
} }
/// Dump the contents of the installed table to stdout for debugging /// Dump the contents of the installed table to stdout for debugging
pub fn dump_installed_table(&self) -> Result<()> { pub fn dump_installed_table(&self) -> Result<()> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction // 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)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Open the installed table // Open the installed table
match tx.open_table(INSTALLED_TABLE) { match tx.open_table(INSTALLED_TABLE) {
Ok(table) => { Ok(table) => {
let mut count = 0; let mut count = 0;
for entry_result in table.iter().map_err(|e| InstalledError::Database(format!("Failed to iterate installed table: {}", e)))? { for entry_result in table.iter().map_err(|e| {
let (key, value) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", 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(); let key_str = key.value();
// Try to deserialize the manifest // Try to deserialize the manifest
match serde_json::from_slice::<Manifest>(value.value()) { match serde_json::from_slice::<Manifest>(value.value()) {
Ok(manifest) => { Ok(manifest) => {
// Extract the publisher from the FMRI attribute // Extract the publisher from the FMRI attribute
let publisher = manifest.attributes.iter() let publisher = manifest
.attributes
.iter()
.find(|attr| attr.key == "pkg.fmri") .find(|attr| attr.key == "pkg.fmri")
.and_then(|attr| attr.values.get(0).cloned()) .and_then(|attr| attr.values.get(0).cloned())
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
println!("Key: {}", key_str); println!("Key: {}", key_str);
println!(" FMRI: {}", publisher); println!(" FMRI: {}", publisher);
println!(" Attributes: {}", manifest.attributes.len()); println!(" Attributes: {}", manifest.attributes.len());
println!(" Files: {}", manifest.files.len()); println!(" Files: {}", manifest.files.len());
println!(" Directories: {}", manifest.directories.len()); println!(" Directories: {}", manifest.directories.len());
println!(" Dependencies: {}", manifest.dependencies.len()); println!(" Dependencies: {}", manifest.dependencies.len());
}, }
Err(e) => { Err(e) => {
println!("Key: {}", key_str); println!("Key: {}", key_str);
println!(" Error deserializing manifest: {}", e); println!(" Error deserializing manifest: {}", e);
@ -119,214 +129,252 @@ impl InstalledPackages {
} }
println!("Total entries in installed table: {}", count); println!("Total entries in installed table: {}", count);
Ok(()) Ok(())
}, }
Err(e) => { Err(e) => {
println!("Error opening installed table: {}", e); println!("Error opening installed table: {}", e);
Err(InstalledError::Database(format!("Failed to open installed table: {}", e))) Err(InstalledError::Database(format!(
"Failed to open installed table: {}",
e
)))
} }
} }
} }
/// Get database statistics /// Get database statistics
pub fn get_db_stats(&self) -> Result<()> { pub fn get_db_stats(&self) -> Result<()> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction // 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)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Get table statistics // Get table statistics
let mut installed_count = 0; let mut installed_count = 0;
// Count installed entries // Count installed entries
if let Ok(table) = tx.open_table(INSTALLED_TABLE) { 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)))? { for result in table.iter().map_err(|e| {
let _ = result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", 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; installed_count += 1;
} }
} }
// Print statistics // Print statistics
println!("Database path: {}", self.db_path.display()); println!("Database path: {}", self.db_path.display());
println!("Table statistics:"); println!("Table statistics:");
println!(" Installed table: {} entries", installed_count); println!(" Installed table: {} entries", installed_count);
println!("Total entries: {}", installed_count); println!("Total entries: {}", installed_count);
Ok(()) Ok(())
} }
/// Initialize the installed packages database /// Initialize the installed packages database
pub fn init_db(&self) -> Result<()> { pub fn init_db(&self) -> Result<()> {
// Create a parent directory if it doesn't exist // Create a parent directory if it doesn't exist
if let Some(parent) = self.db_path.parent() { if let Some(parent) = self.db_path.parent() {
fs::create_dir_all(parent)?; fs::create_dir_all(parent)?;
} }
// Open or create the database // Open or create the database
let db = Database::create(&self.db_path) let db = Database::create(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to create database: {}", e)))?;
// Create tables // Create tables
let tx = db.begin_write() let tx = db
.begin_write()
.map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
tx.open_table(INSTALLED_TABLE) tx.open_table(INSTALLED_TABLE).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to create installed table: {}", 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(()) Ok(())
} }
/// Add a package to the installed packages database /// Add a package to the installed packages database
pub fn add_package(&self, fmri: &Fmri, manifest: &Manifest) -> Result<()> { pub fn add_package(&self, fmri: &Fmri, manifest: &Manifest) -> Result<()> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a writing transaction // 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)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher) // Create the key (full FMRI including publisher)
let key = fmri.to_string(); let key = fmri.to_string();
// Serialize the manifest // Serialize the manifest
let manifest_bytes = serde_json::to_vec(manifest)?; let manifest_bytes = serde_json::to_vec(manifest)?;
// Use a block scope to ensure the table is dropped before committing the transaction // Use a block scope to ensure the table is dropped before committing the transaction
{ {
// Open the installed table // Open the installed table
let mut installed_table = tx.open_table(INSTALLED_TABLE) let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; InstalledError::Database(format!("Failed to open installed table: {}", e))
})?;
// Insert the package into the installed table // Insert the package into the installed table
installed_table.insert(key.as_str(), manifest_bytes.as_slice()) installed_table
.map_err(|e| InstalledError::Database(format!("Failed to insert into installed table: {}", e)))?; .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 // The table is dropped at the end of this block, releasing its borrow of tx
} }
// Commit the transaction // Commit the transaction
tx.commit() tx.commit().map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?; InstalledError::Database(format!("Failed to commit transaction: {}", e))
})?;
info!("Added package to installed database: {}", key); info!("Added package to installed database: {}", key);
Ok(()) Ok(())
} }
/// Remove a package from the installed packages database /// Remove a package from the installed packages database
pub fn remove_package(&self, fmri: &Fmri) -> Result<()> { pub fn remove_package(&self, fmri: &Fmri) -> Result<()> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a writing transaction // 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)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher) // Create the key (full FMRI including publisher)
let key = fmri.to_string(); let key = fmri.to_string();
// Use a block scope to ensure the table is dropped before committing the transaction // Use a block scope to ensure the table is dropped before committing the transaction
{ {
// Open the installed table // Open the installed table
let mut installed_table = tx.open_table(INSTALLED_TABLE) let mut installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; InstalledError::Database(format!("Failed to open installed table: {}", e))
})?;
// Check if the package exists // Check if the package exists
if let Ok(None) = installed_table.get(key.as_str()) { if let Ok(None) = installed_table.get(key.as_str()) {
return Err(InstalledError::PackageNotFound(key)); return Err(InstalledError::PackageNotFound(key));
} }
// Remove the package from the installed table // Remove the package from the installed table
installed_table.remove(key.as_str()) installed_table.remove(key.as_str()).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to remove from installed table: {}", 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 // The table is dropped at the end of this block, releasing its borrow of tx
} }
// Commit the transaction // Commit the transaction
tx.commit() tx.commit().map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to commit transaction: {}", e)))?; InstalledError::Database(format!("Failed to commit transaction: {}", e))
})?;
info!("Removed package from installed database: {}", key); info!("Removed package from installed database: {}", key);
Ok(()) Ok(())
} }
/// Query the installed packages database for packages matching a pattern /// Query the installed packages database for packages matching a pattern
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> { pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<InstalledPackageInfo>> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction // 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)))?; .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 // Use a block scope to ensure the table is dropped when no longer needed
let results = { let results = {
// Open the installed table // Open the installed table
let installed_table = tx.open_table(INSTALLED_TABLE) let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; InstalledError::Database(format!("Failed to open installed table: {}", e))
})?;
let mut results = Vec::new(); let mut results = Vec::new();
// Process the installed table // Process the installed table
// Iterate through all entries in the 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)))? { for entry_result in installed_table.iter().map_err(|e| {
let (key, _) = entry_result.map_err(|e| InstalledError::Database(format!("Failed to get entry from installed table: {}", 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(); let key_str = key.value();
// Skip if the key doesn't match the pattern // Skip if the key doesn't match the pattern
if let Some(pattern) = pattern { if let Some(pattern) = pattern {
if !key_str.contains(pattern) { if !key_str.contains(pattern) {
continue; continue;
} }
} }
// Parse the key to get the FMRI // Parse the key to get the FMRI
let fmri = Fmri::from_str(key_str)?; let fmri = Fmri::from_str(key_str)?;
// Get the publisher (handling the Option<String>) // 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 // Add to results
results.push(InstalledPackageInfo { results.push(InstalledPackageInfo { fmri, publisher });
fmri,
publisher,
});
} }
results results
// The table is dropped at the end of this block // The table is dropped at the end of this block
}; };
Ok(results) Ok(results)
} }
/// Get a manifest from the installed packages database /// Get a manifest from the installed packages database
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> { pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction // 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)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher) // Create the key (full FMRI including publisher)
let key = fmri.to_string(); let key = fmri.to_string();
// Use a block scope to ensure the table is dropped when no longer needed // Use a block scope to ensure the table is dropped when no longer needed
let manifest_option = { let manifest_option = {
// Open the installed table // Open the installed table
let installed_table = tx.open_table(INSTALLED_TABLE) let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; InstalledError::Database(format!("Failed to open installed table: {}", e))
})?;
// Try to get the manifest from the installed table // Try to get the manifest from the installed table
if let Ok(Some(bytes)) = installed_table.get(key.as_str()) { if let Ok(Some(bytes)) = installed_table.get(key.as_str()) {
Some(serde_json::from_slice(bytes.value())?) Some(serde_json::from_slice(bytes.value())?)
@ -335,29 +383,31 @@ impl InstalledPackages {
} }
// The table is dropped at the end of this block // The table is dropped at the end of this block
}; };
Ok(manifest_option) Ok(manifest_option)
} }
/// Check if a package is installed /// Check if a package is installed
pub fn is_installed(&self, fmri: &Fmri) -> Result<bool> { pub fn is_installed(&self, fmri: &Fmri) -> Result<bool> {
// Open the database // Open the database
let db = Database::open(&self.db_path) let db = Database::open(&self.db_path)
.map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?; .map_err(|e| InstalledError::Database(format!("Failed to open database: {}", e)))?;
// Begin a read transaction // 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)))?; .map_err(|e| InstalledError::Database(format!("Failed to begin transaction: {}", e)))?;
// Create the key (full FMRI including publisher) // Create the key (full FMRI including publisher)
let key = fmri.to_string(); let key = fmri.to_string();
// Use a block scope to ensure the table is dropped when no longer needed // Use a block scope to ensure the table is dropped when no longer needed
let is_installed = { let is_installed = {
// Open the installed table // Open the installed table
let installed_table = tx.open_table(INSTALLED_TABLE) let installed_table = tx.open_table(INSTALLED_TABLE).map_err(|e| {
.map_err(|e| InstalledError::Database(format!("Failed to open installed table: {}", e)))?; InstalledError::Database(format!("Failed to open installed table: {}", e))
})?;
// Check if the package exists // Check if the package exists
if let Ok(Some(_)) = installed_table.get(key.as_str()) { if let Ok(Some(_)) = installed_table.get(key.as_str()) {
true true
@ -366,7 +416,7 @@ impl InstalledPackages {
} }
// The table is dropped at the end of this block // The table is dropped at the end of this block
}; };
Ok(is_installed) Ok(is_installed)
} }
} }

View file

@ -10,67 +10,70 @@ fn test_installed_packages() {
// Create a temporary directory for the test // Create a temporary directory for the test
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let image_path = temp_dir.path().join("image"); let image_path = temp_dir.path().join("image");
// Create the image // Create the image
let image = Image::create_image(&image_path, ImageType::Full).unwrap(); let image = Image::create_image(&image_path, ImageType::Full).unwrap();
// Verify that the installed packages database was initialized // Verify that the installed packages database was initialized
assert!(image.installed_db_path().exists()); assert!(image.installed_db_path().exists());
// Create a test manifest // Create a test manifest
let mut manifest = Manifest::new(); let mut manifest = Manifest::new();
// Add some attributes to the manifest // Add some attributes to the manifest
let mut attr = Attr::default(); let mut attr = Attr::default();
attr.key = "pkg.fmri".to_string(); attr.key = "pkg.fmri".to_string();
attr.values = vec!["pkg://test/example/package@1.0".to_string()]; attr.values = vec!["pkg://test/example/package@1.0".to_string()];
manifest.attributes.push(attr); manifest.attributes.push(attr);
let mut attr = Attr::default(); let mut attr = Attr::default();
attr.key = "pkg.summary".to_string(); attr.key = "pkg.summary".to_string();
attr.values = vec!["Example package".to_string()]; attr.values = vec!["Example package".to_string()];
manifest.attributes.push(attr); manifest.attributes.push(attr);
let mut attr = Attr::default(); let mut attr = Attr::default();
attr.key = "pkg.description".to_string(); attr.key = "pkg.description".to_string();
attr.values = vec!["An example package for testing".to_string()]; attr.values = vec!["An example package for testing".to_string()];
manifest.attributes.push(attr); manifest.attributes.push(attr);
// Create an FMRI for the package // Create an FMRI for the package
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap(); let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
// Install the package // Install the package
image.install_package(&fmri, &manifest).unwrap(); image.install_package(&fmri, &manifest).unwrap();
// Verify that the package is installed // Verify that the package is installed
assert!(image.is_package_installed(&fmri).unwrap()); assert!(image.is_package_installed(&fmri).unwrap());
// Query the installed packages // Query the installed packages
let packages = image.query_installed_packages(None).unwrap(); let packages = image.query_installed_packages(None).unwrap();
// Verify that the package is in the results // Verify that the package is in the results
assert_eq!(packages.len(), 1); 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"); assert_eq!(packages[0].publisher, "test");
// Get the manifest from the installed packages database // Get the manifest from the installed packages database
let installed_manifest = image.get_manifest_from_installed(&fmri).unwrap().unwrap(); let installed_manifest = image.get_manifest_from_installed(&fmri).unwrap().unwrap();
// Verify that the manifest is correct // Verify that the manifest is correct
assert_eq!(installed_manifest.attributes.len(), 3); assert_eq!(installed_manifest.attributes.len(), 3);
// Uninstall the package // Uninstall the package
image.uninstall_package(&fmri).unwrap(); image.uninstall_package(&fmri).unwrap();
// Verify that the package is no longer installed // Verify that the package is no longer installed
assert!(!image.is_package_installed(&fmri).unwrap()); assert!(!image.is_package_installed(&fmri).unwrap());
// Query the installed packages again // Query the installed packages again
let packages = image.query_installed_packages(None).unwrap(); let packages = image.query_installed_packages(None).unwrap();
// Verify that there are no packages // Verify that there are no packages
assert_eq!(packages.len(), 0); assert_eq!(packages.len(), 0);
// Clean up // Clean up
temp_dir.close().unwrap(); temp_dir.close().unwrap();
} }
@ -80,42 +83,42 @@ fn test_installed_packages_key_format() {
// Create a temporary directory for the test // Create a temporary directory for the test
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let db_path = temp_dir.path().join("installed.redb"); let db_path = temp_dir.path().join("installed.redb");
// Create the installed packages database // Create the installed packages database
let installed = InstalledPackages::new(&db_path); let installed = InstalledPackages::new(&db_path);
installed.init_db().unwrap(); installed.init_db().unwrap();
// Create a test manifest // Create a test manifest
let mut manifest = Manifest::new(); let mut manifest = Manifest::new();
// Add some attributes to the manifest // Add some attributes to the manifest
let mut attr = Attr::default(); let mut attr = Attr::default();
attr.key = "pkg.fmri".to_string(); attr.key = "pkg.fmri".to_string();
attr.values = vec!["pkg://test/example/package@1.0".to_string()]; attr.values = vec!["pkg://test/example/package@1.0".to_string()];
manifest.attributes.push(attr); manifest.attributes.push(attr);
// Create an FMRI for the package // Create an FMRI for the package
let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap(); let fmri = Fmri::from_str("pkg://test/example/package@1.0").unwrap();
// Add the package to the database // Add the package to the database
installed.add_package(&fmri, &manifest).unwrap(); installed.add_package(&fmri, &manifest).unwrap();
// Open the database directly to check the key format // Open the database directly to check the key format
let db = Database::open(&db_path).unwrap(); let db = Database::open(&db_path).unwrap();
let tx = db.begin_read().unwrap(); let tx = db.begin_read().unwrap();
let table = tx.open_table(installed::INSTALLED_TABLE).unwrap(); let table = tx.open_table(installed::INSTALLED_TABLE).unwrap();
// Iterate through the keys // Iterate through the keys
let mut keys = Vec::new(); let mut keys = Vec::new();
for entry in table.iter().unwrap() { for entry in table.iter().unwrap() {
let (key, _) = entry.unwrap(); let (key, _) = entry.unwrap();
keys.push(key.value().to_string()); keys.push(key.value().to_string());
} }
// Verify that there is one key and it has the correct format // Verify that there is one key and it has the correct format
assert_eq!(keys.len(), 1); assert_eq!(keys.len(), 1);
assert_eq!(keys[0], "pkg://test/example/package@1.0"); assert_eq!(keys[0], "pkg://test/example/package@1.0");
// Clean up // Clean up
temp_dir.close().unwrap(); temp_dir.close().unwrap();
} }

View file

@ -4,18 +4,18 @@ mod tests;
use miette::Diagnostic; use miette::Diagnostic;
use properties::*; use properties::*;
use redb::{Database, ReadableDatabase, ReadableTable};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::{self, File}; use std::fs::{self, File};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; 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 // Export the catalog module
pub mod catalog; pub mod catalog;
use catalog::{ImageCatalog, PackageInfo, INCORPORATE_TABLE}; use catalog::{INCORPORATE_TABLE, ImageCatalog, PackageInfo};
// Export the installed packages module // Export the installed packages module
pub mod installed; pub mod installed;
@ -49,28 +49,28 @@ pub enum ImageError {
help("Provide a valid path for the image") help("Provide a valid path for the image")
)] )]
InvalidPath(String), InvalidPath(String),
#[error("Repository error: {0}")] #[error("Repository error: {0}")]
#[diagnostic( #[diagnostic(
code(ips::image_error::repository), code(ips::image_error::repository),
help("Check the repository configuration and try again") help("Check the repository configuration and try again")
)] )]
Repository(#[from] RepositoryError), Repository(#[from] RepositoryError),
#[error("Database error: {0}")] #[error("Database error: {0}")]
#[diagnostic( #[diagnostic(
code(ips::image_error::database), code(ips::image_error::database),
help("Check the database configuration and try again") help("Check the database configuration and try again")
)] )]
Database(String), Database(String),
#[error("Publisher not found: {0}")] #[error("Publisher not found: {0}")]
#[diagnostic( #[diagnostic(
code(ips::image_error::publisher_not_found), code(ips::image_error::publisher_not_found),
help("Check the publisher name and try again") help("Check the publisher name and try again")
)] )]
PublisherNotFound(String), PublisherNotFound(String),
#[error("No publishers configured")] #[error("No publishers configured")]
#[diagnostic( #[diagnostic(
code(ips::image_error::no_publishers), code(ips::image_error::no_publishers),
@ -148,9 +148,15 @@ impl Image {
publishers: vec![], publishers: vec![],
} }
} }
/// Add a publisher to the 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 // Check if publisher already exists
if self.publishers.iter().any(|p| p.name == name) { if self.publishers.iter().any(|p| p.name == name) {
// Update existing publisher // Update existing publisher
@ -159,7 +165,7 @@ impl Image {
publisher.origin = origin.to_string(); publisher.origin = origin.to_string();
publisher.mirrors = mirrors; publisher.mirrors = mirrors;
publisher.is_default = is_default; publisher.is_default = is_default;
// If this publisher is now the default, make sure no other publisher is default // If this publisher is now the default, make sure no other publisher is default
if is_default { if is_default {
for other_publisher in &mut self.publishers { for other_publisher in &mut self.publishers {
@ -168,7 +174,7 @@ impl Image {
} }
} }
} }
break; break;
} }
} }
@ -180,43 +186,43 @@ impl Image {
mirrors, mirrors,
is_default, is_default,
}; };
// If this publisher is the default, make sure no other publisher is default // If this publisher is the default, make sure no other publisher is default
if is_default { if is_default {
for publisher in &mut self.publishers { for publisher in &mut self.publishers {
publisher.is_default = false; publisher.is_default = false;
} }
} }
self.publishers.push(publisher); self.publishers.push(publisher);
} }
// Save the image to persist the changes // Save the image to persist the changes
self.save()?; self.save()?;
Ok(()) Ok(())
} }
/// Remove a publisher from the image /// Remove a publisher from the image
pub fn remove_publisher(&mut self, name: &str) -> Result<()> { pub fn remove_publisher(&mut self, name: &str) -> Result<()> {
let initial_len = self.publishers.len(); let initial_len = self.publishers.len();
self.publishers.retain(|p| p.name != name); self.publishers.retain(|p| p.name != name);
if self.publishers.len() == initial_len { if self.publishers.len() == initial_len {
return Err(ImageError::PublisherNotFound(name.to_string())); return Err(ImageError::PublisherNotFound(name.to_string()));
} }
// If we removed the default publisher, set the first remaining publisher as default // If we removed the default publisher, set the first remaining publisher as default
if self.publishers.iter().all(|p| !p.is_default) && !self.publishers.is_empty() { if self.publishers.iter().all(|p| !p.is_default) && !self.publishers.is_empty() {
self.publishers[0].is_default = true; self.publishers[0].is_default = true;
} }
// Save the image to persist the changes // Save the image to persist the changes
self.save()?; self.save()?;
Ok(()) Ok(())
} }
/// Get the default publisher /// Get the default publisher
pub fn default_publisher(&self) -> Result<&Publisher> { pub fn default_publisher(&self) -> Result<&Publisher> {
// Find the default publisher // Find the default publisher
@ -225,15 +231,15 @@ impl Image {
return Ok(publisher); return Ok(publisher);
} }
} }
// If no publisher is marked as default, return the first one // If no publisher is marked as default, return the first one
if !self.publishers.is_empty() { if !self.publishers.is_empty() {
return Ok(&self.publishers[0]); return Ok(&self.publishers[0]);
} }
Err(ImageError::NoPublishers) Err(ImageError::NoPublishers)
} }
/// Get a publisher by name /// Get a publisher by name
pub fn get_publisher(&self, name: &str) -> Result<&Publisher> { pub fn get_publisher(&self, name: &str) -> Result<&Publisher> {
for publisher in &self.publishers { for publisher in &self.publishers {
@ -241,10 +247,10 @@ impl Image {
return Ok(publisher); return Ok(publisher);
} }
} }
Err(ImageError::PublisherNotFound(name.to_string())) Err(ImageError::PublisherNotFound(name.to_string()))
} }
/// Get all publishers /// Get all publishers
pub fn publishers(&self) -> &[Publisher] { pub fn publishers(&self) -> &[Publisher] {
&self.publishers &self.publishers
@ -272,27 +278,27 @@ impl Image {
pub fn image_json_path(&self) -> PathBuf { pub fn image_json_path(&self) -> PathBuf {
self.metadata_dir().join("pkg6.image.json") self.metadata_dir().join("pkg6.image.json")
} }
/// Returns the path to the installed packages database /// Returns the path to the installed packages database
pub fn installed_db_path(&self) -> PathBuf { pub fn installed_db_path(&self) -> PathBuf {
self.metadata_dir().join("installed.redb") self.metadata_dir().join("installed.redb")
} }
/// Returns the path to the manifest directory /// Returns the path to the manifest directory
pub fn manifest_dir(&self) -> PathBuf { pub fn manifest_dir(&self) -> PathBuf {
self.metadata_dir().join("manifests") self.metadata_dir().join("manifests")
} }
/// Returns the path to the catalog directory /// Returns the path to the catalog directory
pub fn catalog_dir(&self) -> PathBuf { pub fn catalog_dir(&self) -> PathBuf {
self.metadata_dir().join("catalog") self.metadata_dir().join("catalog")
} }
/// Returns the path to the catalog database /// Returns the path to the catalog database
pub fn catalog_db_path(&self) -> PathBuf { pub fn catalog_db_path(&self) -> PathBuf {
self.metadata_dir().join("catalog.redb") self.metadata_dir().join("catalog.redb")
} }
/// Returns the path to the obsoleted packages database (separate DB) /// Returns the path to the obsoleted packages database (separate DB)
pub fn obsoleted_db_path(&self) -> PathBuf { pub fn obsoleted_db_path(&self) -> PathBuf {
self.metadata_dir().join("obsoleted.redb") self.metadata_dir().join("obsoleted.redb")
@ -304,46 +310,62 @@ impl Image {
fs::create_dir_all(&metadata_dir).map_err(|e| { fs::create_dir_all(&metadata_dir).map_err(|e| {
ImageError::IO(std::io::Error::new( ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("Failed to create metadata directory at {:?}: {}", metadata_dir, e), format!(
"Failed to create metadata directory at {:?}: {}",
metadata_dir, e
),
)) ))
}) })
} }
/// Creates the manifest directory if it doesn't exist /// Creates the manifest directory if it doesn't exist
pub fn create_manifest_dir(&self) -> Result<()> { pub fn create_manifest_dir(&self) -> Result<()> {
let manifest_dir = self.manifest_dir(); let manifest_dir = self.manifest_dir();
fs::create_dir_all(&manifest_dir).map_err(|e| { fs::create_dir_all(&manifest_dir).map_err(|e| {
ImageError::IO(std::io::Error::new( ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("Failed to create manifest directory at {:?}: {}", manifest_dir, e), format!(
"Failed to create manifest directory at {:?}: {}",
manifest_dir, e
),
)) ))
}) })
} }
/// Creates the catalog directory if it doesn't exist /// Creates the catalog directory if it doesn't exist
pub fn create_catalog_dir(&self) -> Result<()> { pub fn create_catalog_dir(&self) -> Result<()> {
let catalog_dir = self.catalog_dir(); let catalog_dir = self.catalog_dir();
fs::create_dir_all(&catalog_dir).map_err(|e| { fs::create_dir_all(&catalog_dir).map_err(|e| {
ImageError::IO(std::io::Error::new( ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("Failed to create catalog directory at {:?}: {}", catalog_dir, e), format!(
"Failed to create catalog directory at {:?}: {}",
catalog_dir, e
),
)) ))
}) })
} }
/// Initialize the installed packages database /// Initialize the installed packages database
pub fn init_installed_db(&self) -> Result<()> { pub fn init_installed_db(&self) -> Result<()> {
let db_path = self.installed_db_path(); let db_path = self.installed_db_path();
// Create the installed packages database // Create the installed packages database
let installed = InstalledPackages::new(&db_path); let installed = InstalledPackages::new(&db_path);
installed.init_db().map_err(|e| { 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 /// 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 // Precheck incorporation dependencies: fail if any stem already has a lock
for d in &manifest.dependencies { for d in &manifest.dependencies {
if d.dependency_type == "incorporate" { if d.dependency_type == "incorporate" {
@ -351,7 +373,8 @@ impl Image {
let stem = df.stem(); let stem = df.stem();
if let Some(_) = self.get_incorporated_release(stem)? { if let Some(_) = self.get_incorporated_release(stem)? {
return Err(ImageError::Database(format!( 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 // Add to installed database
let installed = InstalledPackages::new(self.installed_db_path()); let installed = InstalledPackages::new(self.installed_db_path());
installed.add_package(fmri, manifest).map_err(|e| { 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 // Write incorporation locks for any incorporate dependencies
@ -380,31 +406,43 @@ impl Image {
} }
Ok(()) Ok(())
} }
/// Remove a package from the installed packages database /// Remove a package from the installed packages database
pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> { pub fn uninstall_package(&self, fmri: &crate::fmri::Fmri) -> Result<()> {
let installed = InstalledPackages::new(self.installed_db_path()); let installed = InstalledPackages::new(self.installed_db_path());
installed.remove_package(fmri).map_err(|e| { 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 /// 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()); let installed = InstalledPackages::new(self.installed_db_path());
installed.query_packages(pattern).map_err(|e| { installed
ImageError::Database(format!("Failed to query installed packages: {}", e)) .query_packages(pattern)
}) .map_err(|e| ImageError::Database(format!("Failed to query installed packages: {}", e)))
} }
/// Get a manifest from the installed packages database /// 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()); let installed = InstalledPackages::new(self.installed_db_path());
installed.get_manifest(fmri).map_err(|e| { installed.get_manifest(fmri).map_err(|e| {
ImageError::Database(format!("Failed to get manifest from installed database: {}", e)) ImageError::Database(format!(
"Failed to get manifest from installed database: {}",
e
))
}) })
} }
/// Check if a package is installed /// Check if a package is installed
pub fn is_package_installed(&self, fmri: &crate::fmri::Fmri) -> Result<bool> { pub fn is_package_installed(&self, fmri: &crate::fmri::Fmri) -> Result<bool> {
let installed = InstalledPackages::new(self.installed_db_path()); let installed = InstalledPackages::new(self.installed_db_path());
@ -412,14 +450,18 @@ impl Image {
ImageError::Database(format!("Failed to check if package is installed: {}", e)) ImageError::Database(format!("Failed to check if package is installed: {}", e))
}) })
} }
/// Save a manifest into the metadata manifests directory for this image. /// Save a manifest into the metadata manifests directory for this image.
/// ///
/// The original, unprocessed manifest text is downloaded from the repository /// The original, unprocessed manifest text is downloaded from the repository
/// and stored under a flattened path: /// and stored under a flattened path:
/// manifests/<publisher>/<encoded_stem>@<encoded_version>.p5m /// manifests/<publisher>/<encoded_stem>@<encoded_version>.p5m
/// Missing publisher will fall back to the image default publisher, then "unknown". /// 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 // Determine publisher name
let pub_name = if let Some(p) = &fmri.publisher { let pub_name = if let Some(p) = &fmri.publisher {
p.clone() p.clone()
@ -438,7 +480,9 @@ impl Image {
let mut out = String::new(); let mut out = String::new();
for b in s.bytes() { for b in s.bytes() {
match b { 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('+'), b' ' => out.push('+'),
_ => { _ => {
out.push('%'); out.push('%');
@ -481,31 +525,35 @@ impl Image {
Ok(file_path) Ok(file_path)
} }
/// Initialize the catalog database /// Initialize the catalog database
pub fn init_catalog_db(&self) -> Result<()> { 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| { catalog.init_db().map_err(|e| {
ImageError::Database(format!("Failed to initialize catalog database: {}", e)) ImageError::Database(format!("Failed to initialize catalog database: {}", e))
}) })
} }
/// Download catalogs from all configured publishers and build the merged catalog /// Download catalogs from all configured publishers and build the merged catalog
pub fn download_catalogs(&self) -> Result<()> { pub fn download_catalogs(&self) -> Result<()> {
// Create catalog directory if it doesn't exist // Create catalog directory if it doesn't exist
self.create_catalog_dir()?; self.create_catalog_dir()?;
// Download catalogs for each publisher // Download catalogs for each publisher
for publisher in &self.publishers { for publisher in &self.publishers {
self.download_publisher_catalog(&publisher.name)?; self.download_publisher_catalog(&publisher.name)?;
} }
// Build the merged catalog // Build the merged catalog
self.build_catalog()?; self.build_catalog()?;
Ok(()) Ok(())
} }
/// Refresh catalogs for specified publishers or all publishers if none specified /// Refresh catalogs for specified publishers or all publishers if none specified
/// ///
/// # Arguments /// # Arguments
@ -519,78 +567,91 @@ impl Image {
pub fn refresh_catalogs(&self, publishers: &[String], full: bool) -> Result<()> { pub fn refresh_catalogs(&self, publishers: &[String], full: bool) -> Result<()> {
// Create catalog directory if it doesn't exist // Create catalog directory if it doesn't exist
self.create_catalog_dir()?; self.create_catalog_dir()?;
// Determine which publishers to refresh // Determine which publishers to refresh
let publishers_to_refresh: Vec<&Publisher> = if publishers.is_empty() { let publishers_to_refresh: Vec<&Publisher> = if publishers.is_empty() {
// If no publishers specified, refresh all // If no publishers specified, refresh all
self.publishers.iter().collect() self.publishers.iter().collect()
} else { } else {
// Otherwise, filter publishers by name // Otherwise, filter publishers by name
self.publishers.iter() self.publishers
.iter()
.filter(|p| publishers.contains(&p.name)) .filter(|p| publishers.contains(&p.name))
.collect() .collect()
}; };
// Check if we have any publishers to refresh // Check if we have any publishers to refresh
if publishers_to_refresh.is_empty() { if publishers_to_refresh.is_empty() {
return Err(ImageError::NoPublishers); return Err(ImageError::NoPublishers);
} }
// If full refresh is requested, clear the catalog directory for each publisher // If full refresh is requested, clear the catalog directory for each publisher
if full { if full {
for publisher in &publishers_to_refresh { for publisher in &publishers_to_refresh {
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name); let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
if publisher_catalog_dir.exists() { if publisher_catalog_dir.exists() {
fs::remove_dir_all(&publisher_catalog_dir) fs::remove_dir_all(&publisher_catalog_dir).map_err(|e| {
.map_err(|e| ImageError::IO(std::io::Error::new( ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("Failed to remove catalog directory for publisher {}: {}", format!(
publisher.name, e) "Failed to remove catalog directory for publisher {}: {}",
)))?; publisher.name, e
),
))
})?;
} }
fs::create_dir_all(&publisher_catalog_dir) fs::create_dir_all(&publisher_catalog_dir).map_err(|e| {
.map_err(|e| ImageError::IO(std::io::Error::new( ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("Failed to create catalog directory for publisher {}: {}", format!(
publisher.name, e) "Failed to create catalog directory for publisher {}: {}",
)))?; publisher.name, e
),
))
})?;
} }
} }
// Download catalogs for each publisher // Download catalogs for each publisher
for publisher in publishers_to_refresh { for publisher in publishers_to_refresh {
self.download_publisher_catalog(&publisher.name)?; self.download_publisher_catalog(&publisher.name)?;
} }
// Build the merged catalog // Build the merged catalog
self.build_catalog()?; self.build_catalog()?;
Ok(()) Ok(())
} }
/// Build the merged catalog from downloaded catalogs /// Build the merged catalog from downloaded catalogs
pub fn build_catalog(&self) -> Result<()> { pub fn build_catalog(&self) -> Result<()> {
// Initialize the catalog database if it doesn't exist // Initialize the catalog database if it doesn't exist
self.init_catalog_db()?; self.init_catalog_db()?;
// Get publisher names // Get publisher names
let publisher_names: Vec<String> = self.publishers.iter() let publisher_names: Vec<String> = self.publishers.iter().map(|p| p.name.clone()).collect();
.map(|p| p.name.clone())
.collect();
// Create the catalog and build it // Create the catalog and build it
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path()); let catalog = ImageCatalog::new(
catalog.build_catalog(&publisher_names).map_err(|e| { self.catalog_dir(),
ImageError::Database(format!("Failed to build catalog: {}", e)) 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 /// Query the catalog for packages matching a pattern
pub fn query_catalog(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> { 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()); let catalog = ImageCatalog::new(
catalog.query_packages(pattern).map_err(|e| { self.catalog_dir(),
ImageError::Database(format!("Failed to query catalog: {}", e)) 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. /// 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>> { pub fn get_incorporated_release(&self, stem: &str) -> Result<Option<String>> {
let db = Database::open(self.catalog_db_path()) let db = Database::open(self.catalog_db_path())
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?; .map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
let tx = db.begin_read() let tx = db.begin_read().map_err(|e| {
.map_err(|e| ImageError::Database(format!("Failed to begin read transaction: {}", e)))?; ImageError::Database(format!("Failed to begin read transaction: {}", e))
})?;
match tx.open_table(INCORPORATE_TABLE) { match tx.open_table(INCORPORATE_TABLE) {
Ok(table) => { Ok(table) => match table.get(stem) {
match table.get(stem) { Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())),
Ok(Some(val)) => Ok(Some(String::from_utf8_lossy(val.value()).to_string())), Ok(None) => Ok(None),
Ok(None) => Ok(None), Err(e) => Err(ImageError::Database(format!(
Err(e) => Err(ImageError::Database(format!("Failed to read incorporate lock: {}", e))), "Failed to read incorporate lock: {}",
} e
} ))),
},
Err(_) => Ok(None), Err(_) => Ok(None),
} }
} }
@ -617,91 +680,109 @@ impl Image {
pub fn add_incorporation_lock(&self, stem: &str, release: &str) -> Result<()> { pub fn add_incorporation_lock(&self, stem: &str, release: &str) -> Result<()> {
let db = Database::open(self.catalog_db_path()) let db = Database::open(self.catalog_db_path())
.map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?; .map_err(|e| ImageError::Database(format!("Failed to open catalog database: {}", e)))?;
let tx = db.begin_write() let tx = db.begin_write().map_err(|e| {
.map_err(|e| ImageError::Database(format!("Failed to begin write transaction: {}", e)))?; ImageError::Database(format!("Failed to begin write transaction: {}", e))
})?;
{ {
let mut table = tx.open_table(INCORPORATE_TABLE) let mut table = tx.open_table(INCORPORATE_TABLE).map_err(|e| {
.map_err(|e| ImageError::Database(format!("Failed to open incorporate table: {}", e)))?; ImageError::Database(format!("Failed to open incorporate table: {}", e))
})?;
if let Ok(Some(_)) = table.get(stem) { 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()) table.insert(stem, release.as_bytes()).map_err(|e| {
.map_err(|e| ImageError::Database(format!("Failed to insert incorporate lock: {}", e)))?; ImageError::Database(format!("Failed to insert incorporate lock: {}", e))
})?;
} }
tx.commit() tx.commit().map_err(|e| {
.map_err(|e| ImageError::Database(format!("Failed to commit incorporate lock: {}", e)))? ImageError::Database(format!("Failed to commit incorporate lock: {}", e))
; })?;
Ok(()) Ok(())
} }
/// Get a manifest from the catalog /// Get a manifest from the catalog
pub fn get_manifest_from_catalog(&self, fmri: &crate::fmri::Fmri) -> Result<Option<crate::actions::Manifest>> { pub fn get_manifest_from_catalog(
let catalog = ImageCatalog::new(self.catalog_dir(), self.catalog_db_path(), self.obsoleted_db_path()); &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| { catalog.get_manifest(fmri).map_err(|e| {
ImageError::Database(format!("Failed to get manifest from catalog: {}", e)) ImageError::Database(format!("Failed to get manifest from catalog: {}", e))
}) })
} }
/// Fetch a full manifest for the given FMRI directly from its repository origin. /// Fetch a full manifest for the given FMRI directly from its repository origin.
/// ///
/// This bypasses the local catalog database and retrieves the full manifest from /// This bypasses the local catalog database and retrieves the full manifest from
/// the configured publisher origin (REST for http/https origins; File backend for /// the configured publisher origin (REST for http/https origins; File backend for
/// file:// origins). A versioned FMRI is required. /// 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 // Determine publisher: use FMRI's publisher if present, otherwise default publisher
let publisher_name = if let Some(p) = &fmri.publisher { let publisher_name = if let Some(p) = &fmri.publisher {
p.clone() p.clone()
} else { } else {
self.default_publisher()?.name.clone() self.default_publisher()?.name.clone()
}; };
// Look up publisher configuration // Look up publisher configuration
let publisher = self.get_publisher(&publisher_name)?; let publisher = self.get_publisher(&publisher_name)?;
let origin = &publisher.origin; let origin = &publisher.origin;
// Require a concrete version in the FMRI // Require a concrete version in the FMRI
if fmri.version().is_empty() { if fmri.version().is_empty() {
return Err(ImageError::Repository(RepositoryError::Other( return Err(ImageError::Repository(RepositoryError::Other(
"FMRI must include a version to fetch manifest".to_string(), "FMRI must include a version to fetch manifest".to_string(),
))); )));
} }
// Choose backend based on origin scheme // Choose backend based on origin scheme
if origin.starts_with("file://") { if origin.starts_with("file://") {
let path_str = origin.trim_start_matches("file://"); let path_str = origin.trim_start_matches("file://");
let path = PathBuf::from(path_str); let path = PathBuf::from(path_str);
let mut repo = FileBackend::open(&path)?; 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 { } else {
let mut repo = RestBackend::open(origin)?; let mut repo = RestBackend::open(origin)?;
// Optionally set a per-publisher cache directory (used by other REST ops) // Optionally set a per-publisher cache directory (used by other REST ops)
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name); let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
repo.set_local_cache_path(&publisher_catalog_dir)?; repo.set_local_cache_path(&publisher_catalog_dir)?;
repo.fetch_manifest(&publisher_name, fmri).map_err(Into::into) repo.fetch_manifest(&publisher_name, fmri)
.map_err(Into::into)
} }
} }
/// Download catalog for a specific publisher /// Download catalog for a specific publisher
pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> { pub fn download_publisher_catalog(&self, publisher_name: &str) -> Result<()> {
// Get the publisher // Get the publisher
let publisher = self.get_publisher(publisher_name)?; let publisher = self.get_publisher(publisher_name)?;
// Create a REST backend for the publisher // Create a REST backend for the publisher
let mut repo = RestBackend::open(&publisher.origin)?; let mut repo = RestBackend::open(&publisher.origin)?;
// Set local cache path to the catalog directory for this publisher // Set local cache path to the catalog directory for this publisher
let publisher_catalog_dir = self.catalog_dir().join(&publisher.name); let publisher_catalog_dir = self.catalog_dir().join(&publisher.name);
fs::create_dir_all(&publisher_catalog_dir)?; fs::create_dir_all(&publisher_catalog_dir)?;
repo.set_local_cache_path(&publisher_catalog_dir)?; repo.set_local_cache_path(&publisher_catalog_dir)?;
// Download the catalog // Download the catalog
repo.download_catalog(&publisher.name, None)?; repo.download_catalog(&publisher.name, None)?;
Ok(()) Ok(())
} }
/// Create a new image with the basic directory structure /// Create a new image with the basic directory structure
/// ///
/// This method only creates the image structure without adding publishers or downloading catalogs. /// This method only creates the image structure without adding publishers or downloading catalogs.
/// Publisher addition and catalog downloading should be handled separately. /// Publisher addition and catalog downloading should be handled separately.
/// ///
@ -715,21 +796,21 @@ impl Image {
ImageType::Full => Image::new_full(path.as_ref().to_path_buf()), ImageType::Full => Image::new_full(path.as_ref().to_path_buf()),
ImageType::Partial => Image::new_partial(path.as_ref().to_path_buf()), ImageType::Partial => Image::new_partial(path.as_ref().to_path_buf()),
}; };
// Create the directory structure // Create the directory structure
image.create_metadata_dir()?; image.create_metadata_dir()?;
image.create_manifest_dir()?; image.create_manifest_dir()?;
image.create_catalog_dir()?; image.create_catalog_dir()?;
// Initialize the installed packages database // Initialize the installed packages database
image.init_installed_db()?; image.init_installed_db()?;
// Initialize the catalog database // Initialize the catalog database
image.init_catalog_db()?; image.init_catalog_db()?;
// Save the image // Save the image
image.save()?; image.save()?;
Ok(image) Ok(image)
} }
@ -749,14 +830,14 @@ impl Image {
/// Loads an image from the specified path /// Loads an image from the specified path
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> { pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref(); let path = path.as_ref();
// Check for both full and partial image JSON files // Check for both full and partial image JSON files
let full_image = Image::new_full(path); let full_image = Image::new_full(path);
let partial_image = Image::new_partial(path); let partial_image = Image::new_partial(path);
let full_json_path = full_image.image_json_path(); let full_json_path = full_image.image_json_path();
let partial_json_path = partial_image.image_json_path(); let partial_json_path = partial_image.image_json_path();
// Determine which JSON file exists // Determine which JSON file exists
let json_path = if full_json_path.exists() { let json_path = if full_json_path.exists() {
full_json_path full_json_path
@ -764,18 +845,18 @@ impl Image {
partial_json_path partial_json_path
} else { } else {
return Err(ImageError::InvalidPath(format!( return Err(ImageError::InvalidPath(format!(
"Image JSON file not found at either {:?} or {:?}", "Image JSON file not found at either {:?} or {:?}",
full_json_path, partial_json_path full_json_path, partial_json_path
))); )));
}; };
let file = File::open(&json_path).map_err(|e| { let file = File::open(&json_path).map_err(|e| {
ImageError::IO(std::io::Error::new( ImageError::IO(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
format!("Failed to open image JSON file at {:?}: {}", json_path, e), format!("Failed to open image JSON file at {:?}: {}", json_path, e),
)) ))
})?; })?;
serde_json::from_reader(file).map_err(ImageError::Json) serde_json::from_reader(file).map_err(ImageError::Json)
} }
} }

View file

@ -7,13 +7,13 @@ fn test_image_catalog() {
// Create a temporary directory for the test // Create a temporary directory for the test
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let image_path = temp_dir.path().join("image"); let image_path = temp_dir.path().join("image");
// Create the image // Create the image
let image = Image::create_image(&image_path, ImageType::Full).unwrap(); let image = Image::create_image(&image_path, ImageType::Full).unwrap();
// Verify that the catalog database was initialized // Verify that the catalog database was initialized
assert!(image.catalog_db_path().exists()); assert!(image.catalog_db_path().exists());
// Clean up // Clean up
temp_dir.close().unwrap(); temp_dir.close().unwrap();
} }
@ -23,28 +23,30 @@ fn test_catalog_methods() {
// Create a temporary directory for the test // Create a temporary directory for the test
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let image_path = temp_dir.path().join("image"); let image_path = temp_dir.path().join("image");
// Create the image // Create the image
let mut image = Image::create_image(&image_path, ImageType::Full).unwrap(); let mut image = Image::create_image(&image_path, ImageType::Full).unwrap();
// Print the image type and paths // Print the image type and paths
println!("Image type: {:?}", image.image_type()); println!("Image type: {:?}", image.image_type());
println!("Image path: {:?}", image.path()); println!("Image path: {:?}", image.path());
println!("Metadata dir: {:?}", image.metadata_dir()); println!("Metadata dir: {:?}", image.metadata_dir());
println!("Catalog dir: {:?}", image.catalog_dir()); println!("Catalog dir: {:?}", image.catalog_dir());
// Add a publisher // 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 // Print the publishers
println!("Publishers: {:?}", image.publishers()); println!("Publishers: {:?}", image.publishers());
// Create the catalog directory structure // Create the catalog directory structure
let catalog_dir = image.catalog_dir(); let catalog_dir = image.catalog_dir();
let publisher_dir = catalog_dir.join("test"); let publisher_dir = catalog_dir.join("test");
println!("Publisher dir: {:?}", publisher_dir); println!("Publisher dir: {:?}", publisher_dir);
fs::create_dir_all(&publisher_dir).unwrap(); fs::create_dir_all(&publisher_dir).unwrap();
// Create a simple catalog.attrs file // Create a simple catalog.attrs file
let attrs_content = r#"{ let attrs_content = r#"{
"created": "2025-08-04T23:01:00Z", "created": "2025-08-04T23:01:00Z",
@ -59,10 +61,13 @@ fn test_catalog_methods() {
"updates": {}, "updates": {},
"version": 1 "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); println!("catalog.attrs content: {}", attrs_content);
fs::write(publisher_dir.join("catalog.attrs"), attrs_content).unwrap(); fs::write(publisher_dir.join("catalog.attrs"), attrs_content).unwrap();
// Create a simple base catalog part // Create a simple base catalog part
let base_content = r#"{ let base_content = r#"{
"test": { "test": {
@ -88,68 +93,80 @@ fn test_catalog_methods() {
] ]
} }
}"#; }"#;
println!("Writing base catalog part to {:?}", publisher_dir.join("base")); println!(
"Writing base catalog part to {:?}",
publisher_dir.join("base")
);
println!("base catalog part content: {}", base_content); println!("base catalog part content: {}", base_content);
fs::write(publisher_dir.join("base"), base_content).unwrap(); fs::write(publisher_dir.join("base"), base_content).unwrap();
// Verify that the files were written correctly // Verify that the files were written correctly
println!("Checking if catalog.attrs exists: {}", publisher_dir.join("catalog.attrs").exists()); println!(
println!("Checking if base catalog part exists: {}", publisher_dir.join("base").exists()); "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 // Build the catalog
println!("Building catalog..."); println!("Building catalog...");
match image.build_catalog() { match image.build_catalog() {
Ok(_) => println!("Catalog built successfully"), Ok(_) => println!("Catalog built successfully"),
Err(e) => println!("Failed to build catalog: {:?}", e), Err(e) => println!("Failed to build catalog: {:?}", e),
} }
// Query the catalog // Query the catalog
println!("Querying catalog..."); println!("Querying catalog...");
let packages = match image.query_catalog(None) { let packages = match image.query_catalog(None) {
Ok(pkgs) => { Ok(pkgs) => {
println!("Found {} packages", pkgs.len()); println!("Found {} packages", pkgs.len());
pkgs pkgs
}, }
Err(e) => { Err(e) => {
println!("Failed to query catalog: {:?}", e); println!("Failed to query catalog: {:?}", e);
panic!("Failed to query catalog: {:?}", e); panic!("Failed to query catalog: {:?}", e);
} }
}; };
// Verify that both non-obsolete and obsolete packages are in the results // Verify that both non-obsolete and obsolete packages are in the results
assert_eq!(packages.len(), 2); assert_eq!(packages.len(), 2);
// Verify that one package is marked as obsolete // Verify that one package is marked as obsolete
let obsolete_packages: Vec<_> = packages.iter().filter(|p| p.obsolete).collect(); let obsolete_packages: Vec<_> = packages.iter().filter(|p| p.obsolete).collect();
assert_eq!(obsolete_packages.len(), 1); assert_eq!(obsolete_packages.len(), 1);
assert_eq!(obsolete_packages[0].fmri.stem(), "example/obsolete"); assert_eq!(obsolete_packages[0].fmri.stem(), "example/obsolete");
// Verify that the obsolete package has the full FMRI as key // 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 // 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 // Verify that one package is not marked as obsolete
let non_obsolete_packages: Vec<_> = packages.iter().filter(|p| !p.obsolete).collect(); let non_obsolete_packages: Vec<_> = packages.iter().filter(|p| !p.obsolete).collect();
assert_eq!(non_obsolete_packages.len(), 1); assert_eq!(non_obsolete_packages.len(), 1);
assert_eq!(non_obsolete_packages[0].fmri.stem(), "example/package"); assert_eq!(non_obsolete_packages[0].fmri.stem(), "example/package");
// Get the manifest for the non-obsolete package // Get the manifest for the non-obsolete package
let fmri = &non_obsolete_packages[0].fmri; let fmri = &non_obsolete_packages[0].fmri;
let manifest = image.get_manifest_from_catalog(fmri).unwrap(); let manifest = image.get_manifest_from_catalog(fmri).unwrap();
assert!(manifest.is_some()); assert!(manifest.is_some());
// Get the manifest for the obsolete package // Get the manifest for the obsolete package
let fmri = &obsolete_packages[0].fmri; let fmri = &obsolete_packages[0].fmri;
let manifest = image.get_manifest_from_catalog(fmri).unwrap(); let manifest = image.get_manifest_from_catalog(fmri).unwrap();
assert!(manifest.is_some()); assert!(manifest.is_some());
// Verify that the obsolete package's manifest has the obsolete attribute // Verify that the obsolete package's manifest has the obsolete attribute
let manifest = manifest.unwrap(); let manifest = manifest.unwrap();
let is_obsolete = manifest.attributes.iter().any(|attr| { let is_obsolete = manifest.attributes.iter().any(|attr| {
attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true") attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true")
}); });
assert!(is_obsolete); assert!(is_obsolete);
// Clean up // Clean up
temp_dir.close().unwrap(); temp_dir.close().unwrap();
} }
@ -159,45 +176,61 @@ fn test_refresh_catalogs_directory_clearing() {
// Create a temporary directory for the test // Create a temporary directory for the test
let temp_dir = tempdir().unwrap(); let temp_dir = tempdir().unwrap();
let image_path = temp_dir.path().join("image"); let image_path = temp_dir.path().join("image");
// Create the image // Create the image
let mut image = Image::create_image(&image_path, ImageType::Full).unwrap(); let mut image = Image::create_image(&image_path, ImageType::Full).unwrap();
// Add two publishers // Add two publishers
image.add_publisher("test1", "http://example.com/repo1", vec![], true).unwrap(); image
image.add_publisher("test2", "http://example.com/repo2", vec![], false).unwrap(); .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 // Create the catalog directory structure for both publishers
let catalog_dir = image.catalog_dir(); let catalog_dir = image.catalog_dir();
let publisher1_dir = catalog_dir.join("test1"); let publisher1_dir = catalog_dir.join("test1");
let publisher2_dir = catalog_dir.join("test2"); let publisher2_dir = catalog_dir.join("test2");
fs::create_dir_all(&publisher1_dir).unwrap(); fs::create_dir_all(&publisher1_dir).unwrap();
fs::create_dir_all(&publisher2_dir).unwrap(); fs::create_dir_all(&publisher2_dir).unwrap();
// Create marker files in both publisher directories // Create marker files in both publisher directories
let marker_file1 = publisher1_dir.join("marker"); let marker_file1 = publisher1_dir.join("marker");
let marker_file2 = publisher2_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(
fs::write(&marker_file2, "This file should be removed during full refresh").unwrap(); &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_file1.exists());
assert!(marker_file2.exists()); assert!(marker_file2.exists());
// Directly test the directory clearing functionality for a specific publisher // Directly test the directory clearing functionality for a specific publisher
// This simulates the behavior of refresh_catalogs with full=true for a specific publisher // This simulates the behavior of refresh_catalogs with full=true for a specific publisher
if publisher1_dir.exists() { if publisher1_dir.exists() {
fs::remove_dir_all(&publisher1_dir).unwrap(); fs::remove_dir_all(&publisher1_dir).unwrap();
} }
fs::create_dir_all(&publisher1_dir).unwrap(); fs::create_dir_all(&publisher1_dir).unwrap();
// Verify that the marker file for publisher1 was removed // Verify that the marker file for publisher1 was removed
assert!(!marker_file1.exists()); assert!(!marker_file1.exists());
// Verify that the marker file for publisher2 still exists // Verify that the marker file for publisher2 still exists
assert!(marker_file2.exists()); assert!(marker_file2.exists());
// Create a new marker file for publisher1 // 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()); assert!(marker_file1.exists());
// Directly test the directory clearing functionality for all publishers // Directly test the directory clearing functionality for all publishers
// This simulates the behavior of refresh_catalogs with full=true for all publishers // This simulates the behavior of refresh_catalogs with full=true for all publishers
for publisher in &image.publishers { for publisher in &image.publishers {
@ -207,11 +240,11 @@ fn test_refresh_catalogs_directory_clearing() {
} }
fs::create_dir_all(&publisher_dir).unwrap(); fs::create_dir_all(&publisher_dir).unwrap();
} }
// Verify that both marker files were removed // Verify that both marker files were removed
assert!(!marker_file1.exists()); assert!(!marker_file1.exists());
assert!(!marker_file2.exists()); assert!(!marker_file2.exists());
// Clean up // Clean up
temp_dir.close().unwrap(); temp_dir.close().unwrap();
} }

View file

@ -5,17 +5,17 @@
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
pub mod actions; pub mod actions;
pub mod api;
pub mod depend;
pub mod digest; pub mod digest;
pub mod fmri; pub mod fmri;
pub mod image; pub mod image;
pub mod payload; pub mod payload;
pub mod repository;
pub mod publisher; pub mod publisher;
pub mod transformer; pub mod repository;
pub mod solver; pub mod solver;
pub mod depend;
pub mod api;
mod test_json_manifest; mod test_json_manifest;
pub mod transformer;
#[cfg(test)] #[cfg(test)]
mod publisher_tests; mod publisher_tests;
@ -69,91 +69,101 @@ set name=pkg.summary value=\"'XZ Utils - loss-less file compression application
); );
let test_results = vec![ let test_results = vec![
Attr{ Attr {
key: String::from("pkg.fmri"), 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(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("com.oracle.info.name"), key: String::from("com.oracle.info.name"),
values: vec![String::from("nginx"), String::from("test")], values: vec![String::from("nginx"), String::from("test")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("userland.info.git-remote"), key: String::from("userland.info.git-remote"),
values: vec![String::from("git://github.com/OpenIndiana/oi-userland.git")], values: vec![String::from("git://github.com/OpenIndiana/oi-userland.git")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("userland.info.git-branch"), key: String::from("userland.info.git-branch"),
values: vec![String::from("HEAD")], values: vec![String::from("HEAD")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("userland.info.git-rev"), key: String::from("userland.info.git-rev"),
values: vec![String::from("1665491ba61bd494bf73e2916cd2250f3024260e")], values: vec![String::from("1665491ba61bd494bf73e2916cd2250f3024260e")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("pkg.summary"), key: String::from("pkg.summary"),
values: vec![String::from("Nginx Webserver")], values: vec![String::from("Nginx Webserver")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("info.classification"), 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(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("info.upstream-url"), key: String::from("info.upstream-url"),
values: vec![String::from("http://nginx.net/")], values: vec![String::from("http://nginx.net/")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("info.source-url"), 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(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("org.opensolaris.consolidation"), key: String::from("org.opensolaris.consolidation"),
values: vec![String::from("userland")], values: vec![String::from("userland")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("com.oracle.info.version"), key: String::from("com.oracle.info.version"),
values: vec![String::from("1.18.0")], values: vec![String::from("1.18.0")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("pkg.summary"), key: String::from("pkg.summary"),
values: vec![String::from("provided mouse accessibility enhancements")], values: vec![String::from("provided mouse accessibility enhancements")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("info.upstream"), key: String::from("info.upstream"),
values: vec![String::from("X.Org Foundation")], values: vec![String::from("X.Org Foundation")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("pkg.description"), key: String::from("pkg.description"),
values: vec![String::from("Latvian language support's extra files")], values: vec![String::from("Latvian language support's extra files")],
properties: HashMap::new(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("variant.arch"), key: String::from("variant.arch"),
values: vec![String::from("i386")], values: vec![String::from("i386")],
properties: optional_hash, properties: optional_hash,
}, },
Attr{ Attr {
key: String::from("info.source-url"), 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(), properties: HashMap::new(),
}, },
Attr{ Attr {
key: String::from("pkg.summary"), 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(), properties: HashMap::new(),
} },
]; ];
let res = Manifest::parse_string(manifest_string); let res = Manifest::parse_string(manifest_string);

View file

@ -3,15 +3,15 @@
// MPL was not distributed with this file, You can // MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/. // obtain one at https://mozilla.org/MPL/2.0/.
use std::path::{Path, PathBuf};
use std::fs; use std::fs;
use std::path::{Path, PathBuf};
use miette::Diagnostic; use miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
use crate::actions::{File as FileAction, Manifest, Transform as TransformAction}; 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::file_backend::{FileBackend, Transaction};
use crate::repository::{ReadableRepository, RepositoryError, WritableRepository};
use crate::transformer; use crate::transformer;
/// Error type for high-level publishing operations /// Error type for high-level publishing operations
@ -30,7 +30,10 @@ pub enum PublisherError {
Io(String), Io(String),
#[error("invalid root path: {0}")] #[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), InvalidRoot(String),
} }
@ -51,7 +54,12 @@ impl PublisherClient {
/// Open an existing repository located at `path` with a selected `publisher`. /// 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> { pub fn open<P: AsRef<Path>>(path: P, publisher: impl Into<String>) -> Result<Self> {
let backend = FileBackend::open(path)?; 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. /// 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())); return Err(PublisherError::InvalidRoot(root.display().to_string()));
} }
let mut manifest = Manifest::new(); 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 // Ensure a transaction is open
if self.tx.is_none() { if self.tx.is_none() {
self.open_transaction()?; self.open_transaction()?;

View file

@ -21,7 +21,8 @@ mod tests {
let repo_path = tmp.path().to_path_buf(); let repo_path = tmp.path().to_path_buf();
// Initialize repository // 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"); backend.add_publisher("test").expect("add publisher");
// Prepare a prototype directory with a nested file // Prepare a prototype directory with a nested file
@ -36,16 +37,27 @@ mod tests {
// Use PublisherClient to publish // Use PublisherClient to publish
let mut client = PublisherClient::open(&repo_path, "test").expect("open client"); let mut client = PublisherClient::open(&repo_path, "test").expect("open client");
client.open_transaction().expect("open tx"); 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"); client.publish(manifest, true).expect("publish");
// Verify the manifest exists at the default path for unknown version // Verify the manifest exists at the default path for unknown version
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest"); let manifest_path =
assert!(manifest_path.exists(), "manifest not found at {}", manifest_path.display()); 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 // Verify at least one file was stored under publisher/test/file
let file_root = repo_path.join("publisher").join("test").join("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; let mut any_file = false;
if let Ok(entries) = fs::read_dir(&file_root) { if let Ok(entries) = fs::read_dir(&file_root) {
for entry in entries.flatten() { for entry in entries.flatten() {
@ -62,14 +74,15 @@ mod tests {
} else if path.is_file() { } else if path.is_file() {
any_file = true; any_file = true;
} }
if any_file { break; } if any_file {
break;
}
} }
} }
assert!(any_file, "no stored file found in file store"); assert!(any_file, "no stored file found in file store");
} }
} }
#[cfg(test)] #[cfg(test)]
mod transform_rule_integration_tests { mod transform_rule_integration_tests {
use crate::actions::Manifest; use crate::actions::Manifest;
@ -85,7 +98,8 @@ mod transform_rule_integration_tests {
// Setup repository and publisher // Setup repository and publisher
let tmp = TempDir::new().expect("tempdir"); let tmp = TempDir::new().expect("tempdir");
let repo_path = tmp.path().to_path_buf(); 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"); backend.add_publisher("test").expect("add publisher");
// Prototype directory with a file // Prototype directory with a file
@ -102,18 +116,33 @@ mod transform_rule_integration_tests {
// Use PublisherClient to load rules, build manifest and publish // Use PublisherClient to load rules, build manifest and publish
let mut client = PublisherClient::open(&repo_path, "test").expect("open client"); 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"); assert!(loaded >= 1, "expected at least one rule loaded");
client.open_transaction().expect("open tx"); 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"); client.publish(manifest, false).expect("publish");
// Read stored manifest and verify attribute // Read stored manifest and verify attribute
let manifest_path = FileBackend::construct_package_dir(&repo_path, "test", "unknown").join("manifest"); let manifest_path =
assert!(manifest_path.exists(), "manifest missing: {}", manifest_path.display()); 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 json = fs::read_to_string(&manifest_path).expect("read manifest");
let parsed: Manifest = serde_json::from_str(&json).expect("parse manifest json"); 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")); let has_summary = parsed
assert!(has_summary, "pkg.summary attribute added via rules not found"); .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<()> { fn atomic_write_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or(Path::new(".")); let parent = path.parent().unwrap_or(Path::new("."));
fs::create_dir_all(parent) fs::create_dir_all(parent).map_err(|e| RepositoryError::DirectoryCreateError {
.map_err(|e| RepositoryError::DirectoryCreateError { path: parent.to_path_buf(), source: e })?; path: parent.to_path_buf(),
source: e,
})?;
let tmp: PathBuf = path.with_extension("tmp"); let tmp: PathBuf = path.with_extension("tmp");
{ {
let mut f = std::fs::File::create(&tmp) let mut f = std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError {
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; path: tmp.clone(),
source: e,
})?;
f.write_all(bytes) f.write_all(bytes)
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; .map_err(|e| RepositoryError::FileWriteError {
f.flush() path: tmp.clone(),
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; source: e,
})?;
f.flush().map_err(|e| RepositoryError::FileWriteError {
path: tmp.clone(),
source: e,
})?;
} }
fs::rename(&tmp, path) fs::rename(&tmp, path).map_err(|e| RepositoryError::FileWriteError {
.map_err(|e| RepositoryError::FileWriteError { path: path.to_path_buf(), source: e })? path: path.to_path_buf(),
; source: e,
})?;
Ok(()) 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> { pub(crate) fn write_catalog_attrs(path: &Path, attrs: &mut CatalogAttrs) -> Result<String> {
// Compute signature over content without _SIGNATURE // Compute signature over content without _SIGNATURE
attrs.signature = None; attrs.signature = None;
let bytes_without_sig = serde_json::to_vec(&attrs) let bytes_without_sig = serde_json::to_vec(&attrs).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e))
})?;
let sig = sha1_hex(&bytes_without_sig); let sig = sha1_hex(&bytes_without_sig);
let mut sig_map = std::collections::HashMap::new(); let mut sig_map = std::collections::HashMap::new();
sig_map.insert("sha-1".to_string(), sig); sig_map.insert("sha-1".to_string(), sig);
attrs.signature = Some(sig_map); attrs.signature = Some(sig_map);
let final_bytes = serde_json::to_vec(&attrs) let final_bytes = serde_json::to_vec(&attrs).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Catalog attrs serialize error: {}", e))
})?;
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog.attrs"); debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog.attrs");
atomic_write_bytes(path, &final_bytes)?; atomic_write_bytes(path, &final_bytes)?;
// safe to unwrap as signature was just inserted // 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))] #[instrument(level = "debug", skip(part))]
pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result<String> { pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result<String> {
// Compute signature over content without _SIGNATURE // Compute signature over content without _SIGNATURE
part.signature = None; part.signature = None;
let bytes_without_sig = serde_json::to_vec(&part) let bytes_without_sig = serde_json::to_vec(&part).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e))
})?;
let sig = sha1_hex(&bytes_without_sig); let sig = sha1_hex(&bytes_without_sig);
let mut sig_map = std::collections::HashMap::new(); let mut sig_map = std::collections::HashMap::new();
sig_map.insert("sha-1".to_string(), sig); sig_map.insert("sha-1".to_string(), sig);
part.signature = Some(sig_map); part.signature = Some(sig_map);
let final_bytes = serde_json::to_vec(&part) let final_bytes = serde_json::to_vec(&part).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Catalog part serialize error: {}", e))
})?;
debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part"); debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part");
atomic_write_bytes(path, &final_bytes)?; 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))] #[instrument(level = "debug", skip(log))]
pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result<String> { pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result<String> {
// Compute signature over content without _SIGNATURE // Compute signature over content without _SIGNATURE
log.signature = None; log.signature = None;
let bytes_without_sig = serde_json::to_vec(&log) let bytes_without_sig = serde_json::to_vec(&log).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
})?;
let sig = sha1_hex(&bytes_without_sig); let sig = sha1_hex(&bytes_without_sig);
let mut sig_map = std::collections::HashMap::new(); let mut sig_map = std::collections::HashMap::new();
sig_map.insert("sha-1".to_string(), sig); sig_map.insert("sha-1".to_string(), sig);
log.signature = Some(sig_map); log.signature = Some(sig_map);
let final_bytes = serde_json::to_vec(&log) let final_bytes = serde_json::to_vec(&log).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
})?;
debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log"); debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log");
atomic_write_bytes(path, &final_bytes)?; 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/. // obtain one at https://mozilla.org/MPL/2.0/.
use super::{RepositoryError, Result}; use super::{RepositoryError, Result};
use flate2::write::GzEncoder;
use flate2::Compression as GzipCompression; use flate2::Compression as GzipCompression;
use flate2::write::GzEncoder;
use lz4::EncoderBuilder; use lz4::EncoderBuilder;
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -25,11 +25,11 @@ use crate::digest::Digest;
use crate::fmri::Fmri; use crate::fmri::Fmri;
use crate::payload::{Payload, PayloadCompressionAlgorithm}; use crate::payload::{Payload, PayloadCompressionAlgorithm};
use super::{
PackageContents, PackageInfo, PublisherInfo, ReadableRepository, RepositoryConfig,
RepositoryInfo, RepositoryVersion, WritableRepository, REPOSITORY_CONFIG_FILENAME,
};
use super::catalog_writer; use super::catalog_writer;
use super::{
PackageContents, PackageInfo, PublisherInfo, REPOSITORY_CONFIG_FILENAME, ReadableRepository,
RepositoryConfig, RepositoryInfo, RepositoryVersion, WritableRepository,
};
use ini::Ini; use ini::Ini;
// Define a struct to hold the content vectors for each package // 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 /// Uses RefCell for interior mutability to allow mutation through immutable references
catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>, catalog_manager: Option<std::cell::RefCell<crate::repository::catalog::CatalogManager>>,
/// Manager for obsoleted packages /// 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 /// Format a SystemTime as an ISO 8601 timestamp string
@ -342,20 +343,16 @@ impl Transaction {
// Check if the temp file already exists // Check if the temp file already exists
if temp_file_path.exists() { if temp_file_path.exists() {
// If it exists, remove it to avoid any issues with existing content // If it exists, remove it to avoid any issues with existing content
fs::remove_file(&temp_file_path).map_err(|e| { fs::remove_file(&temp_file_path).map_err(|e| RepositoryError::FileWriteError {
RepositoryError::FileWriteError { path: temp_file_path.clone(),
path: temp_file_path.clone(), source: e,
source: e,
}
})?; })?;
} }
// Read the file content // Read the file content
let file_content = fs::read(file_path).map_err(|e| { let file_content = fs::read(file_path).map_err(|e| RepositoryError::FileReadError {
RepositoryError::FileReadError { path: file_path.to_path_buf(),
path: file_path.to_path_buf(), source: e,
source: e,
}
})?; })?;
// Create a payload with the hash information if it doesn't exist // Create a payload with the hash information if it doesn't exist
@ -493,7 +490,8 @@ impl Transaction {
// Copy files to their final location // Copy files to their final location
for (source_path, hash) in self.files { for (source_path, hash) in self.files {
// Create the destination path using the helper function with publisher // 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 // Create parent directories if they don't exist
if let Some(parent) = dest_path.parent() { if let Some(parent) = dest_path.parent() {
@ -567,7 +565,8 @@ impl Transaction {
// Construct the manifest path using the helper method // Construct the manifest path using the helper method
let pkg_manifest_path = if package_version.is_empty() { let pkg_manifest_path = if package_version.is_empty() {
// If no version was provided, store as a default manifest file // 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 { } else {
FileBackend::construct_manifest_path( FileBackend::construct_manifest_path(
&self.repo, &self.repo,
@ -597,14 +596,14 @@ impl Transaction {
if config_path.exists() { if config_path.exists() {
let config_content = fs::read_to_string(&config_path)?; let config_content = fs::read_to_string(&config_path)?;
let config: RepositoryConfig = serde_json::from_str(&config_content)?; let config: RepositoryConfig = serde_json::from_str(&config_content)?;
// Check if this publisher was just added in this transaction // Check if this publisher was just added in this transaction
let publisher_dir = self.repo.join("publisher").join(&publisher); let publisher_dir = self.repo.join("publisher").join(&publisher);
let pub_p5i_path = publisher_dir.join("pub.p5i"); let pub_p5i_path = publisher_dir.join("pub.p5i");
if !pub_p5i_path.exists() { if !pub_p5i_path.exists() {
debug!("Creating pub.p5i file for publisher: {}", publisher); debug!("Creating pub.p5i file for publisher: {}", publisher);
// Create the pub.p5i file // Create the pub.p5i file
let repo = FileBackend { let repo = FileBackend {
path: self.repo.clone(), path: self.repo.clone(),
@ -612,7 +611,7 @@ impl Transaction {
catalog_manager: None, catalog_manager: None,
obsoleted_manager: None, obsoleted_manager: None,
}; };
repo.create_pub_p5i_file(&publisher)?; repo.create_pub_p5i_file(&publisher)?;
} }
} }
@ -667,13 +666,15 @@ impl ReadableRepository for FileBackend {
let config5_path = path.join("pkg5.repository"); let config5_path = path.join("pkg5.repository");
let config: RepositoryConfig = if config6_path.exists() { let config: RepositoryConfig = if config6_path.exists() {
let config_data = fs::read_to_string(&config6_path) let config_data = fs::read_to_string(&config6_path).map_err(|e| {
.map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e)))?; RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e))
})?;
serde_json::from_str(&config_data)? serde_json::from_str(&config_data)?
} else if config5_path.exists() { } else if config5_path.exists() {
// Minimal mapping for legacy INI: take publishers only from INI; do not scan disk. // Minimal mapping for legacy INI: take publishers only from INI; do not scan disk.
let ini = Ini::load_from_file(&config5_path) let ini = Ini::load_from_file(&config5_path).map_err(|e| {
.map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e)))?; RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e))
})?;
// Default repository version for legacy format is v4 // Default repository version for legacy format is v4
let mut cfg = RepositoryConfig::default(); let mut cfg = RepositoryConfig::default();
@ -829,7 +830,10 @@ impl ReadableRepository for FileBackend {
pattern: Option<&str>, pattern: Option<&str>,
action_types: Option<&[String]>, action_types: Option<&[String]>,
) -> Result<Vec<PackageContents>> { ) -> 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 // Use a HashMap to store package information
let mut packages = HashMap::new(); let mut packages = HashMap::new();
@ -889,7 +893,9 @@ impl ReadableRepository for FileBackend {
// Check if the file starts with a valid manifest marker // Check if the file starts with a valid manifest marker
if bytes_read == 0 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; continue;
} }
@ -901,7 +907,9 @@ impl ReadableRepository for FileBackend {
let mut pkg_id = String::new(); let mut pkg_id = String::new();
for attr in &manifest.attributes { 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]; let fmri = &attr.values[0];
// Parse the FMRI using our Fmri type // Parse the FMRI using our Fmri type
@ -913,14 +921,22 @@ impl ReadableRepository for FileBackend {
match Regex::new(pat) { match Regex::new(pat) {
Ok(regex) => { Ok(regex) => {
// Use regex matching // Use regex matching
if !regex.is_match(parsed_fmri.stem()) { if !regex.is_match(
parsed_fmri.stem(),
) {
continue; continue;
} }
} }
Err(err) => { Err(err) => {
// Log the error but fall back to the simple string contains // Log the error but fall back to the simple string contains
error!("FileBackend::show_contents: Error compiling regex pattern '{}': {}", pat, err); error!(
if !parsed_fmri.stem().contains(pat) { "FileBackend::show_contents: Error compiling regex pattern '{}': {}",
pat, err
);
if !parsed_fmri
.stem()
.contains(pat)
{
continue; continue;
} }
} }
@ -970,7 +986,9 @@ impl ReadableRepository for FileBackend {
.contains(&"file".to_string()) .contains(&"file".to_string())
{ {
for file in &manifest.files { 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()) .contains(&"dir".to_string())
{ {
for dir in &manifest.directories { 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()) .contains(&"link".to_string())
{ {
for link in &manifest.links { 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 { for depend in &manifest.dependencies {
if let Some(fmri) = &depend.fmri { 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()) .contains(&"license".to_string())
{ {
for license in &manifest.licenses { for license in &manifest.licenses {
if let Some(path_prop) = license.properties.get("path") { if let Some(path_prop) =
content_vectors.licenses.push(path_prop.value.clone()); license.properties.get("path")
} else if let Some(license_prop) = license.properties.get("license") { {
content_vectors.licenses.push(license_prop.value.clone()); 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 { } 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) => { Err(err) => {
// Log the error but fall back to the simple string contains // 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) if !parsed_fmri.stem().contains(pat)
{ {
continue; continue;
@ -1323,16 +1360,30 @@ impl ReadableRepository for FileBackend {
// If destination already exists and matches digest, do nothing // If destination already exists and matches digest, do nothing
if dest.exists() { if dest.exists() {
let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError { path: dest.to_path_buf(), source: e })?; let bytes = fs::read(dest).map_err(|e| RepositoryError::FileReadError {
match crate::digest::Digest::from_bytes(&bytes, algo.clone(), crate::digest::DigestSource::PrimaryPayloadHash) { 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(()), Ok(comp) if comp.hash == hash => return Ok(()),
_ => { /* fall through to overwrite */ } _ => { /* fall through to overwrite */ }
} }
} }
// Read source content and verify digest // Read source content and verify digest
let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError { path: source_path.clone(), source: e })?; let bytes = fs::read(&source_path).map_err(|e| RepositoryError::FileReadError {
match crate::digest::Digest::from_bytes(&bytes, algo, crate::digest::DigestSource::PrimaryPayloadHash) { path: source_path.clone(),
source: e,
})?;
match crate::digest::Digest::from_bytes(
&bytes,
algo,
crate::digest::DigestSource::PrimaryPayloadHash,
) {
Ok(comp) => { Ok(comp) => {
if comp.hash != hash { if comp.hash != hash {
return Err(RepositoryError::DigestError(format!( return Err(RepositoryError::DigestError(format!(
@ -1363,7 +1414,9 @@ impl ReadableRepository for FileBackend {
// Require a concrete version // Require a concrete version
let version = fmri.version(); let version = fmri.version();
if version.is_empty() { 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 // Preferred path: publisher-scoped manifest path
@ -1375,7 +1428,11 @@ impl ReadableRepository for FileBackend {
// Fallbacks: global pkg layout without publisher // Fallbacks: global pkg layout without publisher
let encoded_stem = Self::url_encode(fmri.stem()); let encoded_stem = Self::url_encode(fmri.stem());
let encoded_version = Self::url_encode(&version); 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() { if alt1.exists() {
return crate::actions::Manifest::parse_file(&alt1).map_err(RepositoryError::from); return crate::actions::Manifest::parse_file(&alt1).map_err(RepositoryError::from);
} }
@ -1520,10 +1577,10 @@ impl WritableRepository for FileBackend {
let config_path = self.path.join(REPOSITORY_CONFIG_FILENAME); let config_path = self.path.join(REPOSITORY_CONFIG_FILENAME);
let config_data = serde_json::to_string_pretty(&self.config)?; let config_data = serde_json::to_string_pretty(&self.config)?;
fs::write(config_path, config_data)?; fs::write(config_path, config_data)?;
// Save the legacy INI format for backward compatibility // Save the legacy INI format for backward compatibility
self.save_legacy_config()?; self.save_legacy_config()?;
Ok(()) Ok(())
} }
@ -1744,7 +1801,10 @@ impl FileBackend {
locale: &str, locale: &str,
fmri: &crate::fmri::Fmri, fmri: &crate::fmri::Fmri,
op_type: crate::repository::catalog::CatalogOperationType, 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>, signature_sha1: Option<String>,
) -> Result<()> { ) -> Result<()> {
let catalog_dir = Self::construct_catalog_path(&self.path, publisher); let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
@ -1816,19 +1876,29 @@ impl FileBackend {
// Require a concrete version // Require a concrete version
let version = fmri.version(); let version = fmri.version();
if version.is_empty() { 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 // Preferred path: publisher-scoped manifest path
let path = Self::construct_manifest_path(&self.path, publisher, fmri.stem(), &version); let path = Self::construct_manifest_path(&self.path, publisher, fmri.stem(), &version);
if path.exists() { 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 // Fallbacks: global pkg layout without publisher
let encoded_stem = Self::url_encode(fmri.stem()); let encoded_stem = Self::url_encode(fmri.stem());
let encoded_version = Self::url_encode(&version); 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() { 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 let alt2 = self
.path .path
@ -1838,9 +1908,15 @@ impl FileBackend {
.join(&encoded_stem) .join(&encoded_stem)
.join(&encoded_version); .join(&encoded_version);
if alt2.exists() { 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 /// Fetch catalog file path
pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> { pub fn get_catalog_file_path(&self, publisher: &str, filename: &str) -> Result<PathBuf> {
@ -1865,32 +1941,31 @@ impl FileBackend {
pub fn save_legacy_config(&self) -> Result<()> { pub fn save_legacy_config(&self) -> Result<()> {
let legacy_config_path = self.path.join("pkg5.repository"); let legacy_config_path = self.path.join("pkg5.repository");
let mut conf = Ini::new(); let mut conf = Ini::new();
// Add publisher section with default publisher // Add publisher section with default publisher
if let Some(default_publisher) = &self.config.default_publisher { if let Some(default_publisher) = &self.config.default_publisher {
conf.with_section(Some("publisher")) conf.with_section(Some("publisher"))
.set("prefix", default_publisher); .set("prefix", default_publisher);
} }
// Add repository section with version and default values // Add repository section with version and default values
conf.with_section(Some("repository")) conf.with_section(Some("repository"))
.set("version", "4") .set("version", "4")
.set("trust-anchor-directory", "/etc/certs/CA/") .set("trust-anchor-directory", "/etc/certs/CA/")
.set("signature-required-names", "[]") .set("signature-required-names", "[]")
.set("check-certificate-revocation", "False"); .set("check-certificate-revocation", "False");
// Add CONFIGURATION section with version // Add CONFIGURATION section with version
conf.with_section(Some("CONFIGURATION")) conf.with_section(Some("CONFIGURATION")).set("version", "4");
.set("version", "4");
// Write the INI file // Write the INI file
conf.write_to_file(legacy_config_path)?; conf.write_to_file(legacy_config_path)?;
Ok(()) Ok(())
} }
/// Create a pub.p5i file for a publisher for backward compatibility /// Create a pub.p5i file for a publisher for backward compatibility
/// ///
/// Format: base_path/publisher/publisher_name/pub.p5i /// Format: base_path/publisher/publisher_name/pub.p5i
fn create_pub_p5i_file(&self, publisher: &str) -> Result<()> { fn create_pub_p5i_file(&self, publisher: &str) -> Result<()> {
// Define the structure for the pub.p5i file // Define the structure for the pub.p5i file
@ -1937,17 +2012,14 @@ impl FileBackend {
} }
/// Helper method to construct a catalog path consistently /// Helper method to construct a catalog path consistently
/// ///
/// Format: base_path/publisher/publisher_name/catalog /// Format: base_path/publisher/publisher_name/catalog
pub fn construct_catalog_path( pub fn construct_catalog_path(base_path: &Path, publisher: &str) -> PathBuf {
base_path: &Path,
publisher: &str,
) -> PathBuf {
base_path.join("publisher").join(publisher).join("catalog") base_path.join("publisher").join(publisher).join("catalog")
} }
/// Helper method to construct a manifest path consistently /// Helper method to construct a manifest path consistently
/// ///
/// Format: base_path/publisher/publisher_name/pkg/stem/encoded_version /// Format: base_path/publisher/publisher_name/pkg/stem/encoded_version
pub fn construct_manifest_path( pub fn construct_manifest_path(
base_path: &Path, base_path: &Path,
@ -1959,27 +2031,24 @@ impl FileBackend {
let encoded_version = Self::url_encode(version); let encoded_version = Self::url_encode(version);
pkg_dir.join(encoded_version) pkg_dir.join(encoded_version)
} }
/// Helper method to construct a package directory path consistently /// Helper method to construct a package directory path consistently
/// ///
/// Format: base_path/publisher/publisher_name/pkg/url_encoded_stem /// Format: base_path/publisher/publisher_name/pkg/url_encoded_stem
pub fn construct_package_dir( pub fn construct_package_dir(base_path: &Path, publisher: &str, stem: &str) -> PathBuf {
base_path: &Path,
publisher: &str,
stem: &str,
) -> PathBuf {
let encoded_stem = Self::url_encode(stem); 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 /// Helper method to construct a file path consistently
/// ///
/// Format: base_path/file/XX/hash /// Format: base_path/file/XX/hash
/// Where XX is the first two characters of the hash /// Where XX is the first two characters of the hash
pub fn construct_file_path( pub fn construct_file_path(base_path: &Path, hash: &str) -> PathBuf {
base_path: &Path,
hash: &str,
) -> PathBuf {
if hash.len() < 2 { if hash.len() < 2 {
// Fallback for very short hashes (shouldn't happen with SHA256) // Fallback for very short hashes (shouldn't happen with SHA256)
base_path.join("file").join(hash) base_path.join("file").join(hash)
@ -1988,15 +2057,12 @@ impl FileBackend {
let first_two = &hash[0..2]; let first_two = &hash[0..2];
// Create the path: $REPO/file/XX/XXYY... // Create the path: $REPO/file/XX/XXYY...
base_path base_path.join("file").join(first_two).join(hash)
.join("file")
.join(first_two)
.join(hash)
} }
} }
/// Helper method to construct a file path consistently with publisher /// Helper method to construct a file path consistently with publisher
/// ///
/// Format: base_path/publisher/publisher_name/file/XX/hash /// Format: base_path/publisher/publisher_name/file/XX/hash
/// Where XX is the first two characters of the hash /// Where XX is the first two characters of the hash
pub fn construct_file_path_with_publisher( pub fn construct_file_path_with_publisher(
@ -2006,7 +2072,11 @@ impl FileBackend {
) -> PathBuf { ) -> PathBuf {
if hash.len() < 2 { if hash.len() < 2 {
// Fallback for very short hashes (shouldn't happen with SHA256) // 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 { } else {
// Extract the first two characters from the hash // Extract the first two characters from the hash
let first_two = &hash[0..2]; let first_two = &hash[0..2];
@ -2094,7 +2164,10 @@ impl FileBackend {
} }
Err(err) => { Err(err) => {
// Log the error but fall back to the simple string contains // 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) { if !parsed_fmri.stem().contains(pat) {
continue; continue;
} }
@ -2111,20 +2184,22 @@ impl FileBackend {
} else { } else {
parsed_fmri.clone() parsed_fmri.clone()
}; };
// Check if the package is obsoleted // Check if the package is obsoleted
let is_obsoleted = if let Some(obsoleted_manager) = &self.obsoleted_manager { let is_obsoleted = if let Some(obsoleted_manager) =
obsoleted_manager.borrow().is_obsoleted(publisher, &final_fmri) &self.obsoleted_manager
{
obsoleted_manager
.borrow()
.is_obsoleted(publisher, &final_fmri)
} else { } else {
false false
}; };
// Only add the package if it's not obsoleted // Only add the package if it's not obsoleted
if !is_obsoleted { if !is_obsoleted {
// Create a PackageInfo struct and add it to the list // Create a PackageInfo struct and add it to the list
packages.push(PackageInfo { packages.push(PackageInfo { fmri: final_fmri });
fmri: final_fmri,
});
} }
// Found the package info, no need to check other attributes // Found the package info, no need to check other attributes
@ -2186,7 +2261,7 @@ impl FileBackend {
opts: crate::repository::BatchOptions, opts: crate::repository::BatchOptions,
) -> Result<()> { ) -> Result<()> {
info!("Rebuilding catalog (batched) for publisher: {}", publisher); info!("Rebuilding catalog (batched) for publisher: {}", publisher);
// Create the catalog directory for the publisher if it doesn't exist // Create the catalog directory for the publisher if it doesn't exist
let catalog_dir = Self::construct_catalog_path(&self.path, publisher); let catalog_dir = Self::construct_catalog_path(&self.path, publisher);
debug!("Publisher catalog directory: {}", catalog_dir.display()); debug!("Publisher catalog directory: {}", catalog_dir.display());
@ -2245,7 +2320,11 @@ impl FileBackend {
} }
// Read the manifest content for hash calculation // 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 // Parse the manifest using parse_file which handles JSON correctly
let manifest = Manifest::parse_file(&manifest_path)?; let manifest = Manifest::parse_file(&manifest_path)?;
@ -2334,7 +2413,12 @@ impl FileBackend {
processed_in_batch += 1; processed_in_batch += 1;
if processed_in_batch >= opts.batch_size { if processed_in_batch >= opts.batch_size {
batch_no += 1; 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; processed_in_batch = 0;
} }
} }
@ -2407,7 +2491,8 @@ impl FileBackend {
for (fmri, actions, signature) in dependency_entries { for (fmri, actions, signature) in dependency_entries {
dependency_part.add_package(publisher, &fmri, actions, Some(signature)); 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"); debug!("Wrote dependency part file");
// Summary part // Summary part
@ -2417,7 +2502,8 @@ impl FileBackend {
for (fmri, actions, signature) in summary_entries { for (fmri, actions, signature) in summary_entries {
summary_part.add_package(publisher, &fmri, actions, Some(signature)); 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"); debug!("Wrote summary part file");
// Update part signatures in attrs (written after parts) // Update part signatures in attrs (written after parts)
@ -2495,29 +2581,46 @@ impl FileBackend {
// Ensure catalog dir exists // Ensure catalog dir exists
let catalog_dir = Self::construct_catalog_path(&self.path, publisher); 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 // Serialize JSON
let json = serde_json::to_vec_pretty(log) let json = serde_json::to_vec_pretty(log).map_err(|e| {
.map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e))
})?;
// Write atomically // Write atomically
let target = catalog_dir.join(log_filename); let target = catalog_dir.join(log_filename);
let tmp = target.with_extension("tmp"); let tmp = target.with_extension("tmp");
{ {
let mut f = std::fs::File::create(&tmp) let mut f =
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; std::fs::File::create(&tmp).map_err(|e| RepositoryError::FileWriteError {
path: tmp.clone(),
source: e,
})?;
use std::io::Write as _; use std::io::Write as _;
f.write_all(&json) f.write_all(&json)
.map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; .map_err(|e| RepositoryError::FileWriteError {
f.flush().map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; path: tmp.clone(),
source: e,
})?;
f.flush().map_err(|e| RepositoryError::FileWriteError {
path: tmp.clone(),
source: e,
})?;
} }
std::fs::rename(&tmp, &target) std::fs::rename(&tmp, &target).map_err(|e| RepositoryError::FileWriteError {
.map_err(|e| RepositoryError::FileWriteError { path: target.clone(), source: e })?; path: target.clone(),
source: e,
})?;
Ok(()) Ok(())
} }
/// Generate the file path for a given hash using the new directory structure with publisher /// Generate the file path for a given hash using the new directory structure with publisher
/// This is a wrapper around the construct_file_path_with_publisher helper method /// This is a wrapper around the construct_file_path_with_publisher helper method
fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf { fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf {
@ -2528,7 +2631,7 @@ impl FileBackend {
/// ///
/// This method returns a mutable reference to the catalog manager. /// This method returns a mutable reference to the catalog manager.
/// It uses interior mutability with RefCell to allow mutation through an immutable reference. /// It uses interior mutability with RefCell to allow mutation through an immutable reference.
/// ///
/// The catalog manager is specific to the given publisher. /// The catalog manager is specific to the given publisher.
pub fn get_catalog_manager( pub fn get_catalog_manager(
&mut self, &mut self,
@ -2536,7 +2639,8 @@ impl FileBackend {
) -> Result<std::cell::RefMut<'_, crate::repository::catalog::CatalogManager>> { ) -> Result<std::cell::RefMut<'_, crate::repository::catalog::CatalogManager>> {
if self.catalog_manager.is_none() { if self.catalog_manager.is_none() {
let publisher_dir = self.path.join("publisher"); 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); let refcell = std::cell::RefCell::new(manager);
self.catalog_manager = Some(refcell); self.catalog_manager = Some(refcell);
} }
@ -2544,7 +2648,7 @@ impl FileBackend {
// This is safe because we just checked that catalog_manager is Some // This is safe because we just checked that catalog_manager is Some
Ok(self.catalog_manager.as_ref().unwrap().borrow_mut()) Ok(self.catalog_manager.as_ref().unwrap().borrow_mut())
} }
/// Get or initialize the obsoleted package manager /// Get or initialize the obsoleted package manager
/// ///
/// This method returns a mutable reference to the obsoleted package manager. /// This method returns a mutable reference to the obsoleted package manager.
@ -2597,7 +2701,7 @@ impl FileBackend {
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
{ {
let path = entry.path(); let path = entry.path();
if path.is_file() { if path.is_file() {
// Try to read the first few bytes of the file to check if it's a manifest file // Try to read the first few bytes of the file to check if it's a manifest file
let mut file = match fs::File::open(&path) { let mut file = match fs::File::open(&path) {
@ -2669,18 +2773,17 @@ impl FileBackend {
None None
}; };
let directories = let directories = if !manifest.directories.is_empty() {
if !manifest.directories.is_empty() { Some(
Some( manifest
manifest .directories
.directories .iter()
.iter() .map(|d| d.path.clone())
.map(|d| d.path.clone()) .collect(),
.collect(), )
) } else {
} else { None
None };
};
let links = if !manifest.links.is_empty() { let links = if !manifest.links.is_empty() {
Some( Some(
@ -2694,22 +2797,20 @@ impl FileBackend {
None None
}; };
let dependencies = let dependencies = if !manifest.dependencies.is_empty()
if !manifest.dependencies.is_empty() { {
Some( Some(
manifest manifest
.dependencies .dependencies
.iter() .iter()
.filter_map(|d| { .filter_map(|d| {
d.fmri d.fmri.as_ref().map(|f| f.to_string())
.as_ref() })
.map(|f| f.to_string()) .collect(),
}) )
.collect(), } else {
) None
} else { };
None
};
let licenses = if !manifest.licenses.is_empty() { let licenses = if !manifest.licenses.is_empty() {
Some( Some(
@ -2746,8 +2847,11 @@ impl FileBackend {
}; };
// Add the package to the index // 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 // Found the package info, no need to check other attributes
break; break;
} }

View file

@ -216,8 +216,8 @@ impl From<bincode::error::EncodeError> for RepositoryError {
} }
} }
pub mod catalog; pub mod catalog;
pub(crate) mod file_backend;
mod catalog_writer; mod catalog_writer;
pub(crate) mod file_backend;
mod obsoleted; mod obsoleted;
pub mod progress; pub mod progress;
mod rest_backend; mod rest_backend;
@ -231,7 +231,7 @@ pub use catalog::{
}; };
pub use file_backend::FileBackend; pub use file_backend::FileBackend;
pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata}; pub use obsoleted::{ObsoletedPackageManager, ObsoletedPackageMetadata};
pub use progress::{ProgressInfo, ProgressReporter, NoopProgressReporter}; pub use progress::{NoopProgressReporter, ProgressInfo, ProgressReporter};
pub use rest_backend::RestBackend; pub use rest_backend::RestBackend;
/// Repository configuration filename /// Repository configuration filename
@ -248,7 +248,10 @@ pub struct BatchOptions {
impl Default for BatchOptions { impl Default for BatchOptions {
fn default() -> Self { 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. /// Fetch a content payload identified by digest into the destination path.
/// Implementations should download/copy the payload to a temporary path, /// Implementations should download/copy the payload to a temporary path,
/// verify integrity, and atomically move into `dest`. /// verify integrity, and atomically move into `dest`.
fn fetch_payload( fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()>;
&mut self,
publisher: &str,
digest: &str,
dest: &Path,
) -> Result<()>;
/// Fetch a package manifest by FMRI from the repository. /// Fetch a package manifest by FMRI from the repository.
/// Implementations should retrieve and parse the manifest for the given /// Implementations should retrieve and parse the manifest for the given

File diff suppressed because it is too large Load diff

View file

@ -63,13 +63,13 @@ pub trait ProgressReporter {
pub struct ProgressInfo { pub struct ProgressInfo {
/// The name of the operation being performed /// The name of the operation being performed
pub operation: String, pub operation: String,
/// The current progress value (e.g., bytes downloaded, files processed) /// The current progress value (e.g., bytes downloaded, files processed)
pub current: Option<u64>, pub current: Option<u64>,
/// The total expected value (e.g., total bytes, total files) /// The total expected value (e.g., total bytes, total files)
pub total: Option<u64>, pub total: Option<u64>,
/// Additional context about the operation (e.g., current file name) /// Additional context about the operation (e.g., current file name)
pub context: Option<String>, pub context: Option<String>,
} }
@ -139,18 +139,18 @@ impl ProgressInfo {
impl fmt::Display for ProgressInfo { impl fmt::Display for ProgressInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.operation)?; write!(f, "{}", self.operation)?;
if let (Some(current), Some(total)) = (self.current, self.total) { if let (Some(current), Some(total)) = (self.current, self.total) {
let percentage = (current as f64 / total as f64) * 100.0; let percentage = (current as f64 / total as f64) * 100.0;
write!(f, " {:.1}% ({}/{})", percentage, current, total)?; write!(f, " {:.1}% ({}/{})", percentage, current, total)?;
} else if let Some(current) = self.current { } else if let Some(current) = self.current {
write!(f, " {}", current)?; write!(f, " {}", current)?;
} }
if let Some(context) = &self.context { if let Some(context) = &self.context {
write!(f, " - {}", context)?; write!(f, " - {}", context)?;
} }
Ok(()) Ok(())
} }
} }
@ -165,4 +165,4 @@ impl ProgressReporter for NoopProgressReporter {
fn start(&self, _info: &ProgressInfo) {} fn start(&self, _info: &ProgressInfo) {}
fn update(&self, _info: &ProgressInfo) {} fn update(&self, _info: &ProgressInfo) {}
fn finish(&self, _info: &ProgressInfo) {} fn finish(&self, _info: &ProgressInfo) {}
} }

View file

@ -13,12 +13,12 @@ use tracing::{debug, info, warn};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use serde_json::Value; use serde_json::Value;
use super::catalog::CatalogManager;
use super::{ use super::{
NoopProgressReporter, PackageContents, PackageInfo, ProgressInfo, ProgressReporter, NoopProgressReporter, PackageContents, PackageInfo, ProgressInfo, ProgressReporter,
PublisherInfo, ReadableRepository, RepositoryConfig, RepositoryError, RepositoryInfo, PublisherInfo, ReadableRepository, RepositoryConfig, RepositoryError, RepositoryInfo,
RepositoryVersion, Result, WritableRepository, RepositoryVersion, Result, WritableRepository,
}; };
use super::catalog::CatalogManager;
/// Repository implementation that uses a REST API to interact with a remote repository. /// Repository implementation that uses a REST API to interact with a remote repository.
/// ///
@ -125,27 +125,33 @@ impl WritableRepository for RestBackend {
println!("Creating publisher directory..."); println!("Creating publisher directory...");
let publisher_dir = cache_path.join("publisher").join(publisher); let publisher_dir = cache_path.join("publisher").join(publisher);
println!("Publisher directory path: {}", publisher_dir.display()); println!("Publisher directory path: {}", publisher_dir.display());
match fs::create_dir_all(&publisher_dir) { match fs::create_dir_all(&publisher_dir) {
Ok(_) => println!("Successfully created publisher directory"), Ok(_) => println!("Successfully created publisher directory"),
Err(e) => println!("Failed to create publisher directory: {}", e), Err(e) => println!("Failed to create publisher directory: {}", e),
} }
// Check if the directory was created // 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 // Create catalog directory
let catalog_dir = publisher_dir.join("catalog"); let catalog_dir = publisher_dir.join("catalog");
println!("Catalog directory path: {}", catalog_dir.display()); println!("Catalog directory path: {}", catalog_dir.display());
match fs::create_dir_all(&catalog_dir) { match fs::create_dir_all(&catalog_dir) {
Ok(_) => println!("Successfully created catalog directory"), Ok(_) => println!("Successfully created catalog directory"),
Err(e) => println!("Failed to create catalog directory: {}", e), Err(e) => println!("Failed to create catalog directory: {}", e),
} }
// Check if the directory was created // 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()); debug!("Created publisher directory: {}", publisher_dir.display());
} else { } else {
println!("No local cache path set, skipping directory creation"); println!("No local cache path set, skipping directory creation");
@ -256,10 +262,12 @@ impl WritableRepository for RestBackend {
client: Client::new(), client: Client::new(),
catalog_managers: HashMap::new(), catalog_managers: HashMap::new(),
}; };
// Check if we have a local cache path // Check if we have a local cache path
if cloned_self.local_cache_path.is_none() { 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 // Filter publishers if specified
@ -316,18 +324,18 @@ impl ReadableRepository for RestBackend {
/// Open an existing repository /// Open an existing repository
fn open<P: AsRef<Path>>(uri: P) -> Result<Self> { fn open<P: AsRef<Path>>(uri: P) -> Result<Self> {
let uri_str = uri.as_ref().to_string_lossy().to_string(); let uri_str = uri.as_ref().to_string_lossy().to_string();
// Create an HTTP client // Create an HTTP client
let client = Client::new(); let client = Client::new();
// Fetch the repository configuration from the remote server // Fetch the repository configuration from the remote server
// We'll try to get the publisher information using the publisher endpoint // We'll try to get the publisher information using the publisher endpoint
let url = format!("{}/publisher/0", uri_str); let url = format!("{}/publisher/0", uri_str);
debug!("Fetching repository configuration from: {}", url); debug!("Fetching repository configuration from: {}", url);
let mut config = RepositoryConfig::default(); let mut config = RepositoryConfig::default();
// Try to fetch publisher information // Try to fetch publisher information
match client.get(&url).send() { match client.get(&url).send() {
Ok(response) => { Ok(response) => {
@ -336,31 +344,36 @@ impl ReadableRepository for RestBackend {
match response.json::<Value>() { match response.json::<Value>() {
Ok(json) => { Ok(json) => {
// Extract publisher information // 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 { for (name, _) in publishers {
debug!("Found publisher: {}", name); debug!("Found publisher: {}", name);
config.publishers.push(name.clone()); config.publishers.push(name.clone());
} }
} }
}, }
Err(e) => { Err(e) => {
warn!("Failed to parse publisher information: {}", e); warn!("Failed to parse publisher information: {}", e);
} }
} }
} else { } else {
warn!("Failed to fetch publisher information: HTTP status {}", response.status()); warn!(
"Failed to fetch publisher information: HTTP status {}",
response.status()
);
} }
}, }
Err(e) => { Err(e) => {
warn!("Failed to connect to repository: {}", e); warn!("Failed to connect to repository: {}", e);
} }
} }
// If we couldn't get any publishers, add a default one // If we couldn't get any publishers, add a default one
if config.publishers.is_empty() { if config.publishers.is_empty() {
config.publishers.push("openindiana.org".to_string()); config.publishers.push("openindiana.org".to_string());
} }
// Create the repository instance // Create the repository instance
Ok(RestBackend { Ok(RestBackend {
uri: uri_str, uri: uri_str,
@ -536,12 +549,7 @@ impl ReadableRepository for RestBackend {
Ok(package_contents) Ok(package_contents)
} }
fn fetch_payload( fn fetch_payload(&mut self, publisher: &str, digest: &str, dest: &Path) -> Result<()> {
&mut self,
publisher: &str,
digest: &str,
dest: &Path,
) -> Result<()> {
// Determine hash and algorithm from the provided digest string // Determine hash and algorithm from the provided digest string
let mut hash = digest.to_string(); let mut hash = digest.to_string();
let mut algo: Option<crate::digest::DigestAlgorithm> = None; 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())); 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![ let candidates = vec![
format!("{}/file/{}/{}", self.uri, shard, hash), 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 // Ensure destination directory exists
@ -571,11 +586,17 @@ impl ReadableRepository for RestBackend {
for url in candidates { for url in candidates {
match self.client.get(&url).send() { match self.client.get(&url).send() {
Ok(resp) if resp.status().is_success() => { 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 // Verify digest if algorithm is known
if let Some(alg) = algo.clone() { 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) => { Ok(comp) => {
if comp.hash != hash { if comp.hash != hash {
return Err(RepositoryError::DigestError(format!( 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( fn fetch_manifest(
@ -636,14 +659,18 @@ impl RestBackend {
// Require versioned FMRI // Require versioned FMRI
let version = fmri.version(); let version = fmri.version();
if version.is_empty() { 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 // URL-encode helper
let url_encode = |s: &str| -> String { let url_encode = |s: &str| -> String {
let mut out = String::new(); let mut out = String::new();
for b in s.bytes() { for b in s.bytes() {
match b { 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('+'), b' ' => out.push('+'),
_ => { _ => {
out.push('%'); out.push('%');
@ -658,16 +685,24 @@ impl RestBackend {
let encoded_version = url_encode(&version); let encoded_version = url_encode(&version);
let candidates = vec![ let candidates = vec![
format!("{}/manifest/0/{}", self.uri, encoded_fmri), 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 // Fallbacks to direct file-style paths if server exposes static files
format!("{}/pkg/{}/{}", self.uri, encoded_stem, encoded_version), 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; let mut last_err: Option<String> = None;
for url in candidates { for url in candidates {
match self.client.get(&url).send() { match self.client.get(&url).send() {
Ok(resp) if resp.status().is_success() => { 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); return Ok(text);
} }
Ok(resp) => { 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. /// Sets the local path where catalog files will be cached.
/// ///
@ -698,15 +735,15 @@ impl RestBackend {
/// Returns an error if the directory could not be created. /// Returns an error if the directory could not be created.
pub fn set_local_cache_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> { pub fn set_local_cache_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.local_cache_path = Some(path.as_ref().to_path_buf()); self.local_cache_path = Some(path.as_ref().to_path_buf());
// Create the directory if it doesn't exist // Create the directory if it doesn't exist
if let Some(path) = &self.local_cache_path { if let Some(path) = &self.local_cache_path {
fs::create_dir_all(path)?; fs::create_dir_all(path)?;
} }
Ok(()) Ok(())
} }
/// Initializes the repository by downloading catalog files for all publishers. /// Initializes the repository by downloading catalog files for all publishers.
/// ///
/// This method should be called after setting the local cache path with /// This method should be called after setting the local cache path with
@ -729,21 +766,27 @@ impl RestBackend {
pub fn initialize(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> { pub fn initialize(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> {
// Check if we have a local cache path // Check if we have a local cache path
if self.local_cache_path.is_none() { 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 // Download catalogs for all publishers
self.download_all_catalogs(progress)?; self.download_all_catalogs(progress)?;
Ok(()) Ok(())
} }
/// Get the catalog manager for a publisher /// Get the catalog manager for a publisher
fn get_catalog_manager(&mut self, publisher: &str) -> Result<&mut CatalogManager> { fn get_catalog_manager(&mut self, publisher: &str) -> Result<&mut CatalogManager> {
// Check if we have a local cache path // Check if we have a local cache path
let cache_path = match &self.local_cache_path { let cache_path = match &self.local_cache_path {
Some(path) => 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 // The local cache path is expected to already point to the per-publisher directory
@ -753,12 +796,13 @@ impl RestBackend {
// Get or create the catalog manager pointing at the per-publisher directory directly // Get or create the catalog manager pointing at the per-publisher directory directly
if !self.catalog_managers.contains_key(publisher) { if !self.catalog_managers.contains_key(publisher) {
let catalog_manager = CatalogManager::new(cache_path, 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()) Ok(self.catalog_managers.get_mut(publisher).unwrap())
} }
/// Downloads a catalog file from the remote server. /// Downloads a catalog file from the remote server.
/// ///
/// # Arguments /// # Arguments
@ -789,12 +833,18 @@ impl RestBackend {
// Prepare candidate URLs to support both modern and legacy pkg5 depotd layouts // Prepare candidate URLs to support both modern and legacy pkg5 depotd layouts
let mut urls: Vec<String> = vec![ let mut urls: Vec<String> = vec![
format!("{}/catalog/1/{}", self.uri, file_name), 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" { if file_name == "catalog.attrs" {
// Some older depots expose catalog.attrs at the root or under publisher path // Some older depots expose catalog.attrs at the root or under publisher path
urls.insert(1, format!("{}/catalog.attrs", self.uri)); 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!( debug!(
@ -855,13 +905,10 @@ impl RestBackend {
"Failed to download '{}' from any known endpoint: {}", "Failed to download '{}' from any known endpoint: {}",
file_name, s file_name, s
), ),
None => format!( None => format!("Failed to download '{}' from any known endpoint", file_name),
"Failed to download '{}' from any known endpoint",
file_name
),
})) }))
} }
/// Download and store a catalog file /// Download and store a catalog file
/// ///
/// # Arguments /// # Arguments
@ -890,7 +937,11 @@ impl RestBackend {
// Check if we have a local cache path // Check if we have a local cache path
let cache_path = match &self.local_cache_path { let cache_path = match &self.local_cache_path {
Some(path) => 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 // Ensure the per-publisher directory (local cache path) exists
@ -913,19 +964,23 @@ impl RestBackend {
// Store the file directly under the per-publisher directory // Store the file directly under the per-publisher directory
let file_path = cache_path.join(file_name); let file_path = cache_path.join(file_name);
let mut file = File::create(&file_path) let mut file = File::create(&file_path).map_err(|e| {
.map_err(|e| { // Report failure
// Report failure progress.finish(&progress_info);
progress.finish(&progress_info); RepositoryError::FileWriteError {
RepositoryError::FileWriteError { path: file_path.clone(), source: e } path: file_path.clone(),
})?; source: e,
}
})?;
file.write_all(&content) file.write_all(&content).map_err(|e| {
.map_err(|e| { // Report failure
// Report failure progress.finish(&progress_info);
progress.finish(&progress_info); RepositoryError::FileWriteError {
RepositoryError::FileWriteError { path: file_path.clone(), source: e } path: file_path.clone(),
})?; source: e,
}
})?;
debug!("Stored catalog file: {}", file_path.display()); debug!("Stored catalog file: {}", file_path.display());
@ -935,7 +990,7 @@ impl RestBackend {
Ok(file_path) Ok(file_path)
} }
/// Downloads all catalog files for a specific publisher. /// Downloads all catalog files for a specific publisher.
/// ///
/// This method downloads the catalog.attrs file first to determine what catalog parts /// This method downloads the catalog.attrs file first to determine what catalog parts
@ -967,73 +1022,77 @@ impl RestBackend {
) -> Result<()> { ) -> Result<()> {
// Use a no-op reporter if none was provided // Use a no-op reporter if none was provided
let progress_reporter = progress.unwrap_or(&NoopProgressReporter); let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
// Create progress info for the overall operation // 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 // Notify that we're starting the download
progress_reporter.start(&overall_progress); progress_reporter.start(&overall_progress);
// First download catalog.attrs to get the list of available parts // 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 // Parse the catalog.attrs file to get the list of parts
let attrs_content = fs::read_to_string(&attrs_path) let attrs_content = fs::read_to_string(&attrs_path).map_err(|e| {
.map_err(|e| { progress_reporter.finish(&overall_progress);
progress_reporter.finish(&overall_progress); RepositoryError::FileReadError {
RepositoryError::FileReadError { path: attrs_path.clone(), source: e } path: attrs_path.clone(),
})?; source: e,
}
let attrs: Value = serde_json::from_str(&attrs_content) })?;
.map_err(|e| {
progress_reporter.finish(&overall_progress); let attrs: Value = serde_json::from_str(&attrs_content).map_err(|e| {
RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e)) progress_reporter.finish(&overall_progress);
})?; RepositoryError::JsonParseError(format!("Failed to parse catalog.attrs: {}", e))
})?;
// Get the list of parts // Get the list of parts
let parts = attrs["parts"].as_object().ok_or_else(|| { let parts = attrs["parts"].as_object().ok_or_else(|| {
progress_reporter.finish(&overall_progress); progress_reporter.finish(&overall_progress);
RepositoryError::JsonParseError("Missing 'parts' field in catalog.attrs".to_string()) RepositoryError::JsonParseError("Missing 'parts' field in catalog.attrs".to_string())
})?; })?;
// Update progress with total number of parts // Update progress with total number of parts
let total_parts = parts.len() as u64 + 1; // +1 for catalog.attrs let total_parts = parts.len() as u64 + 1; // +1 for catalog.attrs
overall_progress = overall_progress.with_total(total_parts).with_current(1); overall_progress = overall_progress.with_total(total_parts).with_current(1);
progress_reporter.update(&overall_progress); progress_reporter.update(&overall_progress);
// Download each part // Download each part
for (i, part_name) in parts.keys().enumerate() { for (i, part_name) in parts.keys().enumerate() {
debug!("Downloading catalog part: {}", part_name); debug!("Downloading catalog part: {}", part_name);
// Update progress with current part // 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)); .with_context(format!("Downloading part: {}", part_name));
progress_reporter.update(&overall_progress); progress_reporter.update(&overall_progress);
self.download_and_store_catalog_file(publisher, part_name, progress)?; self.download_and_store_catalog_file(publisher, part_name, progress)?;
} }
// Get the catalog manager for this publisher // Get the catalog manager for this publisher
let catalog_manager = self.get_catalog_manager(publisher)?; let catalog_manager = self.get_catalog_manager(publisher)?;
// Update progress for loading parts // Update progress for loading parts
overall_progress = overall_progress.with_context("Loading catalog parts".to_string()); overall_progress = overall_progress.with_context("Loading catalog parts".to_string());
progress_reporter.update(&overall_progress); progress_reporter.update(&overall_progress);
// Load the catalog parts // Load the catalog parts
for part_name in parts.keys() { for part_name in parts.keys() {
catalog_manager.load_part(part_name)?; catalog_manager.load_part(part_name)?;
} }
// Report completion // Report completion
overall_progress = overall_progress.with_current(total_parts); overall_progress = overall_progress.with_current(total_parts);
progress_reporter.finish(&overall_progress); progress_reporter.finish(&overall_progress);
info!("Downloaded catalog for publisher: {}", publisher); info!("Downloaded catalog for publisher: {}", publisher);
Ok(()) Ok(())
} }
/// Download catalogs for all publishers /// Download catalogs for all publishers
/// ///
/// # Arguments /// # Arguments
@ -1046,19 +1105,19 @@ impl RestBackend {
pub fn download_all_catalogs(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> { pub fn download_all_catalogs(&mut self, progress: Option<&dyn ProgressReporter>) -> Result<()> {
// Use a no-op reporter if none was provided // Use a no-op reporter if none was provided
let progress_reporter = progress.unwrap_or(&NoopProgressReporter); let progress_reporter = progress.unwrap_or(&NoopProgressReporter);
// Clone the publishers list to avoid borrowing issues // Clone the publishers list to avoid borrowing issues
let publishers = self.config.publishers.clone(); let publishers = self.config.publishers.clone();
let total_publishers = publishers.len() as u64; let total_publishers = publishers.len() as u64;
// Create progress info for the overall operation // Create progress info for the overall operation
let mut overall_progress = ProgressInfo::new("Downloading all catalogs") let mut overall_progress = ProgressInfo::new("Downloading all catalogs")
.with_total(total_publishers) .with_total(total_publishers)
.with_current(0); .with_current(0);
// Notify that we're starting the download // Notify that we're starting the download
progress_reporter.start(&overall_progress); progress_reporter.start(&overall_progress);
// Download catalogs for each publisher // Download catalogs for each publisher
for (i, publisher) in publishers.iter().enumerate() { for (i, publisher) in publishers.iter().enumerate() {
// Update progress with current publisher // Update progress with current publisher
@ -1066,21 +1125,21 @@ impl RestBackend {
.with_current(i as u64) .with_current(i as u64)
.with_context(format!("Publisher: {}", publisher)); .with_context(format!("Publisher: {}", publisher));
progress_reporter.update(&overall_progress); progress_reporter.update(&overall_progress);
// Download catalog for this publisher // Download catalog for this publisher
self.download_catalog(publisher, progress)?; self.download_catalog(publisher, progress)?;
// Update progress after completing this publisher // Update progress after completing this publisher
overall_progress = overall_progress.with_current(i as u64 + 1); overall_progress = overall_progress.with_current(i as u64 + 1);
progress_reporter.update(&overall_progress); progress_reporter.update(&overall_progress);
} }
// Report completion // Report completion
progress_reporter.finish(&overall_progress); progress_reporter.finish(&overall_progress);
Ok(()) Ok(())
} }
/// Refresh the catalog for a publisher /// Refresh the catalog for a publisher
/// ///
/// # Arguments /// # Arguments
@ -1091,7 +1150,11 @@ impl RestBackend {
/// # Returns /// # Returns
/// ///
/// * `Result<()>` - Ok if the catalog was refreshed successfully, Err otherwise /// * `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) self.download_catalog(publisher, progress)
} }
} }

View file

@ -8,9 +8,9 @@ mod tests {
use crate::actions::Manifest; use crate::actions::Manifest;
use crate::fmri::Fmri; use crate::fmri::Fmri;
use crate::repository::{ use crate::repository::{
CatalogManager, FileBackend, ProgressInfo, ProgressReporter, CatalogManager, FileBackend, ProgressInfo, ProgressReporter, REPOSITORY_CONFIG_FILENAME,
ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result, WritableRepository, ReadableRepository, RepositoryError, RepositoryVersion, RestBackend, Result,
REPOSITORY_CONFIG_FILENAME, WritableRepository,
}; };
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@ -208,15 +208,21 @@ mod tests {
assert!(repo.config.publishers.contains(&"example.com".to_string())); assert!(repo.config.publishers.contains(&"example.com".to_string()));
assert!(FileBackend::construct_catalog_path(&repo_path, "example.com").exists()); assert!(FileBackend::construct_catalog_path(&repo_path, "example.com").exists());
assert!(FileBackend::construct_package_dir(&repo_path, "example.com", "").exists()); assert!(FileBackend::construct_package_dir(&repo_path, "example.com", "").exists());
// Check that the pub.p5i file was created for backward compatibility // 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"); let pub_p5i_path = repo_path
assert!(pub_p5i_path.exists(), "pub.p5i file should be created for backward compatibility"); .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 // Verify the content of the pub.p5i file
let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap(); let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap();
let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap(); let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap();
// Check the structure of the pub.p5i file // Check the structure of the pub.p5i file
assert_eq!(pub_p5i_json["version"], 1); assert_eq!(pub_p5i_json["version"], 1);
assert!(pub_p5i_json["packages"].is_array()); assert!(pub_p5i_json["packages"].is_array());
@ -246,7 +252,9 @@ mod tests {
// Add a package to the part using the stored publisher // Add a package to the part using the stored publisher
let fmri = Fmri::parse("pkg://test/example@1.0.0").unwrap(); 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 // Save the part
catalog_manager.save_part("test_part").unwrap(); catalog_manager.save_part("test_part").unwrap();
@ -286,7 +294,13 @@ mod tests {
publish_package(&mut repo, &manifest_path, &prototype_dir, "test").unwrap(); publish_package(&mut repo, &manifest_path, &prototype_dir, "test").unwrap();
// Check that the files were published in the publisher-specific directory // 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 // Get repository information
let repo_info = repo.get_info().unwrap(); let repo_info = repo.get_info().unwrap();
@ -364,9 +378,11 @@ mod tests {
// Check for specific files // Check for specific files
assert!(files.iter().any(|f| f.contains("usr/bin/hello"))); assert!(files.iter().any(|f| f.contains("usr/bin/hello")));
assert!(files assert!(
.iter() files
.any(|f| f.contains("usr/share/doc/example/README.txt"))); .iter()
.any(|f| f.contains("usr/share/doc/example/README.txt"))
);
assert!(files.iter().any(|f| f.contains("etc/config/example.conf"))); assert!(files.iter().any(|f| f.contains("etc/config/example.conf")));
// Clean up // Clean up
@ -428,7 +444,8 @@ mod tests {
let hash = repo.store_file(&test_file_path, "test").unwrap(); let hash = repo.store_file(&test_file_path, "test").unwrap();
// Check if the file was stored in the correct directory structure // 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 // Verify that the file exists at the expected path
assert!( assert!(
@ -448,7 +465,7 @@ mod tests {
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_transaction_pub_p5i_creation() { fn test_transaction_pub_p5i_creation() {
// Run the setup script to prepare the test environment // Run the setup script to prepare the test environment
@ -463,39 +480,42 @@ mod tests {
// Create a new publisher through a transaction // Create a new publisher through a transaction
let publisher = "transaction_test"; let publisher = "transaction_test";
// Start a transaction // Start a transaction
let mut transaction = repo.begin_transaction().unwrap(); let mut transaction = repo.begin_transaction().unwrap();
// Set the publisher for the transaction // Set the publisher for the transaction
transaction.set_publisher(publisher); transaction.set_publisher(publisher);
// Add a simple manifest to the transaction // Add a simple manifest to the transaction
let manifest_path = manifest_dir.join("example.p5m"); let manifest_path = manifest_dir.join("example.p5m");
let manifest = Manifest::parse_file(&manifest_path).unwrap(); let manifest = Manifest::parse_file(&manifest_path).unwrap();
transaction.update_manifest(manifest); transaction.update_manifest(manifest);
// Commit the transaction // Commit the transaction
transaction.commit().unwrap(); transaction.commit().unwrap();
// Check that the pub.p5i file was created for the new publisher // 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"); 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 // Verify the content of the pub.p5i file
let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap(); let pub_p5i_content = fs::read_to_string(&pub_p5i_path).unwrap();
let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap(); let pub_p5i_json: serde_json::Value = serde_json::from_str(&pub_p5i_content).unwrap();
// Check the structure of the pub.p5i file // Check the structure of the pub.p5i file
assert_eq!(pub_p5i_json["version"], 1); assert_eq!(pub_p5i_json["version"], 1);
assert!(pub_p5i_json["packages"].is_array()); assert!(pub_p5i_json["packages"].is_array());
assert!(pub_p5i_json["publishers"].is_array()); assert!(pub_p5i_json["publishers"].is_array());
assert_eq!(pub_p5i_json["publishers"][0]["name"], publisher); assert_eq!(pub_p5i_json["publishers"][0]["name"], publisher);
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_legacy_pkg5_repository_creation() { fn test_legacy_pkg5_repository_creation() {
// Create a test directory // Create a test directory
@ -508,20 +528,23 @@ mod tests {
// Add a publisher // Add a publisher
let publisher = "openindiana.org"; let publisher = "openindiana.org";
repo.add_publisher(publisher).unwrap(); repo.add_publisher(publisher).unwrap();
// Set as default publisher // Set as default publisher
repo.set_default_publisher(publisher).unwrap(); repo.set_default_publisher(publisher).unwrap();
// Check that the pkg5.repository file was created // Check that the pkg5.repository file was created
let pkg5_repo_path = repo_path.join("pkg5.repository"); 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 // Verify the content of the pkg5.repository file
let pkg5_content = fs::read_to_string(&pkg5_repo_path).unwrap(); let pkg5_content = fs::read_to_string(&pkg5_repo_path).unwrap();
// Print the content for debugging // Print the content for debugging
println!("pkg5.repository content:\n{}", pkg5_content); println!("pkg5.repository content:\n{}", pkg5_content);
// Check that the file contains the expected sections and values // Check that the file contains the expected sections and values
assert!(pkg5_content.contains("[publisher]")); assert!(pkg5_content.contains("[publisher]"));
assert!(pkg5_content.contains("prefix=openindiana.org")); assert!(pkg5_content.contains("prefix=openindiana.org"));
@ -531,55 +554,58 @@ mod tests {
assert!(pkg5_content.contains("signature-required-names=[]")); assert!(pkg5_content.contains("signature-required-names=[]"));
assert!(pkg5_content.contains("check-certificate-revocation=False")); assert!(pkg5_content.contains("check-certificate-revocation=False"));
assert!(pkg5_content.contains("[CONFIGURATION]")); assert!(pkg5_content.contains("[CONFIGURATION]"));
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_rest_repository_local_functionality() { fn test_rest_repository_local_functionality() {
use crate::repository::RestBackend; use crate::repository::RestBackend;
// Create a test directory // Create a test directory
let test_dir = create_test_dir("rest_repository"); let test_dir = create_test_dir("rest_repository");
let cache_path = test_dir.join("cache"); let cache_path = test_dir.join("cache");
println!("Test directory: {}", test_dir.display()); println!("Test directory: {}", test_dir.display());
println!("Cache path: {}", cache_path.display()); println!("Cache path: {}", cache_path.display());
// Create a REST repository // Create a REST repository
let uri = "http://pkg.opensolaris.org/release"; let uri = "http://pkg.opensolaris.org/release";
let mut repo = RestBackend::open(uri).unwrap(); let mut repo = RestBackend::open(uri).unwrap();
// Set the local cache path // Set the local cache path
repo.set_local_cache_path(&cache_path).unwrap(); repo.set_local_cache_path(&cache_path).unwrap();
println!("Local cache path set to: {:?}", repo.local_cache_path); println!("Local cache path set to: {:?}", repo.local_cache_path);
// Add a publisher // Add a publisher
let publisher = "openindiana.org"; let publisher = "openindiana.org";
repo.add_publisher(publisher).unwrap(); repo.add_publisher(publisher).unwrap();
println!("Publisher added: {}", publisher); println!("Publisher added: {}", publisher);
println!("Publishers in config: {:?}", repo.config.publishers); println!("Publishers in config: {:?}", repo.config.publishers);
// Verify that the directory structure was created correctly // Verify that the directory structure was created correctly
let publisher_dir = cache_path.join("publisher").join(publisher); let publisher_dir = cache_path.join("publisher").join(publisher);
println!("Publisher directory: {}", publisher_dir.display()); println!("Publisher directory: {}", publisher_dir.display());
println!("Publisher directory exists: {}", publisher_dir.exists()); 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"); let catalog_dir = publisher_dir.join("catalog");
println!("Catalog directory: {}", catalog_dir.display()); println!("Catalog directory: {}", catalog_dir.display());
println!("Catalog directory exists: {}", catalog_dir.exists()); println!("Catalog directory exists: {}", catalog_dir.exists());
assert!(catalog_dir.exists(), "Catalog directory should be created"); assert!(catalog_dir.exists(), "Catalog directory should be created");
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
/// A test progress reporter that records all progress events /// A test progress reporter that records all progress events
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct TestProgressReporter { struct TestProgressReporter {
@ -590,7 +616,7 @@ mod tests {
/// Records of all finish events /// Records of all finish events
finish_events: Arc<Mutex<Vec<ProgressInfo>>>, finish_events: Arc<Mutex<Vec<ProgressInfo>>>,
} }
impl TestProgressReporter { impl TestProgressReporter {
/// Create a new test progress reporter /// Create a new test progress reporter
fn new() -> Self { fn new() -> Self {
@ -600,116 +626,116 @@ mod tests {
finish_events: Arc::new(Mutex::new(Vec::new())), finish_events: Arc::new(Mutex::new(Vec::new())),
} }
} }
/// Get the number of start events recorded /// Get the number of start events recorded
fn start_count(&self) -> usize { fn start_count(&self) -> usize {
self.start_events.lock().unwrap().len() self.start_events.lock().unwrap().len()
} }
/// Get the number of update events recorded /// Get the number of update events recorded
fn update_count(&self) -> usize { fn update_count(&self) -> usize {
self.update_events.lock().unwrap().len() self.update_events.lock().unwrap().len()
} }
/// Get the number of finish events recorded /// Get the number of finish events recorded
fn finish_count(&self) -> usize { fn finish_count(&self) -> usize {
self.finish_events.lock().unwrap().len() self.finish_events.lock().unwrap().len()
} }
/// Get a clone of all start events /// Get a clone of all start events
fn get_start_events(&self) -> Vec<ProgressInfo> { fn get_start_events(&self) -> Vec<ProgressInfo> {
self.start_events.lock().unwrap().clone() self.start_events.lock().unwrap().clone()
} }
/// Get a clone of all update events /// Get a clone of all update events
fn get_update_events(&self) -> Vec<ProgressInfo> { fn get_update_events(&self) -> Vec<ProgressInfo> {
self.update_events.lock().unwrap().clone() self.update_events.lock().unwrap().clone()
} }
/// Get a clone of all finish events /// Get a clone of all finish events
fn get_finish_events(&self) -> Vec<ProgressInfo> { fn get_finish_events(&self) -> Vec<ProgressInfo> {
self.finish_events.lock().unwrap().clone() self.finish_events.lock().unwrap().clone()
} }
} }
impl ProgressReporter for TestProgressReporter { impl ProgressReporter for TestProgressReporter {
fn start(&self, info: &ProgressInfo) { fn start(&self, info: &ProgressInfo) {
let mut events = self.start_events.lock().unwrap(); let mut events = self.start_events.lock().unwrap();
events.push(info.clone()); events.push(info.clone());
} }
fn update(&self, info: &ProgressInfo) { fn update(&self, info: &ProgressInfo) {
let mut events = self.update_events.lock().unwrap(); let mut events = self.update_events.lock().unwrap();
events.push(info.clone()); events.push(info.clone());
} }
fn finish(&self, info: &ProgressInfo) { fn finish(&self, info: &ProgressInfo) {
let mut events = self.finish_events.lock().unwrap(); let mut events = self.finish_events.lock().unwrap();
events.push(info.clone()); events.push(info.clone());
} }
} }
#[test] #[test]
fn test_progress_reporter() { fn test_progress_reporter() {
// Create a test progress reporter // Create a test progress reporter
let reporter = TestProgressReporter::new(); let reporter = TestProgressReporter::new();
// Create some progress info // Create some progress info
let info1 = ProgressInfo::new("Test operation 1"); let info1 = ProgressInfo::new("Test operation 1");
let info2 = ProgressInfo::new("Test operation 2") let info2 = ProgressInfo::new("Test operation 2")
.with_current(50) .with_current(50)
.with_total(100); .with_total(100);
// Report some progress // Report some progress
reporter.start(&info1); reporter.start(&info1);
reporter.update(&info2); reporter.update(&info2);
reporter.finish(&info1); reporter.finish(&info1);
// Check that the events were recorded // Check that the events were recorded
assert_eq!(reporter.start_count(), 1); assert_eq!(reporter.start_count(), 1);
assert_eq!(reporter.update_count(), 1); assert_eq!(reporter.update_count(), 1);
assert_eq!(reporter.finish_count(), 1); assert_eq!(reporter.finish_count(), 1);
// Check the content of the events // Check the content of the events
let start_events = reporter.get_start_events(); let start_events = reporter.get_start_events();
let update_events = reporter.get_update_events(); let update_events = reporter.get_update_events();
let finish_events = reporter.get_finish_events(); let finish_events = reporter.get_finish_events();
assert_eq!(start_events[0].operation, "Test operation 1"); assert_eq!(start_events[0].operation, "Test operation 1");
assert_eq!(update_events[0].operation, "Test operation 2"); assert_eq!(update_events[0].operation, "Test operation 2");
assert_eq!(update_events[0].current, Some(50)); assert_eq!(update_events[0].current, Some(50));
assert_eq!(update_events[0].total, Some(100)); assert_eq!(update_events[0].total, Some(100));
assert_eq!(finish_events[0].operation, "Test operation 1"); assert_eq!(finish_events[0].operation, "Test operation 1");
} }
#[test] #[test]
fn test_rest_backend_with_progress() { fn test_rest_backend_with_progress() {
// This test is a mock test that doesn't actually connect to a remote server // This test is a mock test that doesn't actually connect to a remote server
// It just verifies that the progress reporting mechanism works correctly // It just verifies that the progress reporting mechanism works correctly
// Create a test directory // Create a test directory
let test_dir = create_test_dir("rest_progress"); let test_dir = create_test_dir("rest_progress");
let cache_path = test_dir.join("cache"); let cache_path = test_dir.join("cache");
// Create a REST repository // Create a REST repository
let uri = "http://pkg.opensolaris.org/release"; let uri = "http://pkg.opensolaris.org/release";
let mut repo = RestBackend::create(uri, RepositoryVersion::V4).unwrap(); let mut repo = RestBackend::create(uri, RepositoryVersion::V4).unwrap();
// Set the local cache path // Set the local cache path
repo.set_local_cache_path(&cache_path).unwrap(); repo.set_local_cache_path(&cache_path).unwrap();
// Create a test progress reporter // Create a test progress reporter
let reporter = TestProgressReporter::new(); let reporter = TestProgressReporter::new();
// Add a publisher // Add a publisher
let publisher = "test"; let publisher = "test";
repo.add_publisher(publisher).unwrap(); repo.add_publisher(publisher).unwrap();
// Create a mock catalog.attrs file // Create a mock catalog.attrs file
let publisher_dir = cache_path.join("publisher").join(publisher); let publisher_dir = cache_path.join("publisher").join(publisher);
let catalog_dir = publisher_dir.join("catalog"); let catalog_dir = publisher_dir.join("catalog");
fs::create_dir_all(&catalog_dir).unwrap(); fs::create_dir_all(&catalog_dir).unwrap();
let attrs_content = r#"{ let attrs_content = r#"{
"created": "20250803T124900Z", "created": "20250803T124900Z",
"last-modified": "20250803T124900Z", "last-modified": "20250803T124900Z",
@ -728,35 +754,39 @@ mod tests {
}, },
"version": 1 "version": 1
}"#; }"#;
let attrs_path = catalog_dir.join("catalog.attrs"); let attrs_path = catalog_dir.join("catalog.attrs");
fs::write(&attrs_path, attrs_content).unwrap(); fs::write(&attrs_path, attrs_content).unwrap();
// Create mock catalog part files // 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); let part_path = catalog_dir.join(part_name);
fs::write(&part_path, "{}").unwrap(); fs::write(&part_path, "{}").unwrap();
} }
// Mock the download_catalog_file method to avoid actual HTTP requests // Mock the download_catalog_file method to avoid actual HTTP requests
// This is done by creating the files before calling download_catalog // This is done by creating the files before calling download_catalog
// Create a simple progress update to ensure update events are recorded // Create a simple progress update to ensure update events are recorded
let progress_info = ProgressInfo::new("Test update") let progress_info = ProgressInfo::new("Test update")
.with_current(1) .with_current(1)
.with_total(2); .with_total(2);
reporter.update(&progress_info); reporter.update(&progress_info);
// Call download_catalog with the progress reporter // Call download_catalog with the progress reporter
// This will fail because we're not actually connecting to a server, // This will fail because we're not actually connecting to a server,
// but we can still verify that the progress reporter was called // but we can still verify that the progress reporter was called
let _ = repo.download_catalog(publisher, Some(&reporter)); let _ = repo.download_catalog(publisher, Some(&reporter));
// Check that the progress reporter was called // Check that the progress reporter was called
assert!(reporter.start_count() > 0, "No start events recorded"); assert!(reporter.start_count() > 0, "No start events recorded");
assert!(reporter.update_count() > 0, "No update events recorded"); assert!(reporter.update_count() > 0, "No update events recorded");
assert!(reporter.finish_count() > 0, "No finish events recorded"); assert!(reporter.finish_count() > 0, "No finish events recorded");
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }

View file

@ -33,38 +33,52 @@ pub struct AdviceReport {
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct AdviceOptions { pub struct AdviceOptions {
pub max_depth: usize, // 0 = unlimited pub max_depth: usize, // 0 = unlimited
pub dependency_cap: usize, // 0 = unlimited per node pub dependency_cap: usize, // 0 = unlimited per node
} }
#[derive(Default)] #[derive(Default)]
struct Ctx { struct Ctx {
// caches // caches
catalog_cache: HashMap<String, Vec<(String, Fmri)>>, // stem -> [(publisher, fmri)] catalog_cache: HashMap<String, Vec<(String, Fmri)>>, // stem -> [(publisher, fmri)]
manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest manifest_cache: HashMap<String, Manifest>, // fmri string -> manifest
lock_cache: HashMap<String, Option<String>>, // stem -> incorporated release 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) candidate_cache:
HashMap<(String, Option<String>, Option<String>, Option<String>), Option<Fmri>>, // (stem, rel, branch, publisher)
publisher_filter: Option<String>, publisher_filter: Option<String>,
cap: usize, cap: usize,
} }
impl Ctx { impl Ctx {
fn new(publisher_filter: Option<String>, cap: usize) -> Self { 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 mut report = AdviceReport::default();
let Some(problem) = err.problem() else { let Some(problem) = err.problem() else {
return Ok(report); return Ok(report);
}; };
match &problem.kind { match &problem.kind {
SolverProblemKind::NoCandidates { stem, release, branch } => { SolverProblemKind::NoCandidates {
stem,
release,
branch,
} => {
// Advise directly on the missing root // Advise directly on the missing root
let mut ctx = Ctx::new(None, opts.dependency_cap); 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 { report.issues.push(AdviceIssue {
path: vec![stem.clone()], path: vec![stem.clone()],
stem: 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. // Fall back to analyzing roots and traversing dependencies to find a missing candidate leaf.
let mut ctx = Ctx::new(None, opts.dependency_cap); let mut ctx = Ctx::new(None, opts.dependency_cap);
for root in &problem.roots { 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, Ok(Some(f)) => f,
_ => { _ => {
// Missing root candidate // 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 { report.issues.push(AdviceIssue {
path: vec![root.stem.clone()], path: vec![root.stem.clone()],
stem: 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 // Depth-first traversal looking for missing candidates
let mut path = vec![root.stem.clone()]; let mut path = vec![root.stem.clone()];
let mut seen = std::collections::HashSet::new(); 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) Ok(report)
} }
@ -114,30 +149,43 @@ fn advise_recursive(
seen: &mut std::collections::HashSet<String>, seen: &mut std::collections::HashSet<String>,
report: &mut AdviceReport, report: &mut AdviceReport,
) -> Result<(), AdviceError> { ) -> 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 manifest = get_manifest_cached(image, ctx, fmri)?;
let mut processed = 0usize; let mut processed = 0usize;
for dep in manifest.dependencies.iter().filter(|d| d.dependency_type == "require" || d.dependency_type == "incorporate") { for dep in manifest
let Some(df) = &dep.fmri else { continue; }; .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(); let dep_stem = df.stem().to_string();
// Extract constraints from optional properties and, if absent, from the dependency FMRI version 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 (mut rel, mut br) = extract_constraint(&dep.optional);
let df_ver_str = df.version(); let df_ver_str = df.version();
if !df_ver_str.is_empty() { if !df_ver_str.is_empty() {
if rel.is_none() { rel = version_release(&df_ver_str); } if rel.is_none() {
if br.is_none() { br = version_branch(&df_ver_str); } 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 // Mirror solver behavior: lock child to parent's branch when not explicitly constrained
if br.is_none() { if br.is_none() {
let parent_branch = fmri let parent_branch = fmri.version.as_ref().and_then(|v| v.branch.clone());
.version if let Some(pb) = parent_branch {
.as_ref() br = Some(pb);
.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; processed += 1;
match find_best_candidate(image, ctx, &dep_stem, rel.as_deref(), br.as_deref())? { match find_best_candidate(image, ctx, &dep_stem, rel.as_deref(), br.as_deref())? {
@ -150,7 +198,8 @@ fn advise_recursive(
} }
} }
None => { 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 { report.issues.push(AdviceIssue {
path: path.clone(), path: path.clone(),
stem: dep_stem.clone(), stem: dep_stem.clone(),
@ -177,32 +226,76 @@ fn extract_constraint(optional: &[Property]) -> (Option<String>, Option<String>)
(release, branch) (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(); let mut available: Vec<String> = Vec::new();
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) { if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
for (pubname, fmri) in list { for (pubname, fmri) in list {
if let Some(ref pfilter) = ctx.publisher_filter { if &pubname != pfilter { continue; } } if let Some(ref pfilter) = ctx.publisher_filter {
if fmri.stem() != stem { continue; } if &pubname != pfilter {
continue;
}
}
if fmri.stem() != stem {
continue;
}
let ver = fmri.version(); let ver = fmri.version();
if ver.is_empty() { continue; } if ver.is_empty() {
continue;
}
available.push(ver); available.push(ver);
} }
} }
available.sort(); available.sort();
available.dedup(); available.dedup();
let available_str = if available.is_empty() { "<none>".to_string() } else { available.join(", ") }; let available_str = if available.is_empty() {
let lock = get_incorporated_release_cached(image, ctx, stem).ok().flatten(); "<none>".to_string()
} else {
available.join(", ")
};
let lock = get_incorporated_release_cached(image, ctx, stem)
.ok()
.flatten();
match (release, branch, lock.as_deref()) { 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), Some(lr)) => format!(
(Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str), "Required release={}, branch={} not found. Image incorporation lock release={} may constrain candidates. Available versions: {}",
(Some(r), None, Some(lr)) => format!("Required release={} not found. Image incorporation lock release={} present. Available versions: {}", r, lr, available_str), r, b, 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), (Some(r), Some(b), None) => format!(
(None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str), "Required release={}, branch={} not found. Available versions: {}",
(None, None, Some(lr)) => format!("No candidates matched. Image incorporation lock release={} present. Available versions: {}", lr, available_str), r, b, available_str
(None, None, None) => format!("No candidates matched. Available versions: {}", 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()), req_branch.map(|s| s.to_string()),
ctx.publisher_filter.clone(), 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(); let mut candidates: Vec<(String, Fmri)> = Vec::new();
for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? { for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
if let Some(ref pfilter) = ctx.publisher_filter { if &pubf != pfilter { continue; } } if let Some(ref pfilter) = ctx.publisher_filter {
if pfmri.stem() != stem { continue; } if &pubf != pfilter {
continue;
}
}
if pfmri.stem() != stem {
continue;
}
let ver = pfmri.version(); let ver = pfmri.version();
if ver.is_empty() { continue; } if ver.is_empty() {
continue;
}
let rel = version_release(&ver); let rel = version_release(&ver);
let br = version_branch(&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_r) = req_release {
if let Some(req_b) = req_branch { if Some(req_b) != br.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 let Some(req_b) = req_branch {
if Some(req_b) != br.as_deref() {
continue;
}
}
candidates.push((ver.clone(), pfmri.clone())); candidates.push((ver.clone(), pfmri.clone()));
} }
@ -247,7 +368,9 @@ fn version_release(version: &str) -> Option<String> {
} }
fn version_branch(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 None
} }
@ -256,8 +379,13 @@ fn query_catalog_cached(
ctx: &Ctx, ctx: &Ctx,
stem: &str, stem: &str,
) -> Result<Vec<(String, Fmri)>, AdviceError> { ) -> 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) {
let mut tmp = Ctx { catalog_cache: ctx.catalog_cache.clone(), ..Default::default() }; return Ok(v.clone());
}
let mut tmp = Ctx {
catalog_cache: ctx.catalog_cache.clone(),
..Default::default()
};
query_catalog_cached_mut(image, &mut tmp, stem) query_catalog_cached_mut(image, &mut tmp, stem)
} }
@ -266,26 +394,48 @@ fn query_catalog_cached_mut(
ctx: &mut Ctx, ctx: &mut Ctx,
stem: &str, stem: &str,
) -> Result<Vec<(String, Fmri)>, AdviceError> { ) -> 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 mut out = Vec::new();
let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError{ message: format!("Failed to query catalog for {}: {}", stem, e) })?; let res = image.query_catalog(Some(stem)).map_err(|e| AdviceError {
for p in res { out.push((p.publisher, p.fmri)); } 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()); ctx.catalog_cache.insert(stem.to_string(), out.clone());
Ok(out) Ok(out)
} }
fn get_manifest_cached(image: &Image, ctx: &mut Ctx, fmri: &Fmri) -> Result<Manifest, AdviceError> { fn get_manifest_cached(image: &Image, ctx: &mut Ctx, fmri: &Fmri) -> Result<Manifest, AdviceError> {
let key = fmri.to_string(); let key = fmri.to_string();
if let Some(m) = ctx.manifest_cache.get(&key) { return Ok(m.clone()); } if let Some(m) = ctx.manifest_cache.get(&key) {
let manifest_opt = image.get_manifest_from_catalog(fmri).map_err(|e| AdviceError { message: format!("Failed to load manifest for {}: {}", fmri.to_string(), e) })?; 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); let manifest = manifest_opt.unwrap_or_else(Manifest::new);
ctx.manifest_cache.insert(key, manifest.clone()); ctx.manifest_cache.insert(key, manifest.clone());
Ok(manifest) Ok(manifest)
} }
fn get_incorporated_release_cached(image: &Image, ctx: &mut Ctx, stem: &str) -> Result<Option<String>, AdviceError> { fn get_incorporated_release_cached(
if let Some(v) = ctx.lock_cache.get(stem) { return Ok(v.clone()); } image: &Image,
let v = image.get_incorporated_release(stem).map_err(|e| AdviceError{ message: format!("Failed to read incorporation lock for {}: {}", stem, e) })?; 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()); ctx.lock_cache.insert(stem.to_string(), v.clone());
Ok(v) Ok(v)
} }

File diff suppressed because it is too large Load diff

View file

@ -23,7 +23,7 @@ mod tests {
// Instead of using JSON, let's create a string format manifest // Instead of using JSON, let's create a string format manifest
// that the parser can handle // that the parser can handle
let manifest_string = "set name=pkg.fmri value=pkg://test/example@1.0.0\n"; let manifest_string = "set name=pkg.fmri value=pkg://test/example@1.0.0\n";
// Write the string to a file // Write the string to a file
let mut file = File::create(&manifest_path).unwrap(); let mut file = File::create(&manifest_path).unwrap();
file.write_all(manifest_string.as_bytes()).unwrap(); file.write_all(manifest_string.as_bytes()).unwrap();
@ -68,10 +68,10 @@ mod tests {
#[test] #[test]
fn test_parse_new_json_format() { fn test_parse_new_json_format() {
use std::io::Read; use std::io::Read;
// Create a temporary directory for the test // Create a temporary directory for the test
let temp_dir = tempdir().unwrap(); 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 // Create a JSON manifest in the new format
let json_manifest = r#"{ let json_manifest = r#"{
@ -120,7 +120,7 @@ mod tests {
Ok(manifest) => { Ok(manifest) => {
println!("Manifest parsing succeeded"); println!("Manifest parsing succeeded");
manifest manifest
}, }
Err(e) => { Err(e) => {
println!("Manifest parsing failed: {:?}", e); println!("Manifest parsing failed: {:?}", e);
panic!("Failed to parse manifest: {:?}", e); panic!("Failed to parse manifest: {:?}", e);
@ -129,22 +129,25 @@ mod tests {
// Verify that the parsed manifest has the expected attributes // Verify that the parsed manifest has the expected attributes
assert_eq!(parsed_manifest.attributes.len(), 3); assert_eq!(parsed_manifest.attributes.len(), 3);
// Check first attribute // Check first attribute
assert_eq!(parsed_manifest.attributes[0].key, "pkg.fmri"); assert_eq!(parsed_manifest.attributes[0].key, "pkg.fmri");
assert_eq!( assert_eq!(
parsed_manifest.attributes[0].values[0], parsed_manifest.attributes[0].values[0],
"pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z" "pkg://openindiana.org/library/perl-5/postgres-dbi-5100@2.19.3,5.11-2014.0.1.1:20250628T100651Z"
); );
// Check second attribute // Check second attribute
assert_eq!(parsed_manifest.attributes[1].key, "pkg.obsolete"); assert_eq!(parsed_manifest.attributes[1].key, "pkg.obsolete");
assert_eq!(parsed_manifest.attributes[1].values[0], "true"); assert_eq!(parsed_manifest.attributes[1].values[0], "true");
// Check third attribute // 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"); assert_eq!(parsed_manifest.attributes[2].values[0], "userland");
// Verify that properties is empty but exists // Verify that properties is empty but exists
for attr in &parsed_manifest.attributes { for attr in &parsed_manifest.attributes {
assert!(attr.properties.is_empty()); assert!(attr.properties.is_empty());

View file

@ -803,8 +803,8 @@ fn emit_action_into_manifest(manifest: &mut Manifest, action_line: &str) -> Resu
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::actions::{Attr, File};
use super::*; use super::*;
use crate::actions::{Attr, File};
#[test] #[test]
fn add_default_set_attr() { fn add_default_set_attr() {

View file

@ -19,7 +19,9 @@ use libips::image::{Image, ImageType};
fn should_run_network_tests() -> bool { fn should_run_network_tests() -> bool {
// Even when ignored, provide an env switch to document intent // 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] #[test]
@ -38,7 +40,8 @@ fn e2e_download_and_build_catalog_openindiana() {
let img_path = temp.path().join("image"); let img_path = temp.path().join("image");
// Create the 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 // Add OpenIndiana publisher
let publisher = "openindiana.org"; let publisher = "openindiana.org";
@ -52,12 +55,12 @@ fn e2e_download_and_build_catalog_openindiana() {
.download_publisher_catalog(publisher) .download_publisher_catalog(publisher)
.expect("failed to download publisher catalog"); .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 // Query catalog; we expect at least one package
let packages = image let packages = image.query_catalog(None).expect("failed to query catalog");
.query_catalog(None)
.expect("failed to query catalog");
assert!( assert!(
!packages.is_empty(), !packages.is_empty(),

View file

@ -1,7 +1,7 @@
use libips::actions::executors::InstallerError as LibInstallerError;
use libips::fmri::FmriError; use libips::fmri::FmriError;
use libips::image::ImageError; use libips::image::ImageError;
use libips::solver::SolverError; use libips::solver::SolverError;
use libips::actions::executors::InstallerError as LibInstallerError;
use miette::Diagnostic; use miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
@ -12,17 +12,11 @@ pub type Result<T> = std::result::Result<T, Pkg6Error>;
#[derive(Debug, Error, Diagnostic)] #[derive(Debug, Error, Diagnostic)]
pub enum Pkg6Error { pub enum Pkg6Error {
#[error("I/O error: {0}")] #[error("I/O error: {0}")]
#[diagnostic( #[diagnostic(code(pkg6::io_error), help("Check system resources and permissions"))]
code(pkg6::io_error),
help("Check system resources and permissions")
)]
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("JSON error: {0}")] #[error("JSON error: {0}")]
#[diagnostic( #[diagnostic(code(pkg6::json_error), help("Check the JSON format and try again"))]
code(pkg6::json_error),
help("Check the JSON format and try again")
)]
JsonError(#[from] serde_json::Error), JsonError(#[from] serde_json::Error),
#[error("FMRI error: {0}")] #[error("FMRI error: {0}")]
@ -84,4 +78,4 @@ impl From<&str> for Pkg6Error {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Pkg6Error::Other(s.to_string()) Pkg6Error::Other(s.to_string())
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ pub struct Cli {
#[arg(long)] #[arg(long)]
pub no_daemon: bool, pub no_daemon: bool,
#[arg(long, value_name = "FILE")] #[arg(long, value_name = "FILE")]
pub pid_file: Option<PathBuf>, pub pid_file: Option<PathBuf>,

View file

@ -1,6 +1,6 @@
use std::path::PathBuf;
use crate::errors::DepotError; use crate::errors::DepotError;
use std::fs; use std::fs;
use std::path::PathBuf;
#[derive(Debug, knuffel::Decode, Clone)] #[derive(Debug, knuffel::Decode, Clone)]
pub struct Config { pub struct Config {
@ -83,10 +83,11 @@ pub struct Oauth2Config {
impl Config { impl Config {
pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> { pub fn load(path: Option<PathBuf>) -> crate::errors::Result<Self> {
let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl")); let path = path.unwrap_or_else(|| PathBuf::from("pkg6depotd.kdl"));
let content = fs::read_to_string(&path) let content = fs::read_to_string(&path).map_err(|e| {
.map_err(|e| DepotError::Config(format!("Failed to read config file {:?}: {}", path, e)))?; DepotError::Config(format!("Failed to read config file {:?}: {}", path, e))
})?;
knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content) knuffel::parse(path.to_str().unwrap_or("pkg6depotd.kdl"), &content)
.map_err(|e| DepotError::Config(format!("Failed to parse config: {:?}", e))) .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 miette::Diagnostic;
use thiserror::Error; use thiserror::Error;
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
};
#[derive(Error, Debug, Diagnostic)] #[derive(Error, Debug, Diagnostic)]
pub enum DepotError { pub enum DepotError {
@ -22,7 +22,7 @@ pub enum DepotError {
#[error("Server error: {0}")] #[error("Server error: {0}")]
#[diagnostic(code(ips::depot_error::server))] #[diagnostic(code(ips::depot_error::server))]
Server(String), Server(String),
#[error("Repository error: {0}")] #[error("Repository error: {0}")]
#[diagnostic(code(ips::depot_error::repo))] #[diagnostic(code(ips::depot_error::repo))]
Repo(#[from] libips::repository::RepositoryError), Repo(#[from] libips::repository::RepositoryError),
@ -31,11 +31,15 @@ pub enum DepotError {
impl IntoResponse for DepotError { impl IntoResponse for DepotError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let (status, message) = match &self { let (status, message) = match &self {
DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()), DepotError::Repo(libips::repository::RepositoryError::NotFound(_)) => {
DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(_)) => (StatusCode::NOT_FOUND, self.to_string()), (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()), _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
}; };
(status, message).into_response() (status, message).into_response()
} }
} }

View file

@ -1,8 +1,8 @@
use axum::{ use axum::{
Json,
extract::State, extract::State,
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
Json,
}; };
use serde::Serialize; use serde::Serialize;
use std::sync::Arc; use std::sync::Arc;
@ -14,9 +14,7 @@ struct HealthResponse {
status: &'static str, status: &'static str,
} }
pub async fn health( pub async fn health(_state: State<Arc<DepotRepo>>) -> impl IntoResponse {
_state: State<Arc<DepotRepo>>,
) -> impl IntoResponse {
// Basic liveness/readiness for now. Future: include repo checks. // Basic liveness/readiness for now. Future: include repo checks.
(StatusCode::OK, Json(HealthResponse { status: "ok" })) (StatusCode::OK, Json(HealthResponse { status: "ok" }))
} }
@ -33,11 +31,10 @@ struct AuthCheckResponse<'a> {
/// Admin auth-check endpoint. /// Admin auth-check endpoint.
/// For now, this is a minimal placeholder that only checks for the presence of a Bearer token. /// 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. /// TODO: Validate JWT via OIDC JWKs using configured issuer/jwks_uri and required scopes.
pub async fn auth_check( pub async fn auth_check(_state: State<Arc<DepotRepo>>, headers: HeaderMap) -> Response {
_state: State<Arc<DepotRepo>>, let auth = headers
headers: HeaderMap, .get(axum::http::header::AUTHORIZATION)
) -> Response { .and_then(|v| v.to_str().ok());
let auth = headers.get(axum::http::header::AUTHORIZATION).and_then(|v| v.to_str().ok());
let (authenticated, token_present) = match auth { let (authenticated, token_present) = match auth {
Some(h) if h.to_ascii_lowercase().starts_with("bearer ") => (true, true), Some(h) if h.to_ascii_lowercase().starts_with("bearer ") => (true, true),
Some(_) => (false, true), Some(_) => (false, true),
@ -52,6 +49,10 @@ pub async fn auth_check(
decision: if authenticated { "allow" } else { "deny" }, 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() (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::{ use axum::{
extract::{Path, State, Request}, extract::{Path, Request, State},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
}; };
use std::sync::Arc; use std::sync::Arc;
use crate::repo::DepotRepo;
use crate::errors::DepotError;
use tower_http::services::ServeFile;
use tower::ServiceExt; use tower::ServiceExt;
use axum::http::header; use tower_http::services::ServeFile;
pub async fn get_catalog_v1( pub async fn get_catalog_v1(
State(repo): State<Arc<DepotRepo>>, State(repo): State<Arc<DepotRepo>>,
@ -18,16 +18,19 @@ pub async fn get_catalog_v1(
let service = ServeFile::new(path); let service = ServeFile::new(path);
let result = service.oneshot(req).await; let result = service.oneshot(req).await;
match result { match result {
Ok(mut res) => { Ok(mut res) => {
// Ensure correct content-type for JSON catalog artifacts regardless of file extension // Ensure correct content-type for JSON catalog artifacts regardless of file extension
let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog."); let is_catalog_json = filename == "catalog.attrs" || filename.starts_with("catalog.");
if is_catalog_json { 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()) Ok(res.into_response())
}, }
Err(e) => Err(DepotError::Server(e.to_string())), Err(e) => Err(DepotError::Server(e.to_string())),
} }
} }

View file

@ -1,52 +1,71 @@
use axum::{
extract::{Path, State, Request},
response::{IntoResponse, Response},
http::header,
};
use std::sync::Arc;
use tower_http::services::ServeFile;
use tower::ServiceExt;
use crate::repo::DepotRepo;
use crate::errors::DepotError; use 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 httpdate::fmt_http_date;
use std::fs;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use tower::ServiceExt;
use tower_http::services::ServeFile;
pub async fn get_file( pub async fn get_file(
State(repo): State<Arc<DepotRepo>>, State(repo): State<Arc<DepotRepo>>,
Path((publisher, _algo, digest)): Path<(String, String, String)>, Path((publisher, _algo, digest)): Path<(String, String, String)>,
req: Request, req: Request,
) -> Result<Response, DepotError> { ) -> Result<Response, DepotError> {
let path = repo.get_file_path(&publisher, &digest) let path = repo.get_file_path(&publisher, &digest).ok_or_else(|| {
.ok_or_else(|| DepotError::Repo(libips::repository::RepositoryError::NotFound(digest.clone())))?; DepotError::Repo(libips::repository::RepositoryError::NotFound(
digest.clone(),
))
})?;
let service = ServeFile::new(path); let service = ServeFile::new(path);
let result = service.oneshot(req).await; let result = service.oneshot(req).await;
match result { match result {
Ok(mut res) => { Ok(mut res) => {
// Add caching headers // Add caching headers
let max_age = repo.cache_max_age(); 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 // 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 // Last-Modified from fs metadata
if let Some(body_path) = res.extensions().get::<std::path::PathBuf>().cloned() { if let Some(body_path) = res.extensions().get::<std::path::PathBuf>().cloned() {
if let Ok(meta) = fs::metadata(&body_path) { if let Ok(meta) = fs::metadata(&body_path) {
if let Ok(mtime) = meta.modified() { if let Ok(mtime) = meta.modified() {
let lm = fmt_http_date(mtime); 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) // Fallback: use now if extension not present (should rarely happen)
if !res.headers().contains_key(header::LAST_MODIFIED) { 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); 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()) Ok(res.into_response())
}, }
Err(e) => Err(DepotError::Server(e.to_string())), Err(e) => Err(DepotError::Server(e.to_string())),
} }
} }

View file

@ -1,35 +1,38 @@
use crate::errors::DepotError;
use crate::repo::DepotRepo;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Response},
http::header, http::header,
response::{IntoResponse, Response},
}; };
use std::sync::Arc; use chrono::{Datelike, NaiveDateTime, TimeZone, Timelike, Utc};
use crate::repo::DepotRepo;
use crate::errors::DepotError;
use libips::fmri::Fmri;
use std::str::FromStr;
use libips::actions::Manifest; use libips::actions::Manifest;
use chrono::{NaiveDateTime, Utc, TimeZone, Datelike, Timelike};
use libips::actions::Property; use libips::actions::Property;
use libips::fmri::Fmri;
use std::fs; use std::fs;
use std::io::Read as _; use std::io::Read as _;
use std::str::FromStr;
use std::sync::Arc;
pub async fn get_info( pub async fn get_info(
State(repo): State<Arc<DepotRepo>>, State(repo): State<Arc<DepotRepo>>,
Path((publisher, fmri_str)): Path<(String, String)>, Path((publisher, fmri_str)): Path<(String, String)>,
) -> Result<Response, DepotError> { ) -> 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 content = repo.get_manifest_text(&publisher, &fmri)?;
let manifest = match serde_json::from_str::<Manifest>(&content) { let manifest = match serde_json::from_str::<Manifest>(&content) {
Ok(m) => m, 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(); let mut out = String::new();
out.push_str(&format!("Name: {}\n", fmri.name)); out.push_str(&format!("Name: {}\n", fmri.name));
if let Some(summary) = find_attr(&manifest, "pkg.summary") { if let Some(summary) = find_attr(&manifest, "pkg.summary") {
out.push_str(&format!("Summary: {}\n", summary)); out.push_str(&format!("Summary: {}\n", summary));
} }
@ -46,17 +49,27 @@ pub async fn get_info(
if let Some((rel_branch, ts)) = rest.split_once(':') { if let Some((rel_branch, ts)) = rest.split_once(':') {
ts_str = Some(ts.to_string()); ts_str = Some(ts.to_string());
if let Some((rel, br)) = rel_branch.split_once('-') { if let Some((rel, br)) = rel_branch.split_once('-') {
if !rel.is_empty() { build_release = Some(rel.to_string()); } if !rel.is_empty() {
if !br.is_empty() { branch = Some(br.to_string()); } build_release = Some(rel.to_string());
}
if !br.is_empty() {
branch = Some(br.to_string());
}
} else { } else {
// No branch // 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 { } else {
// No timestamp // No timestamp
if let Some((rel, br)) = rest.split_once('-') { if let Some((rel, br)) = rest.split_once('-') {
if !rel.is_empty() { build_release = Some(rel.to_string()); } if !rel.is_empty() {
if !br.is_empty() { branch = Some(br.to_string()); } build_release = Some(rel.to_string());
}
if !br.is_empty() {
branch = Some(br.to_string());
}
} else if !rest.is_empty() { } else if !rest.is_empty() {
build_release = Some(rest.to_string()); build_release = Some(rest.to_string());
} }
@ -64,8 +77,12 @@ pub async fn get_info(
} }
out.push_str(&format!("Version: {}\n", version_core)); 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(rel) = build_release {
if let Some(br) = branch { out.push_str(&format!("Branch: {}\n", br)); } 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)) { if let Some(ts) = ts_str.and_then(|s| format_packaging_date(&s)) {
out.push_str(&format!("Packaging Date: {}\n", ts)); out.push_str(&format!("Packaging Date: {}\n", ts));
} }
@ -83,13 +100,15 @@ pub async fn get_info(
} else { } else {
out.push_str(&format!("FMRI: pkg://{}/{}@{}\n", publisher, name, version)); out.push_str(&format!("FMRI: pkg://{}/{}@{}\n", publisher, name, version));
} }
// License // License
// Print actual license text content from repository instead of hash. // Print actual license text content from repository instead of hash.
out.push_str("\nLicense:\n"); out.push_str("\nLicense:\n");
let mut first = true; let mut first = true;
for license in &manifest.licenses { for license in &manifest.licenses {
if !first { out.push('\n'); } if !first {
out.push('\n');
}
first = false; first = false;
// Optional license name header for readability // Optional license name header for readability
@ -105,20 +124,22 @@ pub async fn get_info(
match resolve_license_text(&repo, &publisher, digest) { match resolve_license_text(&repo, &publisher, digest) {
Some(text) => { Some(text) => {
out.push_str(&text); out.push_str(&text);
if !text.ends_with('\n') { out.push('\n'); } if !text.ends_with('\n') {
out.push('\n');
}
} }
None => { None => {
// Fallback: show the digest if content could not be resolved // 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(( Ok(([(header::CONTENT_TYPE, "text/plain")], out).into_response())
[(header::CONTENT_TYPE, "text/plain")],
out
).into_response())
} }
// Try to read and decode the license text for a given digest from the repository. // 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(); let mut text = String::from_utf8_lossy(&data).to_string();
if truncated { if truncated {
if !text.ends_with('\n') { text.push('\n'); } if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("...[truncated]\n"); text.push_str("...[truncated]\n");
} }
Some(text) 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> { fn find_attr(manifest: &Manifest, key: &str) -> Option<String> {
for attr in &manifest.attributes { for attr in &manifest.attributes {
if attr.key == key { if attr.key == key {
return attr.values.first().cloned(); return attr.values.first().cloned();
} }
} }
None None
@ -187,17 +210,32 @@ fn month_name(month: u32) -> &'static str {
fn format_packaging_date(ts: &str) -> Option<String> { fn format_packaging_date(ts: &str) -> Option<String> {
// Expect formats like YYYYMMDDThhmmssZ or with fractional seconds before Z // 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 ndt = NaiveDateTime::parse_from_str(&clean_ts, "%Y%m%dT%H%M%SZ").ok()?;
let dt_utc = Utc.from_utc_datetime(&ndt); let dt_utc = Utc.from_utc_datetime(&ndt);
let month = month_name(dt_utc.month() as u32); let month = month_name(dt_utc.month() as u32);
let day = dt_utc.day(); let day = dt_utc.day();
let year = dt_utc.year(); let year = dt_utc.year();
let hour24 = dt_utc.hour(); 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 minute = dt_utc.minute();
let second = dt_utc.second(); 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 // 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 file in &manifest.files {
for Property { key, value } in &file.properties { for Property { key, value } in &file.properties {
if key == "pkg.size" { 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" { } 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,32 +1,34 @@
use crate::errors::DepotError;
use crate::repo::DepotRepo;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Response},
http::header, http::header,
response::{IntoResponse, Response},
}; };
use std::sync::Arc;
use crate::repo::DepotRepo;
use crate::errors::DepotError;
use libips::fmri::Fmri; use libips::fmri::Fmri;
use std::str::FromStr;
use sha1::Digest as _; use sha1::Digest as _;
use std::str::FromStr;
use std::sync::Arc;
pub async fn get_manifest( pub async fn get_manifest(
State(repo): State<Arc<DepotRepo>>, State(repo): State<Arc<DepotRepo>>,
Path((publisher, fmri_str)): Path<(String, String)>, Path((publisher, fmri_str)): Path<(String, String)>,
) -> Result<Response, DepotError> { ) -> 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 content = repo.get_manifest_text(&publisher, &fmri)?;
// Compute weak ETag from SHA-1 of manifest content (legacy friendly) // Compute weak ETag from SHA-1 of manifest content (legacy friendly)
let mut hasher = sha1::Sha1::new(); let mut hasher = sha1::Sha1::new();
hasher.update(content.as_bytes()); hasher.update(content.as_bytes());
let etag = format!("\"{}\"", format!("{:x}", hasher.finalize())); let etag = format!("\"{}\"", format!("{:x}", hasher.finalize()));
Ok(( Ok((
[ [
(header::CONTENT_TYPE, "text/plain"), (header::CONTENT_TYPE, "text/plain"),
(header::ETAG, etag.as_str()), (header::ETAG, etag.as_str()),
], ],
content, content,
).into_response()) )
.into_response())
} }

View file

@ -1,6 +1,6 @@
pub mod versions;
pub mod catalog; pub mod catalog;
pub mod manifest;
pub mod file; pub mod file;
pub mod info; pub mod info;
pub mod manifest;
pub mod publisher; pub mod publisher;
pub mod versions;

View file

@ -1,12 +1,12 @@
use crate::errors::DepotError;
use crate::repo::DepotRepo;
use axum::{ use axum::{
extract::{Path, State}, extract::{Path, State},
response::{IntoResponse, Response},
http::header, http::header,
response::{IntoResponse, Response},
}; };
use std::sync::Arc;
use crate::repo::DepotRepo;
use crate::errors::DepotError;
use serde::Serialize; use serde::Serialize;
use std::sync::Arc;
#[derive(Serialize)] #[derive(Serialize)]
struct P5iPublisherInfo { struct P5iPublisherInfo {
@ -42,11 +42,14 @@ async fn get_publisher_impl(
Path(publisher): Path<String>, Path(publisher): Path<String>,
) -> Result<Response, DepotError> { ) -> Result<Response, DepotError> {
let repo_info = repo.get_info()?; 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 { if let Some(p) = pub_info {
let p5i = P5iFile { let p5i = P5iFile {
packages: Vec::new(), packages: Vec::new(),
publishers: vec![P5iPublisherInfo { publishers: vec![P5iPublisherInfo {
alias: None, alias: None,
@ -56,12 +59,12 @@ async fn get_publisher_impl(
}], }],
version: 1, version: 1,
}; };
let json = serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?; let json =
Ok(( serde_json::to_string_pretty(&p5i).map_err(|e| DepotError::Server(e.to_string()))?;
[(header::CONTENT_TYPE, "application/vnd.pkg5.info")], Ok(([(header::CONTENT_TYPE, "application/vnd.pkg5.info")], json).into_response())
json
).into_response())
} else { } else {
Err(DepotError::Repo(libips::repository::RepositoryError::PublisherNotFound(publisher))) Err(DepotError::Repo(
libips::repository::RepositoryError::PublisherNotFound(publisher),
))
} }
} }

View file

@ -56,14 +56,32 @@ pub async fn get_versions() -> impl IntoResponse {
let response = VersionsResponse { let response = VersionsResponse {
server_version, server_version,
operations: vec![ operations: vec![
SupportedOperation { op: Operation::Info, versions: vec![0] }, SupportedOperation {
SupportedOperation { op: Operation::Versions, versions: vec![0] }, op: Operation::Info,
SupportedOperation { op: Operation::Catalog, versions: vec![1] }, versions: vec![0],
SupportedOperation { op: Operation::Manifest, versions: vec![0, 1] }, },
SupportedOperation { op: Operation::File, versions: vec![0, 1] }, SupportedOperation {
SupportedOperation { op: Operation::Publisher, versions: vec![0, 1] }, op: Operation::Versions,
versions: vec![0],
},
SupportedOperation {
op: Operation::Catalog,
versions: vec![1],
},
SupportedOperation {
op: Operation::Manifest,
versions: vec![0, 1],
},
SupportedOperation {
op: Operation::File,
versions: vec![0, 1],
},
SupportedOperation {
op: Operation::Publisher,
versions: vec![0, 1],
},
], ],
}; };
response.to_string() response.to_string()
} }

View file

@ -1,5 +1,5 @@
pub mod server; pub mod admin;
pub mod routes;
pub mod handlers; pub mod handlers;
pub mod middleware; 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::{ use axum::{
routing::{get, post, head},
Router, Router,
routing::{get, post},
}; };
use std::sync::Arc; use std::sync::Arc;
use crate::repo::DepotRepo; use tower_http::trace::TraceLayer;
use crate::http::handlers::{versions, catalog, manifest, file, info, publisher};
use crate::http::admin;
pub fn app_router(state: Arc<DepotRepo>) -> Router { pub fn app_router(state: Arc<DepotRepo>) -> Router {
Router::new() Router::new()
.route("/versions/0/", get(versions::get_versions)) .route("/versions/0/", get(versions::get_versions))
.route("/{publisher}/catalog/1/{filename}", get(catalog::get_catalog_v1).head(catalog::get_catalog_v1)) .route(
.route("/{publisher}/manifest/0/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest)) "/{publisher}/catalog/1/{filename}",
.route("/{publisher}/manifest/1/{fmri}", get(manifest::get_manifest).head(manifest::get_manifest)) get(catalog::get_catalog_v1).head(catalog::get_catalog_v1),
.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}/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}/info/0/{fmri}", get(info::get_info))
.route("/{publisher}/publisher/0", get(publisher::get_publisher_v0)) .route("/{publisher}/publisher/0", get(publisher::get_publisher_v0))
.route("/{publisher}/publisher/1", get(publisher::get_publisher_v1)) .route("/{publisher}/publisher/1", get(publisher::get_publisher_v1))
// Admin API over HTTP // Admin API over HTTP
.route("/admin/health", get(admin::health)) .route("/admin/health", get(admin::health))
.route("/admin/auth/check", post(admin::auth_check)) .route("/admin/auth/check", post(admin::auth_check))
.layer(TraceLayer::new_for_http())
.with_state(state) .with_state(state)
} }

View file

@ -1,10 +1,12 @@
use tokio::net::TcpListener;
use axum::Router;
use crate::errors::Result; use crate::errors::Result;
use axum::Router;
use tokio::net::TcpListener;
pub async fn run(router: Router, listener: TcpListener) -> Result<()> { pub async fn run(router: Router, listener: TcpListener) -> Result<()> {
let addr = listener.local_addr()?; let addr = listener.local_addr()?;
tracing::info!("Listening on {}", 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,25 +1,25 @@
pub mod cli; pub mod cli;
pub mod config; pub mod config;
pub mod daemon;
pub mod errors; pub mod errors;
pub mod http; pub mod http;
pub mod telemetry;
pub mod repo; pub mod repo;
pub mod daemon; pub mod telemetry;
use clap::Parser; use clap::Parser;
use cli::{Cli, Commands}; use cli::{Cli, Commands};
use config::Config; use config::Config;
use miette::Result; use miette::Result;
use std::sync::Arc;
use repo::DepotRepo; use repo::DepotRepo;
use std::sync::Arc;
pub async fn run() -> Result<()> { pub async fn run() -> Result<()> {
let args = Cli::parse(); let args = Cli::parse();
// Load config // Load config
// For M1, let's just create a dummy default if not found/failed for testing purposes // For M1, let's just create a dummy default if not found/failed for testing purposes
// In a real scenario we'd want to be more specific about errors. // In a real scenario we'd want to be more specific about errors.
let config = Config::load(args.config.clone()).unwrap_or_else(|e| { let config = Config::load(args.config.clone()).unwrap_or_else(|e| {
eprintln!("Failed to load config: {}. Using default.", e); eprintln!("Failed to load config: {}. Using default.", e);
Config { Config {
@ -45,7 +45,7 @@ pub async fn run() -> Result<()> {
// Init telemetry // Init telemetry
telemetry::init(&config); telemetry::init(&config);
// Init repo // Init repo
let repo = DepotRepo::new(&config).map_err(|e| miette::miette!(e))?; let repo = DepotRepo::new(&config).map_err(|e| miette::miette!(e))?;
let state = Arc::new(repo); let state = Arc::new(repo);
@ -55,15 +55,26 @@ pub async fn run() -> Result<()> {
if !args.no_daemon { if !args.no_daemon {
daemon::daemonize().map_err(|e| miette::miette!(e))?; daemon::daemonize().map_err(|e| miette::miette!(e))?;
} }
let router = http::routes::app_router(state); 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 bind_str = config
let addr: std::net::SocketAddr = bind_str.parse().map_err(crate::errors::DepotError::AddrParse)?; .server
let listener = tokio::net::TcpListener::bind(addr).await.map_err(crate::errors::DepotError::Io)?; .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); 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 => { Commands::ConfigTest => {
println!("Configuration loaded successfully: {:?}", config); println!("Configuration loaded successfully: {:?}", config);

View file

@ -1,5 +1,5 @@
use pkg6depotd::run;
use miette::Result; use miette::Result;
use pkg6depotd::run;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { 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::config::Config;
use crate::errors::{Result, DepotError}; use crate::errors::{DepotError, Result};
use libips::fmri::Fmri; use libips::fmri::Fmri;
use libips::repository::{FileBackend, ReadableRepository};
use std::path::PathBuf;
use std::sync::Mutex; use std::sync::Mutex;
pub struct DepotRepo { pub struct DepotRepo {
@ -15,11 +15,12 @@ impl DepotRepo {
pub fn new(config: &Config) -> Result<Self> { pub fn new(config: &Config) -> Result<Self> {
let root = config.repository.root.clone(); let root = config.repository.root.clone();
let backend = FileBackend::open(&root).map_err(DepotError::Repo)?; let backend = FileBackend::open(&root).map_err(DepotError::Repo)?;
let cache_max_age = config let cache_max_age = config.server.cache_max_age.unwrap_or(3600);
.server Ok(Self {
.cache_max_age backend: Mutex::new(backend),
.unwrap_or(3600); root,
Ok(Self { backend: Mutex::new(backend), root, cache_max_age }) cache_max_age,
})
} }
pub fn get_catalog_path(&self, publisher: &str) -> PathBuf { 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> { 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); let cand_pub = FileBackend::construct_file_path_with_publisher(&self.root, publisher, hash);
if cand_pub.exists() { return Some(cand_pub); } 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);
None if cand_global.exists() {
return Some(cand_global);
}
None
} }
pub fn get_manifest_text(&self, publisher: &str, fmri: &Fmri) -> Result<String> { 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)))?; let backend = self
backend.fetch_manifest_text(publisher, fmri).map_err(DepotError::Repo) .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> { pub fn get_manifest_path(&self, publisher: &str, fmri: &Fmri) -> Option<PathBuf> {
@ -46,28 +56,54 @@ impl DepotRepo {
if version.is_empty() { if version.is_empty() {
return None; return None;
} }
let path = FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version); let path =
if path.exists() { return Some(path); } FileBackend::construct_manifest_path(&self.root, publisher, fmri.stem(), &version);
if path.exists() {
return Some(path);
}
// Fallbacks similar to lib logic // Fallbacks similar to lib logic
let encoded_stem = url_encode_filename(fmri.stem()); let encoded_stem = url_encode_filename(fmri.stem());
let encoded_version = url_encode_filename(&version); let encoded_version = url_encode_filename(&version);
let alt1 = self.root.join("pkg").join(&encoded_stem).join(&encoded_version); let alt1 = self
if alt1.exists() { return Some(alt1); } .root
let alt2 = self.root.join("publisher").join(publisher).join("pkg").join(&encoded_stem).join(&encoded_version); .join("pkg")
if alt2.exists() { return Some(alt2); } .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 None
} }
pub fn cache_max_age(&self) -> u64 { self.cache_max_age } 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_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> { 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) 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 crate::config::Config;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
pub fn init(_config: &Config) { pub fn init(_config: &Config) {
let env_filter = EnvFilter::try_from_default_env() let env_filter = EnvFilter::try_from_default_env()
@ -10,6 +10,6 @@ pub fn init(_config: &Config) {
.with(tracing_subscriber::fmt::layer()); .with(tracing_subscriber::fmt::layer());
// TODO: Add OTLP layer if configured in _config // TODO: Add OTLP layer if configured in _config
registry.init(); registry.init();
} }

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::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::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
use tempfile::TempDir; use tempfile::TempDir;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use std::fs;
// Helper to setup a repo with a published package // Helper to setup a repo with a published package
fn setup_repo(dir: &TempDir) -> PathBuf { fn setup_repo(dir: &TempDir) -> PathBuf {
@ -15,44 +15,44 @@ fn setup_repo(dir: &TempDir) -> PathBuf {
let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap(); let mut backend = FileBackend::create(&repo_path, RepositoryVersion::V4).unwrap();
let publisher = "test"; let publisher = "test";
backend.add_publisher(publisher).unwrap(); backend.add_publisher(publisher).unwrap();
// Create a transaction to publish a package // Create a transaction to publish a package
let mut tx = backend.begin_transaction().unwrap(); let mut tx = backend.begin_transaction().unwrap();
tx.set_publisher(publisher); tx.set_publisher(publisher);
// Create content // Create content
let content_dir = dir.path().join("content"); let content_dir = dir.path().join("content");
fs::create_dir_all(&content_dir).unwrap(); fs::create_dir_all(&content_dir).unwrap();
let file_path = content_dir.join("hello.txt"); let file_path = content_dir.join("hello.txt");
fs::write(&file_path, "Hello IPS").unwrap(); fs::write(&file_path, "Hello IPS").unwrap();
// Add file // Add file
let mut fa = FileAction::read_from_path(&file_path).unwrap(); let mut fa = FileAction::read_from_path(&file_path).unwrap();
fa.path = "hello.txt".to_string(); // relative path in package fa.path = "hello.txt".to_string(); // relative path in package
tx.add_file(fa, &file_path).unwrap(); tx.add_file(fa, &file_path).unwrap();
// Update manifest // Update manifest
let mut manifest = Manifest::new(); let mut manifest = Manifest::new();
use libips::actions::Attr; use libips::actions::Attr;
use std::collections::HashMap; use std::collections::HashMap;
manifest.attributes.push(Attr { manifest.attributes.push(Attr {
key: "pkg.fmri".to_string(), key: "pkg.fmri".to_string(),
values: vec!["pkg://test/example@1.0.0".to_string()], values: vec!["pkg://test/example@1.0.0".to_string()],
properties: HashMap::new(), properties: HashMap::new(),
}); });
manifest.attributes.push(Attr { manifest.attributes.push(Attr {
key: "pkg.summary".to_string(), key: "pkg.summary".to_string(),
values: vec!["Test Package".to_string()], values: vec!["Test Package".to_string()],
properties: HashMap::new(), properties: HashMap::new(),
}); });
tx.update_manifest(manifest); tx.update_manifest(manifest);
tx.commit().unwrap(); tx.commit().unwrap();
backend.rebuild(Some(publisher), false, false).unwrap(); backend.rebuild(Some(publisher), false, false).unwrap();
repo_path repo_path
} }
@ -61,7 +61,7 @@ async fn test_depot_server() {
// Setup // Setup
let temp_dir = TempDir::new().unwrap(); let temp_dir = TempDir::new().unwrap();
let repo_path = setup_repo(&temp_dir); let repo_path = setup_repo(&temp_dir);
let config = Config { let config = Config {
server: ServerConfig { server: ServerConfig {
bind: vec!["127.0.0.1:0".to_string()], bind: vec!["127.0.0.1:0".to_string()],
@ -81,24 +81,28 @@ async fn test_depot_server() {
admin: None, admin: None,
oauth2: None, oauth2: None,
}; };
let repo = DepotRepo::new(&config).unwrap(); let repo = DepotRepo::new(&config).unwrap();
let state = Arc::new(repo); let state = Arc::new(repo);
let router = http::routes::app_router(state); let router = http::routes::app_router(state);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
// Spawn server // Spawn server
tokio::spawn(async move { tokio::spawn(async move {
http::server::run(router, listener).await.unwrap(); http::server::run(router, listener).await.unwrap();
}); });
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let base_url = format!("http://{}", addr); let base_url = format!("http://{}", addr);
// 1. Test Versions // 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()); assert!(resp.status().is_success());
let text = resp.text().await.unwrap(); let text = resp.text().await.unwrap();
assert!(text.contains("pkg-server pkg6depotd-0.5.1")); assert!(text.contains("pkg-server pkg6depotd-0.5.1"));
@ -106,12 +110,12 @@ async fn test_depot_server() {
assert!(text.contains("manifest 0 1")); assert!(text.contains("manifest 0 1"));
// 2. Test Catalog // 2. Test Catalog
// Test Catalog v1 // Test Catalog v1
let catalog_v1_url = format!("{}/test/catalog/1/catalog.attrs", base_url); let catalog_v1_url = format!("{}/test/catalog/1/catalog.attrs", base_url);
let resp = client.get(&catalog_v1_url).send().await.unwrap(); let resp = client.get(&catalog_v1_url).send().await.unwrap();
if !resp.status().is_success() { if !resp.status().is_success() {
println!("Catalog v1 failed: {:?}", resp); println!("Catalog v1 failed: {:?}", resp);
} }
assert!(resp.status().is_success()); assert!(resp.status().is_success());
let catalog_attrs = resp.text().await.unwrap(); let catalog_attrs = resp.text().await.unwrap();
@ -128,7 +132,7 @@ async fn test_depot_server() {
let manifest_text = resp.text().await.unwrap(); let manifest_text = resp.text().await.unwrap();
assert!(manifest_text.contains("pkg.fmri")); assert!(manifest_text.contains("pkg.fmri"));
assert!(manifest_text.contains("example@1.0.0")); assert!(manifest_text.contains("example@1.0.0"));
// v1 // v1
let manifest_v1_url = format!("{}/test/manifest/1/{}", base_url, fmri_arg); let manifest_v1_url = format!("{}/test/manifest/1/{}", base_url, fmri_arg);
let resp = client.get(&manifest_v1_url).send().await.unwrap(); let resp = client.get(&manifest_v1_url).send().await.unwrap();
@ -144,25 +148,36 @@ async fn test_depot_server() {
assert!(info_text.contains("Name: example")); assert!(info_text.contains("Name: example"));
assert!(info_text.contains("Summary: Test Package")); assert!(info_text.contains("Summary: Test Package"));
// Ensure FMRI format is correct: pkg://<publisher>/<name>@<version> // 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 // 5. Test Publisher v1
let pub_url = format!("{}/test/publisher/1", base_url); let pub_url = format!("{}/test/publisher/1", base_url);
let resp = client.get(&pub_url).send().await.unwrap(); let resp = client.get(&pub_url).send().await.unwrap();
assert!(resp.status().is_success()); 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(); let pub_json: serde_json::Value = resp.json().await.unwrap();
assert_eq!(pub_json["version"], 1); assert_eq!(pub_json["version"], 1);
assert_eq!(pub_json["publishers"][0]["name"], "test"); assert_eq!(pub_json["publishers"][0]["name"], "test");
// 6. Test File // 6. Test File
// We assume file exists if manifest works. // We assume file exists if manifest works.
} }
#[tokio::test] #[tokio::test]
async fn test_ini_only_repo_serving_catalog() { async fn test_ini_only_repo_serving_catalog() {
use libips::repository::{WritableRepository, ReadableRepository};
use libips::repository::BatchOptions; use libips::repository::BatchOptions;
use libips::repository::{ReadableRepository, WritableRepository};
use std::io::Write as _; use std::io::Write as _;
// Setup temp repo // Setup temp repo
@ -190,18 +205,33 @@ async fn test_ini_only_repo_serving_catalog() {
let mut manifest = Manifest::new(); let mut manifest = Manifest::new();
use libips::actions::Attr; use libips::actions::Attr;
use std::collections::HashMap; 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 {
manifest.attributes.push(Attr { key: "pkg.summary".to_string(), values: vec!["INI Repo Test Package".to_string()], properties: HashMap::new() }); 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.update_manifest(manifest);
tx.commit().unwrap(); tx.commit().unwrap();
// Rebuild catalog using batched API explicitly with small batch to exercise code path // Rebuild catalog using batched API explicitly with small batch to exercise code path
let opts = BatchOptions { batch_size: 1, flush_every_n: 1 }; let opts = BatchOptions {
backend.rebuild_catalog_batched(publisher, true, opts).unwrap(); 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 // Replace pkg6.repository with legacy pkg5.repository so FileBackend::open uses INI
let pkg6_cfg = repo_path.join("pkg6.repository"); 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(); let mut ini = String::new();
ini.push_str("[publisher]\n"); ini.push_str("[publisher]\n");
ini.push_str(&format!("prefix = {}\n", publisher)); ini.push_str(&format!("prefix = {}\n", publisher));
@ -211,9 +241,23 @@ async fn test_ini_only_repo_serving_catalog() {
// Start depot server // Start depot server
let config = Config { 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 }, server: ServerConfig {
repository: RepositoryConfig { root: repo_path.clone(), mode: Some("readonly".to_string()) }, bind: vec!["127.0.0.1:0".to_string()],
telemetry: None, publishers: None, admin: None, oauth2: None, 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 repo = DepotRepo::new(&config).unwrap();
let state = Arc::new(repo); 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 listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().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 client = reqwest::Client::new();
let base_url = format!("http://{}", addr); let base_url = format!("http://{}", addr);
@ -235,19 +281,48 @@ async fn test_ini_only_repo_serving_catalog() {
assert!(body.contains("parts")); assert!(body.contains("parts"));
// Also fetch individual catalog 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 url = format!("{}/{}/catalog/1/{}", base_url, publisher, part);
let resp = client.get(&url).send().await.unwrap(); let resp = client.get(&url).send().await.unwrap();
assert!(resp.status().is_success(), "{} status: {:?}", part, resp.status()); assert!(
let ct = resp.headers().get("content-type").unwrap().to_str().unwrap().to_string(); resp.status().is_success(),
assert!(ct.contains("application/json"), "content-type for {} was {}", part, ct); "{} 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(); let txt = resp.text().await.unwrap();
assert!(!txt.is_empty(), "{} should not be empty", part); assert!(!txt.is_empty(), "{} should not be empty", part);
if *part == "catalog.base.C" { 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 { } else {
// dependency/summary may be empty for this test package; at least ensure signature is present // 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 // Check that the publisher was added
assert!(repo_path.join("publisher").join("example.com").exists()); assert!(repo_path.join("publisher").join("example.com").exists());
assert!(repo_path.join("publisher").join("example.com").join("catalog").exists()); assert!(
assert!(repo_path.join("publisher").join("example.com").join("pkg").exists()); repo_path
.join("publisher")
.join("example.com")
.join("catalog")
.exists()
);
assert!(
repo_path
.join("publisher")
.join("example.com")
.join("pkg")
.exists()
);
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
@ -388,7 +400,7 @@ mod e2e_tests {
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
#[test] #[test]
fn test_e2e_obsoleted_packages() { fn test_e2e_obsoleted_packages() {
// Run the setup script to prepare the test environment // Run the setup script to prepare the test environment
@ -438,38 +450,47 @@ mod e2e_tests {
"Failed to list packages: {:?}", "Failed to list packages: {:?}",
result.err() result.err()
); );
let output = result.unwrap(); 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 // 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 // We need to extract these fields and construct the FMRI string
let fmri_obj = &packages["packages"][0]["fmri"]; let fmri_obj = &packages["packages"][0]["fmri"];
let scheme = fmri_obj["scheme"].as_str().expect("Failed to get scheme"); 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 name = fmri_obj["name"].as_str().expect("Failed to get name");
let version_obj = &fmri_obj["version"]; 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" // Construct the FMRI string in the format "pkg://publisher/name@version"
let fmri = format!("{}://{}/{}", scheme, publisher, name); let fmri = format!("{}://{}/{}", scheme, publisher, name);
// Add version if available // Add version if available
let fmri = if !release.is_empty() { let fmri = if !release.is_empty() {
format!("{}@{}", fmri, release) format!("{}@{}", fmri, release)
} else { } else {
fmri fmri
}; };
// Print the FMRI and repo path for debugging // Print the FMRI and repo path for debugging
println!("FMRI: {}", fmri); println!("FMRI: {}", fmri);
println!("Repo path: {}", repo_path.display()); println!("Repo path: {}", repo_path.display());
// Check if the package exists in the repository // 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: {}", pkg_dir.display());
println!("Package directory exists: {}", pkg_dir.exists()); println!("Package directory exists: {}", pkg_dir.exists());
// List files in the package directory if it exists // List files in the package directory if it exists
if pkg_dir.exists() { if pkg_dir.exists() {
println!("Files in package directory:"); println!("Files in package directory:");
@ -478,26 +499,31 @@ mod e2e_tests {
println!(" {}", entry.path().display()); println!(" {}", entry.path().display());
} }
} }
// Mark the package as obsoleted // Mark the package as obsoleted
let result = run_pkg6repo(&[ let result = run_pkg6repo(&[
"obsolete-package", "obsolete-package",
"-s", repo_path.to_str().unwrap(), "-s",
"-p", "test", repo_path.to_str().unwrap(),
"-f", &fmri, "-p",
"-m", "This package is obsoleted for testing purposes", "test",
"-r", "pkg://test/example2@1.0" "-f",
&fmri,
"-m",
"This package is obsoleted for testing purposes",
"-r",
"pkg://test/example2@1.0",
]); ]);
// Print the result for debugging // Print the result for debugging
println!("Result: {:?}", result); println!("Result: {:?}", result);
assert!( assert!(
result.is_ok(), result.is_ok(),
"Failed to mark package as obsoleted: {:?}", "Failed to mark package as obsoleted: {:?}",
result.err() result.err()
); );
// Verify the package is no longer in the main repository // Verify the package is no longer in the main repository
let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap()]); let result = run_pkg6repo(&["list", "-s", repo_path.to_str().unwrap()]);
assert!( assert!(
@ -505,40 +531,49 @@ mod e2e_tests {
"Failed to list packages: {:?}", "Failed to list packages: {:?}",
result.err() result.err()
); );
let output = result.unwrap(); let output = result.unwrap();
assert!( assert!(
!output.contains("example"), !output.contains("example"),
"Package still found in repository after being marked as obsoleted" "Package still found in repository after being marked as obsoleted"
); );
// List obsoleted packages // 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!( assert!(
result.is_ok(), result.is_ok(),
"Failed to list obsoleted packages: {:?}", "Failed to list obsoleted packages: {:?}",
result.err() result.err()
); );
let output = result.unwrap(); let output = result.unwrap();
assert!( assert!(
output.contains("example"), output.contains("example"),
"Obsoleted package not found in obsoleted packages list" "Obsoleted package not found in obsoleted packages list"
); );
// Show details of the obsoleted package // Show details of the obsoleted package
let result = run_pkg6repo(&[ let result = run_pkg6repo(&[
"show-obsoleted", "show-obsoleted",
"-s", repo_path.to_str().unwrap(), "-s",
"-p", "test", repo_path.to_str().unwrap(),
"-f", &fmri "-p",
"test",
"-f",
&fmri,
]); ]);
assert!( assert!(
result.is_ok(), result.is_ok(),
"Failed to show obsoleted package details: {:?}", "Failed to show obsoleted package details: {:?}",
result.err() result.err()
); );
let output = result.unwrap(); let output = result.unwrap();
assert!( assert!(
output.contains("Status: obsolete"), output.contains("Status: obsolete"),
@ -552,8 +587,8 @@ mod e2e_tests {
output.contains("pkg://test/example2@1.0"), output.contains("pkg://test/example2@1.0"),
"Replacement package not found in details" "Replacement package not found in details"
); );
// Clean up // Clean up
cleanup_test_dir(&test_dir); cleanup_test_dir(&test_dir);
} }
} }

View file

@ -334,170 +334,170 @@ enum Commands {
#[clap(short = 'p', long)] #[clap(short = 'p', long)]
publisher: Option<String>, publisher: Option<String>,
}, },
/// Mark a package as obsoleted /// Mark a package as obsoleted
ObsoletePackage { ObsoletePackage {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Publisher of the package /// Publisher of the package
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// FMRI of the package to mark as obsoleted /// FMRI of the package to mark as obsoleted
#[clap(short = 'f')] #[clap(short = 'f')]
fmri: String, fmri: String,
/// Optional deprecation message explaining why the package is obsoleted /// Optional deprecation message explaining why the package is obsoleted
#[clap(short = 'm', long = "message")] #[clap(short = 'm', long = "message")]
message: Option<String>, message: Option<String>,
/// Optional list of packages that replace this obsoleted package /// Optional list of packages that replace this obsoleted package
#[clap(short = 'r', long = "replaced-by")] #[clap(short = 'r', long = "replaced-by")]
replaced_by: Option<Vec<String>>, replaced_by: Option<Vec<String>>,
}, },
/// List obsoleted packages in a repository /// List obsoleted packages in a repository
ListObsoleted { ListObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Output format /// Output format
#[clap(short = 'F')] #[clap(short = 'F')]
format: Option<String>, format: Option<String>,
/// Omit headers /// Omit headers
#[clap(short = 'H')] #[clap(short = 'H')]
omit_headers: bool, omit_headers: bool,
/// Publisher to list obsoleted packages for /// Publisher to list obsoleted packages for
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// Page number (1-based, defaults to 1) /// Page number (1-based, defaults to 1)
#[clap(long = "page")] #[clap(long = "page")]
page: Option<usize>, page: Option<usize>,
/// Number of packages per page (defaults to 100, 0 for all) /// Number of packages per page (defaults to 100, 0 for all)
#[clap(long = "page-size")] #[clap(long = "page-size")]
page_size: Option<usize>, page_size: Option<usize>,
}, },
/// Show details of an obsoleted package /// Show details of an obsoleted package
ShowObsoleted { ShowObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Output format /// Output format
#[clap(short = 'F')] #[clap(short = 'F')]
format: Option<String>, format: Option<String>,
/// Publisher of the package /// Publisher of the package
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// FMRI of the obsoleted package to show /// FMRI of the obsoleted package to show
#[clap(short = 'f')] #[clap(short = 'f')]
fmri: String, fmri: String,
}, },
/// Search for obsoleted packages /// Search for obsoleted packages
SearchObsoleted { SearchObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Output format /// Output format
#[clap(short = 'F')] #[clap(short = 'F')]
format: Option<String>, format: Option<String>,
/// Omit headers /// Omit headers
#[clap(short = 'H')] #[clap(short = 'H')]
omit_headers: bool, omit_headers: bool,
/// Publisher to search obsoleted packages for /// Publisher to search obsoleted packages for
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// Search pattern (supports glob patterns) /// Search pattern (supports glob patterns)
#[clap(short = 'q')] #[clap(short = 'q')]
pattern: String, pattern: String,
/// Maximum number of results to return /// Maximum number of results to return
#[clap(short = 'n', long = "limit")] #[clap(short = 'n', long = "limit")]
limit: Option<usize>, limit: Option<usize>,
}, },
/// Restore an obsoleted package to the main repository /// Restore an obsoleted package to the main repository
RestoreObsoleted { RestoreObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Publisher of the package /// Publisher of the package
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// FMRI of the obsoleted package to restore /// FMRI of the obsoleted package to restore
#[clap(short = 'f')] #[clap(short = 'f')]
fmri: String, fmri: String,
/// Skip rebuilding the catalog after restoration /// Skip rebuilding the catalog after restoration
#[clap(long = "no-rebuild")] #[clap(long = "no-rebuild")]
no_rebuild: bool, no_rebuild: bool,
}, },
/// Export obsoleted packages to a file /// Export obsoleted packages to a file
ExportObsoleted { ExportObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Publisher to export obsoleted packages for /// Publisher to export obsoleted packages for
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// Output file path /// Output file path
#[clap(short = 'o')] #[clap(short = 'o')]
output_file: String, output_file: String,
/// Optional search pattern to filter packages /// Optional search pattern to filter packages
#[clap(short = 'q')] #[clap(short = 'q')]
pattern: Option<String>, pattern: Option<String>,
}, },
/// Import obsoleted packages from a file /// Import obsoleted packages from a file
ImportObsoleted { ImportObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Input file path /// Input file path
#[clap(short = 'i')] #[clap(short = 'i')]
input_file: String, input_file: String,
/// Override publisher (use this instead of the one in the export file) /// Override publisher (use this instead of the one in the export file)
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: Option<String>, publisher: Option<String>,
}, },
/// Clean up obsoleted packages older than a specified TTL (time-to-live) /// Clean up obsoleted packages older than a specified TTL (time-to-live)
CleanupObsoleted { CleanupObsoleted {
/// Path or URI of the repository /// Path or URI of the repository
#[clap(short = 's')] #[clap(short = 's')]
repo_uri_or_path: String, repo_uri_or_path: String,
/// Publisher to clean up obsoleted packages for /// Publisher to clean up obsoleted packages for
#[clap(short = 'p')] #[clap(short = 'p')]
publisher: String, publisher: String,
/// TTL in days /// TTL in days
#[clap(short = 't', long = "ttl-days", default_value = "90")] #[clap(short = 't', long = "ttl-days", default_value = "90")]
ttl_days: u32, ttl_days: u32,
/// Perform a dry run (don't actually remove packages) /// Perform a dry run (don't actually remove packages)
#[clap(short = 'n', long = "dry-run")] #[clap(short = 'n', long = "dry-run")]
dry_run: bool, dry_run: bool,
@ -1273,8 +1273,8 @@ fn main() -> Result<()> {
info!("Repository imported successfully"); info!("Repository imported successfully");
Ok(()) Ok(())
}, }
Commands::ObsoletePackage { Commands::ObsoletePackage {
repo_uri_or_path, repo_uri_or_path,
publisher, publisher,
@ -1283,41 +1283,41 @@ fn main() -> Result<()> {
replaced_by, replaced_by,
} => { } => {
info!("Marking package as obsoleted: {}", fmri); info!("Marking package as obsoleted: {}", fmri);
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Parse the FMRI // Parse the FMRI
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?; let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
// Get the manifest for the package using the helper method // Get the manifest for the package using the helper method
let manifest_path = FileBackend::construct_manifest_path( let manifest_path = FileBackend::construct_manifest_path(
&repo.path, &repo.path,
publisher, publisher,
parsed_fmri.stem(), parsed_fmri.stem(),
&parsed_fmri.version() &parsed_fmri.version(),
); );
println!("Looking for manifest at: {}", manifest_path.display()); println!("Looking for manifest at: {}", manifest_path.display());
println!("Publisher: {}", publisher); println!("Publisher: {}", publisher);
println!("Stem: {}", parsed_fmri.stem()); println!("Stem: {}", parsed_fmri.stem());
println!("Version: {}", parsed_fmri.version()); println!("Version: {}", parsed_fmri.version());
if !manifest_path.exists() { if !manifest_path.exists() {
return Err(Pkg6RepoError::from(format!( return Err(Pkg6RepoError::from(format!(
"Package not found: {}", "Package not found: {}",
parsed_fmri parsed_fmri
))); )));
} }
// Read the manifest content // Read the manifest content
let manifest_content = std::fs::read_to_string(&manifest_path)?; let manifest_content = std::fs::read_to_string(&manifest_path)?;
// Create a new scope for the obsoleted_manager to ensure it's dropped before we call repo.rebuild() // Create a new scope for the obsoleted_manager to ensure it's dropped before we call repo.rebuild()
{ {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Store the obsoleted package // Store the obsoleted package
obsoleted_manager.store_obsoleted_package( obsoleted_manager.store_obsoleted_package(
publisher, publisher,
@ -1327,17 +1327,17 @@ fn main() -> Result<()> {
message.clone(), message.clone(),
)?; )?;
} // obsoleted_manager is dropped here, releasing the mutable borrow on repo } // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Remove the original package from the repository // Remove the original package from the repository
std::fs::remove_file(&manifest_path)?; std::fs::remove_file(&manifest_path)?;
// Rebuild the catalog to reflect the changes // Rebuild the catalog to reflect the changes
repo.rebuild(Some(publisher), false, false)?; repo.rebuild(Some(publisher), false, false)?;
info!("Package marked as obsoleted successfully: {}", parsed_fmri); info!("Package marked as obsoleted successfully: {}", parsed_fmri);
Ok(()) Ok(())
}, }
Commands::ListObsoleted { Commands::ListObsoleted {
repo_uri_or_path, repo_uri_or_path,
format, format,
@ -1347,39 +1347,43 @@ fn main() -> Result<()> {
page_size, page_size,
} => { } => {
info!("Listing obsoleted packages for publisher: {}", publisher); info!("Listing obsoleted packages for publisher: {}", publisher);
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Get the obsoleted packages in a new scope to avoid borrowing issues // Get the obsoleted packages in a new scope to avoid borrowing issues
let paginated_result = { let paginated_result = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// List obsoleted packages with pagination // 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 }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Determine the output format // Determine the output format
let output_format = format.as_deref().unwrap_or("table"); let output_format = format.as_deref().unwrap_or("table");
match output_format { match output_format {
"table" => { "table" => {
// Print headers if not omitted // Print headers if not omitted
if !omit_headers { if !omit_headers {
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER"); println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
} }
// Print packages // Print packages
for fmri in &paginated_result.packages { for fmri in &paginated_result.packages {
// Format version and publisher, handling optional fields // Format version and publisher, handling optional fields
let version_str = fmri.version(); let version_str = fmri.version();
let publisher_str = match &fmri.publisher { let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(), Some(publisher) => publisher.clone(),
None => String::new(), None => String::new(),
}; };
println!( println!(
"{:<30} {:<15} {:<10}", "{:<30} {:<15} {:<10}",
fmri.stem(), fmri.stem(),
@ -1387,13 +1391,15 @@ fn main() -> Result<()> {
publisher_str publisher_str
); );
} }
// Print pagination information // Print pagination information
println!("\nPage {} of {} (Total: {} packages)", println!(
paginated_result.page, "\nPage {} of {} (Total: {} packages)",
paginated_result.total_pages, paginated_result.page,
paginated_result.total_count); paginated_result.total_pages,
}, paginated_result.total_count
);
}
"json" => { "json" => {
// Create a JSON representation of the obsoleted packages with pagination info // Create a JSON representation of the obsoleted packages with pagination info
#[derive(Serialize)] #[derive(Serialize)]
@ -1404,8 +1410,12 @@ fn main() -> Result<()> {
total_pages: usize, total_pages: usize,
total_count: usize, 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 { let paginated_output = PaginatedOutput {
packages: packages_str, packages: packages_str,
page: paginated_result.page, page: paginated_result.page,
@ -1413,53 +1423,50 @@ fn main() -> Result<()> {
total_pages: paginated_result.total_pages, total_pages: paginated_result.total_pages,
total_count: paginated_result.total_count, total_count: paginated_result.total_count,
}; };
// Serialize to pretty-printed JSON // Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&paginated_output) let json_output = serde_json::to_string_pretty(&paginated_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output); println!("{}", json_output);
}, }
"tsv" => { "tsv" => {
// Print headers if not omitted // Print headers if not omitted
if !omit_headers { if !omit_headers {
println!("NAME\tVERSION\tPUBLISHER"); println!("NAME\tVERSION\tPUBLISHER");
} }
// Print packages as tab-separated values // Print packages as tab-separated values
for fmri in &paginated_result.packages { for fmri in &paginated_result.packages {
// Format version and publisher, handling optional fields // Format version and publisher, handling optional fields
let version_str = fmri.version(); let version_str = fmri.version();
let publisher_str = match &fmri.publisher { let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(), Some(publisher) => publisher.clone(),
None => String::new(), None => String::new(),
}; };
println!( println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
"{}\t{}\t{}",
fmri.stem(),
version_str,
publisher_str
);
} }
// Print pagination information // Print pagination information
println!("\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}", println!(
paginated_result.page, "\nPAGE\t{}\nTOTAL_PAGES\t{}\nTOTAL_COUNT\t{}",
paginated_result.total_pages, paginated_result.page,
paginated_result.total_count); paginated_result.total_pages,
}, paginated_result.total_count
);
}
_ => { _ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat( return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(), output_format.to_string(),
)); ));
} }
} }
Ok(()) Ok(())
}, }
Commands::ShowObsoleted { Commands::ShowObsoleted {
repo_uri_or_path, repo_uri_or_path,
format, format,
@ -1467,18 +1474,18 @@ fn main() -> Result<()> {
fmri, fmri,
} => { } => {
info!("Showing details of obsoleted package: {}", fmri); info!("Showing details of obsoleted package: {}", fmri);
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Parse the FMRI // Parse the FMRI
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?; let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
// Get the obsoleted package metadata in a new scope to avoid borrowing issues // Get the obsoleted package metadata in a new scope to avoid borrowing issues
let metadata = { let metadata = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Get the obsoleted package metadata // Get the obsoleted package metadata
match obsoleted_manager.get_obsoleted_package_metadata(publisher, &parsed_fmri)? { match obsoleted_manager.get_obsoleted_package_metadata(publisher, &parsed_fmri)? {
Some(metadata) => metadata, Some(metadata) => metadata,
@ -1490,30 +1497,30 @@ fn main() -> Result<()> {
} }
} }
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Determine the output format // Determine the output format
let output_format = format.as_deref().unwrap_or("table"); let output_format = format.as_deref().unwrap_or("table");
match output_format { match output_format {
"table" => { "table" => {
println!("FMRI: {}", metadata.fmri); println!("FMRI: {}", metadata.fmri);
println!("Status: {}", metadata.status); println!("Status: {}", metadata.status);
println!("Obsolescence Date: {}", metadata.obsolescence_date); println!("Obsolescence Date: {}", metadata.obsolescence_date);
if let Some(msg) = &metadata.deprecation_message { if let Some(msg) = &metadata.deprecation_message {
println!("Deprecation Message: {}", msg); println!("Deprecation Message: {}", msg);
} }
if let Some(replacements) = &metadata.obsoleted_by { if let Some(replacements) = &metadata.obsoleted_by {
println!("Replaced By:"); println!("Replaced By:");
for replacement in replacements { for replacement in replacements {
println!(" {}", replacement); println!(" {}", replacement);
} }
} }
println!("Metadata Version: {}", metadata.metadata_version); println!("Metadata Version: {}", metadata.metadata_version);
println!("Content Hash: {}", metadata.content_hash); println!("Content Hash: {}", metadata.content_hash);
}, }
"json" => { "json" => {
// Create a JSON representation of the obsoleted package details // Create a JSON representation of the obsoleted package details
let details_output = ObsoletedPackageDetailsOutput { let details_output = ObsoletedPackageDetailsOutput {
@ -1525,41 +1532,41 @@ fn main() -> Result<()> {
metadata_version: metadata.metadata_version, metadata_version: metadata.metadata_version,
content_hash: metadata.content_hash, content_hash: metadata.content_hash,
}; };
// Serialize to pretty-printed JSON // Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&details_output) let json_output = serde_json::to_string_pretty(&details_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output); println!("{}", json_output);
}, }
"tsv" => { "tsv" => {
println!("FMRI\t{}", metadata.fmri); println!("FMRI\t{}", metadata.fmri);
println!("Status\t{}", metadata.status); println!("Status\t{}", metadata.status);
println!("ObsolescenceDate\t{}", metadata.obsolescence_date); println!("ObsolescenceDate\t{}", metadata.obsolescence_date);
if let Some(msg) = &metadata.deprecation_message { if let Some(msg) = &metadata.deprecation_message {
println!("DeprecationMessage\t{}", msg); println!("DeprecationMessage\t{}", msg);
} }
if let Some(replacements) = &metadata.obsoleted_by { if let Some(replacements) = &metadata.obsoleted_by {
for (i, replacement) in replacements.iter().enumerate() { for (i, replacement) in replacements.iter().enumerate() {
println!("ReplacedBy{}\t{}", i + 1, replacement); println!("ReplacedBy{}\t{}", i + 1, replacement);
} }
} }
println!("MetadataVersion\t{}", metadata.metadata_version); println!("MetadataVersion\t{}", metadata.metadata_version);
println!("ContentHash\t{}", metadata.content_hash); println!("ContentHash\t{}", metadata.content_hash);
}, }
_ => { _ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat( return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(), output_format.to_string(),
)); ));
} }
} }
Ok(()) Ok(())
}, }
Commands::SearchObsoleted { Commands::SearchObsoleted {
repo_uri_or_path, repo_uri_or_path,
format, format,
@ -1568,47 +1575,51 @@ fn main() -> Result<()> {
pattern, pattern,
limit, limit,
} => { } => {
info!("Searching for obsoleted packages: {} (publisher: {})", pattern, publisher); info!(
"Searching for obsoleted packages: {} (publisher: {})",
pattern, publisher
);
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Get the obsoleted packages in a new scope to avoid borrowing issues // Get the obsoleted packages in a new scope to avoid borrowing issues
let obsoleted_packages = { let obsoleted_packages = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Search for obsoleted packages // 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 // Apply limit if specified
if let Some(max_results) = limit { if let Some(max_results) = limit {
packages.truncate(*max_results); packages.truncate(*max_results);
} }
packages packages
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Determine the output format // Determine the output format
let output_format = format.as_deref().unwrap_or("table"); let output_format = format.as_deref().unwrap_or("table");
match output_format { match output_format {
"table" => { "table" => {
// Print headers if not omitted // Print headers if not omitted
if !omit_headers { if !omit_headers {
println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER"); println!("{:<30} {:<15} {:<10}", "NAME", "VERSION", "PUBLISHER");
} }
// Print packages // Print packages
for fmri in obsoleted_packages { for fmri in obsoleted_packages {
// Format version and publisher, handling optional fields // Format version and publisher, handling optional fields
let version_str = fmri.version(); let version_str = fmri.version();
let publisher_str = match &fmri.publisher { let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(), Some(publisher) => publisher.clone(),
None => String::new(), None => String::new(),
}; };
println!( println!(
"{:<30} {:<15} {:<10}", "{:<30} {:<15} {:<10}",
fmri.stem(), fmri.stem(),
@ -1616,102 +1627,101 @@ fn main() -> Result<()> {
publisher_str publisher_str
); );
} }
}, }
"json" => { "json" => {
// Create a JSON representation of the obsoleted packages // 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 { let packages_output = ObsoletedPackagesOutput {
packages: packages_str, packages: packages_str,
}; };
// Serialize to pretty-printed JSON // Serialize to pretty-printed JSON
let json_output = serde_json::to_string_pretty(&packages_output) let json_output = serde_json::to_string_pretty(&packages_output)
.unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)); .unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
println!("{}", json_output); println!("{}", json_output);
}, }
"tsv" => { "tsv" => {
// Print headers if not omitted // Print headers if not omitted
if !omit_headers { if !omit_headers {
println!("NAME\tVERSION\tPUBLISHER"); println!("NAME\tVERSION\tPUBLISHER");
} }
// Print packages as tab-separated values // Print packages as tab-separated values
for fmri in obsoleted_packages { for fmri in obsoleted_packages {
// Format version and publisher, handling optional fields // Format version and publisher, handling optional fields
let version_str = fmri.version(); let version_str = fmri.version();
let publisher_str = match &fmri.publisher { let publisher_str = match &fmri.publisher {
Some(publisher) => publisher.clone(), Some(publisher) => publisher.clone(),
None => String::new(), None => String::new(),
}; };
println!( println!("{}\t{}\t{}", fmri.stem(), version_str, publisher_str);
"{}\t{}\t{}",
fmri.stem(),
version_str,
publisher_str
);
} }
}, }
_ => { _ => {
return Err(Pkg6RepoError::UnsupportedOutputFormat( return Err(Pkg6RepoError::UnsupportedOutputFormat(
output_format.to_string(), output_format.to_string(),
)); ));
} }
} }
Ok(()) Ok(())
}, }
Commands::RestoreObsoleted { Commands::RestoreObsoleted {
repo_uri_or_path, repo_uri_or_path,
publisher, publisher,
fmri, fmri,
no_rebuild, no_rebuild,
} => { } => {
info!("Restoring obsoleted package: {} (publisher: {})", fmri, publisher); info!(
"Restoring obsoleted package: {} (publisher: {})",
fmri, publisher
);
// Parse the FMRI // Parse the FMRI
let parsed_fmri = libips::fmri::Fmri::parse(fmri)?; let parsed_fmri = libips::fmri::Fmri::parse(fmri)?;
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Get the manifest content and remove the obsoleted package // Get the manifest content and remove the obsoleted package
let manifest_content = { let manifest_content = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Get the manifest content and remove the obsoleted package // Get the manifest content and remove the obsoleted package
obsoleted_manager.get_and_remove_obsoleted_package(publisher, &parsed_fmri)? obsoleted_manager.get_and_remove_obsoleted_package(publisher, &parsed_fmri)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
// Parse the manifest // Parse the manifest
let manifest = libips::actions::Manifest::parse_string(manifest_content)?; let manifest = libips::actions::Manifest::parse_string(manifest_content)?;
// Begin a transaction // Begin a transaction
let mut transaction = repo.begin_transaction()?; let mut transaction = repo.begin_transaction()?;
// Set the publisher for the transaction // Set the publisher for the transaction
transaction.set_publisher(publisher); transaction.set_publisher(publisher);
// Update the manifest in the transaction // Update the manifest in the transaction
transaction.update_manifest(manifest); transaction.update_manifest(manifest);
// Commit the transaction // Commit the transaction
transaction.commit()?; transaction.commit()?;
// Rebuild the catalog if not disabled // Rebuild the catalog if not disabled
if !no_rebuild { if !no_rebuild {
info!("Rebuilding catalog..."); info!("Rebuilding catalog...");
repo.rebuild(Some(publisher), false, false)?; repo.rebuild(Some(publisher), false, false)?;
} }
info!("Package restored successfully: {}", parsed_fmri); info!("Package restored successfully: {}", parsed_fmri);
Ok(()) Ok(())
}, }
Commands::ExportObsoleted { Commands::ExportObsoleted {
repo_uri_or_path, repo_uri_or_path,
publisher, publisher,
@ -1719,15 +1729,15 @@ fn main() -> Result<()> {
pattern, pattern,
} => { } => {
info!("Exporting obsoleted packages for publisher: {}", publisher); info!("Exporting obsoleted packages for publisher: {}", publisher);
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Export the obsoleted packages // Export the obsoleted packages
let count = { let count = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Export the obsoleted packages // Export the obsoleted packages
let output_path = PathBuf::from(output_file); let output_path = PathBuf::from(output_file);
obsoleted_manager.export_obsoleted_packages( obsoleted_manager.export_obsoleted_packages(
@ -1736,38 +1746,35 @@ fn main() -> Result<()> {
&output_path, &output_path,
)? )?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
info!("Exported {} obsoleted packages to {}", count, output_file); info!("Exported {} obsoleted packages to {}", count, output_file);
Ok(()) Ok(())
}, }
Commands::ImportObsoleted { Commands::ImportObsoleted {
repo_uri_or_path, repo_uri_or_path,
input_file, input_file,
publisher, publisher,
} => { } => {
info!("Importing obsoleted packages from {}", input_file); info!("Importing obsoleted packages from {}", input_file);
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Import the obsoleted packages // Import the obsoleted packages
let count = { let count = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Import the obsoleted packages // Import the obsoleted packages
let input_path = PathBuf::from(input_file); let input_path = PathBuf::from(input_file);
obsoleted_manager.import_obsoleted_packages( obsoleted_manager.import_obsoleted_packages(&input_path, publisher.as_deref())?
&input_path,
publisher.as_deref(),
)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
info!("Imported {} obsoleted packages", count); info!("Imported {} obsoleted packages", count);
Ok(()) Ok(())
}, }
Commands::CleanupObsoleted { Commands::CleanupObsoleted {
repo_uri_or_path, repo_uri_or_path,
publisher, publisher,
@ -1775,35 +1782,36 @@ fn main() -> Result<()> {
dry_run, dry_run,
} => { } => {
if *dry_run { if *dry_run {
info!("Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}", info!(
ttl_days, publisher); "Dry run: Cleaning up obsoleted packages older than {} days for publisher: {}",
ttl_days, publisher
);
} else { } else {
info!("Cleaning up obsoleted packages older than {} days for publisher: {}", info!(
ttl_days, publisher); "Cleaning up obsoleted packages older than {} days for publisher: {}",
ttl_days, publisher
);
} }
// Open the repository // Open the repository
let mut repo = FileBackend::open(repo_uri_or_path)?; let mut repo = FileBackend::open(repo_uri_or_path)?;
// Clean up the obsoleted packages // Clean up the obsoleted packages
let count = { let count = {
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = repo.get_obsoleted_manager()?; let obsoleted_manager = repo.get_obsoleted_manager()?;
// Clean up the obsoleted packages // Clean up the obsoleted packages
obsoleted_manager.cleanup_obsoleted_packages_older_than_ttl( obsoleted_manager
publisher, .cleanup_obsoleted_packages_older_than_ttl(publisher, *ttl_days, *dry_run)?
*ttl_days,
*dry_run,
)?
}; // obsoleted_manager is dropped here, releasing the mutable borrow on repo }; // obsoleted_manager is dropped here, releasing the mutable borrow on repo
if *dry_run { if *dry_run {
info!("Dry run: Would remove {} obsoleted packages", count); info!("Dry run: Would remove {} obsoleted packages", count);
} else { } else {
info!("Successfully removed {} obsoleted packages", count); info!("Successfully removed {} obsoleted packages", count);
} }
Ok(()) Ok(())
} }
} }

View file

@ -222,7 +222,8 @@ impl Pkg5Importer {
} }
// Import packages and get counts // 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; let total_count = regular_count + obsoleted_count;
// Rebuild catalog and search index // Rebuild catalog and search index
@ -235,7 +236,7 @@ impl Pkg5Importer {
info!(" Total packages processed: {}", total_count); info!(" Total packages processed: {}", total_count);
info!(" Regular packages imported: {}", regular_count); info!(" Regular packages imported: {}", regular_count);
info!(" Obsoleted packages stored: {}", obsoleted_count); info!(" Obsoleted packages stored: {}", obsoleted_count);
Ok(()) Ok(())
} }
@ -267,7 +268,7 @@ impl Pkg5Importer {
} }
/// Imports packages from the source repository /// Imports packages from the source repository
/// ///
/// Returns a tuple of (regular_package_count, obsoleted_package_count) /// Returns a tuple of (regular_package_count, obsoleted_package_count)
fn import_packages( fn import_packages(
&self, &self,
@ -349,13 +350,15 @@ impl Pkg5Importer {
} }
let total_package_count = regular_package_count + obsoleted_package_count; let total_package_count = regular_package_count + obsoleted_package_count;
info!("Imported {} packages ({} regular, {} obsoleted)", info!(
total_package_count, regular_package_count, obsoleted_package_count); "Imported {} packages ({} regular, {} obsoleted)",
total_package_count, regular_package_count, obsoleted_package_count
);
Ok((regular_package_count, obsoleted_package_count)) Ok((regular_package_count, obsoleted_package_count))
} }
/// Imports a specific package version /// Imports a specific package version
/// ///
/// Returns a boolean indicating whether the package was obsoleted /// Returns a boolean indicating whether the package was obsoleted
fn import_package_version( fn import_package_version(
&self, &self,
@ -389,7 +392,7 @@ impl Pkg5Importer {
// Check if this is an obsoleted package // Check if this is an obsoleted package
let mut is_obsoleted = false; let mut is_obsoleted = false;
let mut fmri_str = String::new(); let mut fmri_str = String::new();
// Extract the FMRI from the manifest // Extract the FMRI from the manifest
for attr in &manifest.attributes { for attr in &manifest.attributes {
if attr.key == "pkg.fmri" && !attr.values.is_empty() { if attr.key == "pkg.fmri" && !attr.values.is_empty() {
@ -397,7 +400,7 @@ impl Pkg5Importer {
break; break;
} }
} }
// Check for pkg.obsolete attribute // Check for pkg.obsolete attribute
for attr in &manifest.attributes { for attr in &manifest.attributes {
if attr.key == "pkg.obsolete" && !attr.values.is_empty() { if attr.key == "pkg.obsolete" && !attr.values.is_empty() {
@ -408,11 +411,11 @@ impl Pkg5Importer {
} }
} }
} }
// If this is an obsoleted package, store it in the obsoleted directory // If this is an obsoleted package, store it in the obsoleted directory
if is_obsoleted && !fmri_str.is_empty() { if is_obsoleted && !fmri_str.is_empty() {
debug!("Handling obsoleted package: {}", fmri_str); debug!("Handling obsoleted package: {}", fmri_str);
// Parse the FMRI // Parse the FMRI
let fmri = match Fmri::parse(&fmri_str) { let fmri = match Fmri::parse(&fmri_str) {
Ok(fmri) => fmri, Ok(fmri) => fmri,
@ -424,10 +427,10 @@ impl Pkg5Importer {
))); )));
} }
}; };
// Get the obsoleted package manager // Get the obsoleted package manager
let obsoleted_manager = dest_repo.get_obsoleted_manager()?; let obsoleted_manager = dest_repo.get_obsoleted_manager()?;
// Store the obsoleted package with null hash (don't store the original manifest) // Store the obsoleted package with null hash (don't store the original manifest)
// This saves storage space for obsoleted packages that don't provide any useful // This saves storage space for obsoleted packages that don't provide any useful
// information beyond the fact that they are obsoleted. When a client requests // information beyond the fact that they are obsoleted. When a client requests
@ -439,18 +442,18 @@ impl Pkg5Importer {
publisher, publisher,
&fmri, &fmri,
&manifest_content, &manifest_content,
None, // No obsoleted_by information available None, // No obsoleted_by information available
None, // No deprecation message available None, // No deprecation message available
false, // Don't store the original manifest, use null hash instead false, // Don't store the original manifest, use null hash instead
)?; )?;
info!("Stored obsoleted package: {}", fmri); info!("Stored obsoleted package: {}", fmri);
return Ok(true); // Return true to indicate this was an obsoleted package return Ok(true); // Return true to indicate this was an obsoleted package
} }
// For non-obsoleted packages, proceed with normal import // For non-obsoleted packages, proceed with normal import
debug!("Processing regular (non-obsoleted) package"); debug!("Processing regular (non-obsoleted) package");
// Begin a transaction // Begin a transaction
debug!("Beginning transaction"); debug!("Beginning transaction");
let mut transaction = dest_repo.begin_transaction()?; let mut transaction = dest_repo.begin_transaction()?;
@ -462,7 +465,8 @@ impl Pkg5Importer {
// Debug the repository structure // Debug the repository structure
debug!( debug!(
"Publisher directory: {}", "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 // Extract files referenced in the manifest
@ -486,10 +490,10 @@ impl Pkg5Importer {
let first_two = &hash[0..2]; let first_two = &hash[0..2];
let next_two = &hash[2..4]; let next_two = &hash[2..4];
let file_path_new = file_dir.join(first_two).join(next_two).join(&hash); let file_path_new = file_dir.join(first_two).join(next_two).join(&hash);
// Fall back to the old one-level hierarchy if the file doesn't exist in the new structure // Fall back to the old one-level hierarchy if the file doesn't exist in the new structure
let file_path_old = file_dir.join(first_two).join(&hash); let file_path_old = file_dir.join(first_two).join(&hash);
// Use the path that exists // Use the path that exists
let file_path = if file_path_new.exists() { let file_path = if file_path_new.exists() {
file_path_new file_path_new

View file

@ -82,15 +82,18 @@ impl std::fmt::Display for OutputFormat {
#[derive(Error, Debug, Diagnostic)] #[derive(Error, Debug, Diagnostic)]
#[error("pkgtree error: {message}")] #[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 { struct PkgTreeError {
message: String, message: String,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Edge { struct Edge {
to: String, // target stem to: String, // target stem
dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.) dep_type: String, // dependency type (e.g., require, incorporate, optional, etc.)
} }
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -101,7 +104,10 @@ struct Graph {
impl Graph { impl Graph {
fn add_edge(&mut self, from: String, to: String, dep_type: String) { 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> { fn stems(&self) -> impl Iterator<Item = &String> {
@ -127,8 +133,9 @@ fn main() -> Result<()> {
tracing_subscriber::fmt().with_env_filter(env_filter).init(); tracing_subscriber::fmt().with_env_filter(env_filter).init();
// Load image // Load image
let image = Image::load(&cli.image_path) let image = Image::load(&cli.image_path).map_err(|e| PkgTreeError {
.map_err(|e| PkgTreeError { message: format!("Failed to load image at {:?}: {}", cli.image_path, e) })?; message: format!("Failed to load image at {:?}: {}", cli.image_path, e),
})?;
// Targeted analysis of solver error file has top priority if provided // Targeted analysis of solver error file has top priority if provided
if let Some(err_path) = &cli.solver_error_file { if let Some(err_path) = &cli.solver_error_file {
@ -145,16 +152,27 @@ fn main() -> Result<()> {
// Dangling dependency scan has priority over graph mode // Dangling dependency scan has priority over graph mode
if cli.find_dangling { 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(()); return Ok(());
} }
// Graph mode // Graph mode
// Query catalog (filtered if --package provided) // Query catalog (filtered if --package provided)
let mut pkgs = if let Some(ref needle) = cli.package { 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 { } 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 // 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 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() { 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 { for p in &pkgs {
match image.get_manifest_from_catalog(&p.fmri) { match image.get_manifest_from_catalog(&p.fmri) {
Ok(Some(manifest)) => { Ok(Some(manifest)) => {
let from_stem = p.fmri.stem().to_string(); let from_stem = p.fmri.stem().to_string();
for dep in manifest.dependencies { for dep in manifest.dependencies {
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { if dep.dependency_type != "require" && dep.dependency_type != "incorporate"
{
continue; continue;
} }
if let Some(dep_fmri) = dep.fmri { 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 roots: Vec<String> = if let Some(ref needle) = filter_substr {
let mut r = HashSet::new(); let mut r = HashSet::new();
for k in graph.adj.keys() { 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() r.into_iter().collect()
} else { } else {
@ -253,19 +276,47 @@ fn main() -> Result<()> {
OutputFormat::Json => { OutputFormat::Json => {
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
struct JsonEdge { from: String, to: String, dep_type: String } struct JsonEdge {
from: String,
to: String,
dep_type: String,
}
#[derive(Serialize)] #[derive(Serialize)]
struct JsonCycle { nodes: Vec<String>, edges: Vec<String> } struct JsonCycle {
nodes: Vec<String>,
edges: Vec<String>,
}
#[derive(Serialize)] #[derive(Serialize)]
struct Payload { edges: Vec<JsonEdge>, cycles: Vec<JsonCycle> } struct Payload {
edges: Vec<JsonEdge>,
cycles: Vec<JsonCycle>,
}
let mut edges = Vec::new(); let mut edges = Vec::new();
for (from, es) in &graph.adj { 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 cycles_json = cycles
let payload = Payload { edges, cycles: cycles_json }; .iter()
println!("{}", serde_json::to_string_pretty(&payload).into_diagnostic()?); .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 path: Vec<String>, // path from root to the missing dependency stem
stem: String, // the missing stem stem: String, // the missing stem
constraint: DepConstraint, constraint: DepConstraint,
details: String, // human description details: String, // human description
} }
#[derive(Default)] #[derive(Default)]
@ -296,25 +347,42 @@ struct AdviceContext {
catalog_cache: HashMap<String, Vec<(String, libips::fmri::Fmri)>>, // stem -> [(publisher, fmri)] catalog_cache: HashMap<String, Vec<(String, libips::fmri::Fmri)>>, // stem -> [(publisher, fmri)]
manifest_cache: HashMap<String, libips::actions::Manifest>, // fmri string -> manifest manifest_cache: HashMap<String, libips::actions::Manifest>, // fmri string -> manifest
lock_cache: HashMap<String, Option<String>>, // stem -> release lock 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 { impl AdviceContext {
fn new(publisher: Option<String>, advice_cap: usize) -> Self { 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); info!("Advisor analyzing installability for root: {}", root_stem);
// Find best candidate for root // Find best candidate for root
let root_fmri = match find_best_candidate(image, ctx, root_stem, None, None) { let root_fmri = match find_best_candidate(image, ctx, root_stem, None, None) {
Ok(Some(fmri)) => fmri, Ok(Some(fmri)) => fmri,
Ok(None) => { Ok(None) => {
println!("No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.", println!(
root_stem, "No candidates found for root package '{}'.\n- Suggestion: run 'pkg6 refresh' to update catalogs.\n- Ensure publisher{} contains the package.",
ctx.publisher.as_ref().map(|p| format!(" '{}')", p)).unwrap_or_else(|| "".to_string())); root_stem,
ctx.publisher
.as_ref()
.map(|p| format!(" '{}')", p))
.unwrap_or_else(|| "".to_string())
);
return Ok(()); return Ok(());
} }
Err(e) => return Err(e), 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 issues: Vec<AdviceIssue> = Vec::new();
let mut seen: HashSet<String> = HashSet::new(); let mut seen: HashSet<String> = HashSet::new();
let mut path: Vec<String> = vec![root_stem.to_string()]; 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 // Print summary
if issues.is_empty() { 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 { } else {
println!("Found {} installability issue(s):", issues.len()); println!("Found {} installability issue(s):", issues.len());
for (i, iss) in issues.iter().enumerate() { for (i, iss) in issues.iter().enumerate() {
let constraint_str = format!( let constraint_str = format!(
"{}{}", "{}{}",
iss.constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(), iss.constraint
iss.constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(), .release
).trim().to_string(); .as_ref()
println!(" {}. {}\n - Path: {}\n - Constraint: {}\n - Details: {}", .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, i + 1,
format!("No viable candidates for '{}'", iss.stem), format!("No viable candidates for '{}'", iss.stem),
iss.path.join(" -> "), 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, iss.details,
); );
// Suggestions // Suggestions
println!(" - Suggestions:"); println!(" - Suggestions:");
println!(" • Add or publish a matching package for '{}'{}{}.", println!(
" • Add or publish a matching package for '{}'{}{}.",
iss.stem, iss.stem,
iss.constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(), iss.constraint
iss.constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default()); .release
println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases."); .as_ref()
if let Some(lock) = get_incorporated_release_cached(image, ctx, &iss.stem).ok().flatten() { .map(|r| format!(" (release={})", r))
println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", iss.stem, lock); .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'."); println!(" • Ensure catalogs are up to date: 'pkg6 refresh'.");
} }
@ -374,7 +487,9 @@ fn advise_recursive(
seen: &mut HashSet<String>, seen: &mut HashSet<String>,
issues: &mut Vec<AdviceIssue>, issues: &mut Vec<AdviceIssue>,
) -> Result<()> { ) -> 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) // Load manifest of the current FMRI (cached)
let manifest = get_manifest_cached(image, ctx, fmri)?; let manifest = get_manifest_cached(image, ctx, fmri)?;
@ -383,30 +498,61 @@ fn advise_recursive(
let mut constrained = Vec::new(); let mut constrained = Vec::new();
let mut unconstrained = Vec::new(); let mut unconstrained = Vec::new();
for dep in manifest.dependencies { 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(); let has_fmri = dep.fmri.is_some();
if !has_fmri { continue; } if !has_fmri {
continue;
}
let c = extract_constraint(&dep.optional); 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()) { for (dep, constraint) in constrained.into_iter().chain(unconstrained.into_iter()) {
if ctx.advice_cap != 0 && processed >= ctx.advice_cap { 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; break;
} }
processed += 1; processed += 1;
let dep_stem = dep.fmri.unwrap().stem().to_string(); 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) => { Some(next_fmri) => {
// Continue recursion if not seen and depth allows // Continue recursion if not seen and depth allows
if !seen.contains(&dep_stem) { if !seen.contains(&dep_stem) {
seen.insert(dep_stem.clone()); seen.insert(dep_stem.clone());
path.push(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(); path.pop();
} }
} }
@ -438,15 +584,28 @@ fn extract_constraint(optional: &[libips::actions::Property]) -> DepConstraint {
DepConstraint { release, branch } 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 // List available releases/branches for informational purposes
let mut available: Vec<String> = Vec::new(); let mut available: Vec<String> = Vec::new();
if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) { if let Ok(list) = query_catalog_cached_mut(image, ctx, stem) {
for (pubname, fmri) in list { for (pubname, fmri) in list {
if let Some(ref pfilter) = ctx.publisher { if &pubname != pfilter { continue; } } if let Some(ref pfilter) = ctx.publisher {
if fmri.stem() != stem { continue; } if &pubname != pfilter {
continue;
}
}
if fmri.stem() != stem {
continue;
}
let ver = fmri.version(); let ver = fmri.version();
if ver.is_empty() { continue; } if ver.is_empty() {
continue;
}
available.push(ver); available.push(ver);
} }
} }
@ -460,17 +619,43 @@ fn build_missing_detail(image: &Image, ctx: &mut AdviceContext, stem: &str, cons
available.join(", ") 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) { 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), Some(lr)) => format!(
(Some(r), Some(b), None) => format!("Required release={}, branch={} not found. Available versions: {}", r, b, available_str), "Required release={}, branch={} not found. Incorporation lock release={} may also constrain candidates. Available versions: {}",
(Some(r), None, Some(lr)) => format!("Required release={} not found. Incorporation lock release={} present. Available versions: {}", r, lr, available_str), r, b, 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), (Some(r), Some(b), None) => format!(
(None, Some(b), None) => format!("Required branch={} not found. Available versions: {}", b, available_str), "Required release={}, branch={} not found. Available versions: {}",
(None, None, Some(lr)) => format!("No candidates matched. Incorporation lock release={} present. Available versions: {}", lr, available_str), r, b, available_str
(None, None, None) => format!("No candidates matched. Available versions: {}", 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(); let mut candidates: Vec<(String, libips::fmri::Fmri)> = Vec::new();
// Prefer matching release from incorporation lock, unless explicit req_release provided // 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)? { for (pubf, pfmri) in query_catalog_cached(image, ctx, stem)? {
if let Some(ref pfilter) = ctx.publisher { if &pubf != pfilter { continue; } } if let Some(ref pfilter) = ctx.publisher {
if pfmri.stem() != stem { continue; } if &pubf != pfilter {
continue;
}
}
if pfmri.stem() != stem {
continue;
}
let ver = pfmri.version(); 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 // Parse version string to extract release and branch heuristically: release,branch-rest
let rel = version_release(&ver); let rel = version_release(&ver);
let br = version_branch(&ver); let br = version_branch(&ver);
if let Some(req_r) = req_release { 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() { } 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())); 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. // 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. // To keep code simple, provide a small wrapper that fills the cache when needed.
// We'll implement a separate function that has mutable ctx. // 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) query_catalog_cached_mut(image, &mut tmp_ctx, stem)
} }
@ -563,10 +773,9 @@ fn query_catalog_cached_mut(
return Ok(v.clone()); return Ok(v.clone());
} }
let mut out = Vec::new(); let mut out = Vec::new();
for p in image for p in image.query_catalog(Some(stem)).map_err(|e| PkgTreeError {
.query_catalog(Some(stem)) message: format!("Failed to query catalog for {}: {}", stem, e),
.map_err(|e| PkgTreeError { message: format!("Failed to query catalog for {}: {}", stem, e) })? })? {
{
out.push((p.publisher, p.fmri)); out.push((p.publisher, p.fmri));
} }
ctx.catalog_cache.insert(stem.to_string(), out.clone()); ctx.catalog_cache.insert(stem.to_string(), out.clone());
@ -584,7 +793,9 @@ fn get_manifest_cached(
} }
let manifest_opt = image let manifest_opt = image
.get_manifest_from_catalog(fmri) .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()); let manifest = manifest_opt.unwrap_or_else(|| libips::actions::Manifest::new());
ctx.manifest_cache.insert(key, manifest.clone()); ctx.manifest_cache.insert(key, manifest.clone());
Ok(manifest) Ok(manifest)
@ -595,7 +806,9 @@ fn get_incorporated_release_cached(
ctx: &mut AdviceContext, ctx: &mut AdviceContext,
stem: &str, stem: &str,
) -> Result<Option<String>> { ) -> 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)?; let v = image.get_incorporated_release(stem)?;
ctx.lock_cache.insert(stem.to_string(), v.clone()); ctx.lock_cache.insert(stem.to_string(), v.clone());
Ok(v) Ok(v)
@ -607,7 +820,9 @@ fn print_trees(graph: &Graph, roots: &[String], max_depth: usize) {
// Print a tree for each root // Print a tree for each root
let mut printed = HashSet::new(); let mut printed = HashSet::new();
for r in roots { for r in roots {
if printed.contains(r) { continue; } if printed.contains(r) {
continue;
}
printed.insert(r.clone()); printed.insert(r.clone());
println!("{}", r); println!("{}", r);
let mut path = Vec::new(); let mut path = Vec::new();
@ -625,7 +840,9 @@ fn print_tree_rec(
path: &mut Vec<String>, path: &mut Vec<String>,
_seen: &mut HashSet<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()); path.push(node.to_string());
if let Some(edges) = graph.adj.get(node) { if let Some(edges) = graph.adj.get(node) {
@ -675,7 +892,11 @@ fn dfs_cycles(
let mut cycle_edges = Vec::new(); let mut cycle_edges = Vec::new();
for i in pos..stack.len() { for i in pos..stack.len() {
let from = &stack[i]; 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(es2) = graph.adj.get(from) {
if let Some(edge) = es2.iter().find(|ed| &ed.to == to2) { if let Some(edge) = es2.iter().find(|ed| &ed.to == to2) {
cycle_edges.push(edge.dep_type.clone()); 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) { } else if !visited.contains(to) {
dfs_cycles(graph, to, visited, stack, cycles); 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) // rotate to minimal node position (excluding the duplicate last element when comparing)
if c.nodes.len() > 1 { 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) { if let Some((min_idx, _)) = inner.iter().enumerate().min_by_key(|(_, n)| *n) {
c.nodes.rotate_left(min_idx); c.nodes.rotate_left(min_idx);
c.edges.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(); let mut seen = HashSet::new();
cycles.retain(|c| { cycles.retain(|c| {
let key = c.nodes.join("->"); 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 cycles
} }
@ -730,7 +959,9 @@ fn print_cycles(cycles: &[Cycle]) {
} }
fn print_suggestions(cycles: &[Cycle], graph: &Graph) { fn print_suggestions(cycles: &[Cycle], graph: &Graph) {
if cycles.is_empty() { return; } if cycles.is_empty() {
return;
}
println!("\nSuggestions to break cycles (heuristic):"); println!("\nSuggestions to break cycles (heuristic):");
for (i, c) in cycles.iter().enumerate() { for (i, c) in cycles.iter().enumerate() {
// Prefer breaking an 'incorporate' edge if present, otherwise any edge // 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) { if let Some(es) = graph.adj.get(from) {
for e in es { for e in es {
if &e.to == to { if &e.to == to {
if e.dep_type == "incorporate" { suggested = Some((from.clone(), to.clone())); break 'outer; } if e.dep_type == "incorporate" {
if suggested.is_none() { suggested = Some((from.clone(), to.clone())); } suggested = Some((from.clone(), to.clone()));
break 'outer;
}
if suggested.is_none() {
suggested = Some((from.clone(), to.clone()));
}
} }
} }
} }
} }
if let Some((from, to)) = suggested { 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 { } 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 ---------- // ---------- Dangling dependency scan ----------
fn run_dangling_scan( fn run_dangling_scan(
image: &Image, image: &Image,
@ -786,9 +1030,9 @@ fn run_dangling_scan(
format: OutputFormat, format: OutputFormat,
) -> Result<()> { ) -> Result<()> {
// Query full catalog once // Query full catalog once
let mut pkgs = image let mut pkgs = image.query_catalog(None).map_err(|e| PkgTreeError {
.query_catalog(None) message: format!("Failed to query catalog: {}", e),
.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, // Build set of available non-obsolete stems AND an index of available (release, branch) pairs per stem,
// honoring publisher filter // honoring publisher filter
@ -796,9 +1040,13 @@ fn run_dangling_scan(
let mut available_index: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new(); let mut available_index: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
for p in &pkgs { for p in &pkgs {
if let Some(pubf) = publisher { 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(); let stem = p.fmri.stem().to_string();
available_stems.insert(stem.clone()); available_stems.insert(stem.clone());
let ver = p.fmri.version(); let ver = p.fmri.version();
@ -828,8 +1076,12 @@ fn run_dangling_scan(
Ok(Some(man)) => { Ok(Some(man)) => {
let mut missing_for_pkg: Vec<String> = Vec::new(); let mut missing_for_pkg: Vec<String> = Vec::new();
for dep in man.dependencies { for dep in man.dependencies {
if dep.dependency_type != "require" && dep.dependency_type != "incorporate" { continue; } if dep.dependency_type != "require" && dep.dependency_type != "incorporate" {
let Some(df) = dep.fmri else { continue; }; continue;
}
let Some(df) = dep.fmri else {
continue;
};
let stem = df.stem().to_string(); let stem = df.stem().to_string();
// Extract version/branch constraints if any (from optional properties) // 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 { let satisfies = |stem: &str, rel: Option<&str>, br: Option<&str>| -> bool {
if let Some(list) = available_index.get(stem) { if let Some(list) = available_index.get(stem) {
if let (Some(rreq), Some(breq)) = (rel, br) { 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 { } else if let Some(rreq) = rel {
return list.iter().any(|(r, _)| r == rreq); return list.iter().any(|(r, _)| r == rreq);
} else if let Some(breq) = br { } 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()) { if !satisfies(&stem, c.release.as_deref(), c.branch.as_deref()) {
// Include constraint context in output for maintainers // Include constraint context in output for maintainers
let mut ctx = String::new(); let mut ctx = String::new();
if let Some(r) = &c.release { ctx.push_str(&format!("release={} ", r)); } if let Some(r) = &c.release {
if let Some(b) = &c.branch { ctx.push_str(&format!("branch={}", b)); } ctx.push_str(&format!("release={} ", r));
}
if let Some(b) = &c.branch {
ctx.push_str(&format!("branch={}", b));
}
let ctx = ctx.trim().to_string(); 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() { if !missing_for_pkg.is_empty() {
missing_for_pkg.sort(); missing_for_pkg.sort();
@ -898,13 +1162,18 @@ fn run_dangling_scan(
if dangling.is_empty() { if dangling.is_empty() {
println!("No dangling dependencies detected."); println!("No dangling dependencies detected.");
} else { } 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(); let mut keys: Vec<String> = dangling.keys().cloned().collect();
keys.sort(); keys.sort();
for k in keys { for k in keys {
println!("- {}:", k); println!("- {}:", k);
if let Some(list) = dangling.get(&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 => { OutputFormat::Json => {
use serde::Serialize; use serde::Serialize;
#[derive(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(); let mut out: Vec<DanglingJson> = Vec::new();
for (pkg, miss) in dangling.into_iter() { 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)); out.sort_by(|a, b| a.package_fmri.cmp(&b.package_fmri));
println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?); println!("{}", serde_json::to_string_pretty(&out).into_diagnostic()?);
@ -927,8 +1202,9 @@ fn run_dangling_scan(
// ---------- Targeted analysis: parse pkg6 solver error text ---------- // ---------- Targeted analysis: parse pkg6 solver error text ----------
fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathBuf) -> Result<()> { fn analyze_solver_error(image: &Image, publisher: Option<&str>, err_path: &PathBuf) -> Result<()> {
let text = std::fs::read_to_string(err_path) let text = std::fs::read_to_string(err_path).map_err(|e| PkgTreeError {
.map_err(|e| PkgTreeError { message: format!("Failed to read solver error file {:?}: {}", err_path, e) })?; message: format!("Failed to read solver error file {:?}: {}", err_path, e),
})?;
// Build a stack based on indentation before the tree bullet "└─". // Build a stack based on indentation before the tree bullet "└─".
let mut stack: Vec<String> = Vec::new(); 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 "└─ " // Extract node text after "└─ "
let bullet = "└─ "; 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(); let mut node_full = line[start..].trim().to_string();
// Remove trailing diagnostic phrases for leaf line // Remove trailing diagnostic phrases for leaf line
if let Some(pos) = node_full.find("for which no candidates were found") { if let Some(pos) = node_full.find("for which no candidates were found") {
node_full = node_full[..pos].trim().trim_end_matches(',').to_string(); 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") { if line.contains("for which no candidates were found") {
failing_leaf = Some(node_full.clone()); 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() { 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(()); 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):"); println!("Found 1 installability issue (from solver error):");
let constraint_str = format!( let constraint_str = format!(
"{}{}", "{}{}",
constraint.release.as_ref().map(|r| format!("release={} ", r)).unwrap_or_default(), constraint
constraint.branch.as_ref().map(|b| format!("branch={}", b)).unwrap_or_default(), .release
).trim().to_string(); .as_ref()
println!(" 1. No viable candidates for '{}'\n - Path: {}\n - Constraint: {}\n - Details: {}", .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, stem,
path_stems.join(" -> "), 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, details,
); );
println!(" - Suggestions:"); println!(" - Suggestions:");
println!(" • Add or publish a matching package for '{}'{}{}.", println!(
" • Add or publish a matching package for '{}'{}{}.",
stem, stem,
constraint.release.as_ref().map(|r| format!(" (release={})", r)).unwrap_or_default(), constraint
constraint.branch.as_ref().map(|b| format!(" (branch={})", b)).unwrap_or_default()); .release
println!(" • Alternatively, relax the dependency constraint in the requiring package to match available releases."); .as_ref()
if let Some(lock) = get_incorporated_release_cached(image, &mut ctx, &stem).ok().flatten() { .map(|r| format!(" (release={})", r))
println!(" • Incorporation lock present for '{}': release={}. Consider updating the incorporation to allow the required release, or align the dependency.", stem, lock); .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'."); 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 // 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(""); let first = node.split_whitespace().next().unwrap_or("");
if first.starts_with("pkg://") { 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 it contains '@' (FMRI without scheme), parse via Fmri::parse
if first.contains('@') { 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 // Otherwise assume it's a stem token
first.trim_end_matches(',').to_string() first.trim_end_matches(',').to_string()
} }
fn parse_leaf_node(node: &str) -> (String, DepConstraint) { 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 release: Option<String> = None;
let mut branch: Option<String> = None; let mut branch: Option<String> = None;
// Find release= // Find release=
if let Some(p) = core.find("release=") { if let Some(p) = core.find("release=") {
let rest = &core[p + "release=".len()..]; 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()); release = Some(rest[..end].to_string());
} }
// Find branch= // Find branch=
if let Some(p) = core.find("branch=") { if let Some(p) = core.find("branch=") {
let rest = &core[p + "branch=".len()..]; 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()); branch = Some(rest[..end].to_string());
} }

View file

@ -2,10 +2,10 @@ mod sources;
#[allow(clippy::result_large_err)] #[allow(clippy::result_large_err)]
mod workspace; mod workspace;
use clap::ArgAction;
use crate::workspace::Workspace; use crate::workspace::Workspace;
use anyhow::anyhow;
use anyhow::Result; use anyhow::Result;
use anyhow::anyhow;
use clap::ArgAction;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use specfile::macros; use specfile::macros;
use specfile::parse; use specfile::parse;

View file

@ -3,10 +3,10 @@ use libips::actions::{ActionError, File as FileAction, Manifest};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
use std::env::{current_dir, set_current_dir}; 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::copy;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::Error as IOError;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::result::Result as StdResult; use std::result::Result as StdResult;

View file

@ -43,34 +43,38 @@ impl MacroParser {
for macro_pair in inner.clone().into_inner() { for macro_pair in inner.clone().into_inner() {
match macro_pair.as_rule() { match macro_pair.as_rule() {
Rule::macro_name => { Rule::macro_name => {
replaced_line += self.get_variable(macro_pair.as_str())?; replaced_line +=
}, self.get_variable(macro_pair.as_str())?;
}
Rule::macro_parameter => { Rule::macro_parameter => {
println!("macro parameter: {}", macro_pair.as_str()) println!(
}, "macro parameter: {}",
macro_pair.as_str()
)
}
_ => panic!( _ => panic!(
"Unexpected macro match please update the code together with the peg grammar: {:?}", "Unexpected macro match please update the code together with the peg grammar: {:?}",
macro_pair.as_rule() macro_pair.as_rule()
) ),
} }
} }
} }
_ => panic!( _ => panic!(
"Unexpected inner match please update the code together with the peg grammar: {:?}", "Unexpected inner match please update the code together with the peg grammar: {:?}",
inner.as_rule() inner.as_rule()
) ),
} }
} }
}, }
Rule::EOI => (), Rule::EOI => (),
Rule::text => { Rule::text => {
replaced_line += test_pair.as_str(); replaced_line += test_pair.as_str();
replaced_line += " "; replaced_line += " ";
}, }
_ => panic!( _ => panic!(
"Unexpected match please update the code together with the peg grammar: {:?}", "Unexpected match please update the code together with the peg grammar: {:?}",
test_pair.as_rule() test_pair.as_rule()
) ),
} }
} }
} }

View file

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

View file

@ -1,10 +1,10 @@
mod component; mod component;
pub mod repology; pub mod repology;
use anyhow::{anyhow, Context, Result}; use anyhow::{Context, Result, anyhow};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use pest::iterators::Pairs;
use pest::Parser; use pest::Parser;
use pest::iterators::Pairs;
use pest_derive::Parser; use pest_derive::Parser;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
@ -233,13 +233,16 @@ fn parse_makefile(pairs: Pairs<crate::Rule>, m: &mut Makefile) -> Result<()> {
Rule::comment_string => (), Rule::comment_string => (),
Rule::include => { Rule::include => {
parse_include(p.into_inner(), m)?; parse_include(p.into_inner(), m)?;
}, }
Rule::target => (), Rule::target => (),
Rule::define => { Rule::define => {
parse_define(p.into_inner(), m)?; parse_define(p.into_inner(), m)?;
} }
Rule::EOI => (), 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 => { Rule::variable_name => {
var.0 = p.as_str().to_string(); var.0 = p.as_str().to_string();
} }
Rule::variable_set => { Rule::variable_set => var.1.mode = VariableMode::Set,
var.1.mode = VariableMode::Set Rule::variable_add => var.1.mode = VariableMode::Add,
}, Rule::variable_value => match var.1.mode {
Rule::variable_add => { VariableMode::Add => {
var.1.mode = VariableMode::Add if m.variables.contains_key(&var.0) {
} var.1 = m.variables.get(&var.0).unwrap().clone()
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());
} }
var.1.values.push(p.as_str().to_string());
} }
} VariableMode::Set => {
_ => panic!("unexpected rule {:?} inside makefile rule expected variable_name, variable_set, variable_add, variable_value", p.as_rule()), 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); m.variables.insert(var.0, var.1);