2025-08-04 22:01:38 +02:00
|
|
|
use crate::actions::{Manifest};
|
|
|
|
|
use crate::fmri::Fmri;
|
|
|
|
|
use crate::repository::catalog::{CatalogManager, CatalogPart, PackageVersionEntry};
|
|
|
|
|
use miette::Diagnostic;
|
|
|
|
|
use redb::{Database, ReadableTable, TableDefinition};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::fs;
|
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
|
use thiserror::Error;
|
|
|
|
|
use tracing::{info, warn};
|
|
|
|
|
|
|
|
|
|
/// Table definition for the catalog database
|
|
|
|
|
/// Key: stem@version
|
|
|
|
|
/// Value: serialized Manifest
|
|
|
|
|
pub const CATALOG_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("catalog");
|
|
|
|
|
|
|
|
|
|
/// Table definition for the obsoleted packages catalog
|
|
|
|
|
/// Key: full FMRI including publisher (pkg://publisher/stem@version)
|
|
|
|
|
/// Value: nothing
|
|
|
|
|
pub const OBSOLETED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("obsoleted");
|
|
|
|
|
|
|
|
|
|
/// Table definition for the installed packages database
|
|
|
|
|
/// Key: full FMRI including publisher (pkg://publisher/stem@version)
|
|
|
|
|
/// Value: serialized Manifest
|
|
|
|
|
pub const INSTALLED_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("installed");
|
|
|
|
|
|
|
|
|
|
/// Errors that can occur when working with the image catalog
|
|
|
|
|
#[derive(Error, Debug, Diagnostic)]
|
|
|
|
|
pub enum CatalogError {
|
|
|
|
|
#[error("IO error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::io))]
|
|
|
|
|
IO(#[from] std::io::Error),
|
|
|
|
|
|
|
|
|
|
#[error("JSON error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::json))]
|
|
|
|
|
Json(#[from] serde_json::Error),
|
|
|
|
|
|
|
|
|
|
#[error("Database error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::database))]
|
|
|
|
|
Database(String),
|
|
|
|
|
|
|
|
|
|
#[error("Repository error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::repository))]
|
|
|
|
|
Repository(#[from] crate::repository::RepositoryError),
|
|
|
|
|
|
|
|
|
|
#[error("Action error: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::action))]
|
|
|
|
|
Action(#[from] crate::actions::ActionError),
|
|
|
|
|
|
|
|
|
|
#[error("Publisher not found: {0}")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::publisher_not_found))]
|
|
|
|
|
PublisherNotFound(String),
|
|
|
|
|
|
|
|
|
|
#[error("No publishers configured")]
|
|
|
|
|
#[diagnostic(code(ips::catalog_error::no_publishers))]
|
|
|
|
|
NoPublishers,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Result type for catalog operations
|
|
|
|
|
pub type Result<T> = std::result::Result<T, CatalogError>;
|
|
|
|
|
|
|
|
|
|
/// Information about a package in the catalog
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct PackageInfo {
|
|
|
|
|
/// The FMRI of the package
|
|
|
|
|
pub fmri: Fmri,
|
|
|
|
|
|
|
|
|
|
/// Whether the package is obsolete
|
|
|
|
|
pub obsolete: bool,
|
|
|
|
|
|
|
|
|
|
/// The publisher of the package
|
|
|
|
|
pub publisher: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The image catalog, which merges catalogs from all publishers
|
|
|
|
|
pub struct ImageCatalog {
|
|
|
|
|
/// Path to the catalog database
|
|
|
|
|
db_path: PathBuf,
|
|
|
|
|
|
|
|
|
|
/// Path to the catalog directory
|
|
|
|
|
catalog_dir: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ImageCatalog {
|
|
|
|
|
/// Create a new image catalog
|
|
|
|
|
pub fn new<P: AsRef<Path>>(catalog_dir: P, db_path: P) -> Self {
|
|
|
|
|
ImageCatalog {
|
|
|
|
|
db_path: db_path.as_ref().to_path_buf(),
|
|
|
|
|
catalog_dir: catalog_dir.as_ref().to_path_buf(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Initialize the catalog database
|
|
|
|
|
pub fn init_db(&self) -> Result<()> {
|
|
|
|
|
// Create a parent directory if it doesn't exist
|
|
|
|
|
if let Some(parent) = self.db_path.parent() {
|
|
|
|
|
fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open or create the database
|
|
|
|
|
let db = Database::create(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create database: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Create tables
|
|
|
|
|
let tx = db.begin_write()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
tx.open_table(CATALOG_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create catalog table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
tx.open_table(OBSOLETED_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to create obsoleted table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
tx.commit()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to commit transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build the catalog from downloaded catalogs
|
|
|
|
|
pub fn build_catalog(&self, publishers: &[String]) -> Result<()> {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Building catalog with publishers: {:?}", publishers);
|
|
|
|
|
println!("Catalog directory: {:?}", self.catalog_dir);
|
|
|
|
|
println!("Catalog database path: {:?}", self.db_path);
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
if publishers.is_empty() {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("No publishers provided");
|
2025-08-04 22:01:38 +02:00
|
|
|
return Err(CatalogError::NoPublishers);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open the database
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Opening database at {:?}", self.db_path);
|
2025-08-04 22:01:38 +02:00
|
|
|
let db = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Begin a writing transaction
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Beginning write transaction");
|
2025-08-04 22:01:38 +02:00
|
|
|
let tx = db.begin_write()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the catalog table
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Opening catalog table");
|
2025-08-04 22:01:38 +02:00
|
|
|
let mut catalog_table = tx.open_table(CATALOG_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the obsoleted table
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Opening obsoleted table");
|
2025-08-04 22:01:38 +02:00
|
|
|
let mut obsoleted_table = tx.open_table(OBSOLETED_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Process each publisher
|
|
|
|
|
for publisher in publishers {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Processing publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
let publisher_catalog_dir = self.catalog_dir.join(publisher);
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Publisher catalog directory: {:?}", publisher_catalog_dir);
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Skip if the publisher catalog directory doesn't exist
|
|
|
|
|
if !publisher_catalog_dir.exists() {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Publisher catalog directory not found: {}", publisher_catalog_dir.display());
|
2025-08-04 22:01:38 +02:00
|
|
|
warn!("Publisher catalog directory not found: {}", publisher_catalog_dir.display());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a catalog manager for this publisher
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Creating catalog manager for publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
let mut catalog_manager = CatalogManager::new(&publisher_catalog_dir, publisher)
|
|
|
|
|
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to create catalog manager: {}", e))))?;
|
|
|
|
|
|
|
|
|
|
// Get all catalog parts
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Getting catalog parts for publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
let parts = catalog_manager.attrs().parts.clone();
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Catalog parts: {:?}", parts.keys().collect::<Vec<_>>());
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Load all catalog parts
|
|
|
|
|
for part_name in parts.keys() {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Loading catalog part: {}", part_name);
|
2025-08-04 22:01:38 +02:00
|
|
|
catalog_manager.load_part(part_name)
|
|
|
|
|
.map_err(|e| CatalogError::Repository(crate::repository::RepositoryError::Other(format!("Failed to load catalog part: {}", e))))?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process each catalog part
|
|
|
|
|
for (part_name, _) in parts {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Processing catalog part: {}", part_name);
|
2025-08-04 22:01:38 +02:00
|
|
|
if let Some(part) = catalog_manager.get_part(&part_name) {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Found catalog part: {}", part_name);
|
|
|
|
|
println!("Packages in part: {:?}", part.packages.keys().collect::<Vec<_>>());
|
|
|
|
|
if let Some(publisher_packages) = part.packages.get(publisher) {
|
|
|
|
|
println!("Packages for publisher {}: {:?}", publisher, publisher_packages.keys().collect::<Vec<_>>());
|
|
|
|
|
} else {
|
|
|
|
|
println!("No packages found for publisher: {}", publisher);
|
|
|
|
|
}
|
2025-08-04 22:01:38 +02:00
|
|
|
self.process_catalog_part(&mut catalog_table, &mut obsoleted_table, part, publisher)?;
|
2025-08-04 23:01:04 +02:00
|
|
|
} else {
|
|
|
|
|
println!("Catalog part not found: {}", part_name);
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drop the tables to release the borrow on tx
|
|
|
|
|
drop(catalog_table);
|
|
|
|
|
drop(obsoleted_table);
|
|
|
|
|
|
|
|
|
|
// Commit the transaction
|
|
|
|
|
tx.commit()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to commit transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
info!("Catalog built successfully");
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Process a catalog part and add its packages to the catalog
|
|
|
|
|
fn process_catalog_part(
|
|
|
|
|
&self,
|
|
|
|
|
catalog_table: &mut redb::Table<&str, &[u8]>,
|
|
|
|
|
obsoleted_table: &mut redb::Table<&str, &[u8]>,
|
|
|
|
|
part: &CatalogPart,
|
|
|
|
|
publisher: &str,
|
|
|
|
|
) -> Result<()> {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Processing catalog part for publisher: {}", publisher);
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Get packages for this publisher
|
|
|
|
|
if let Some(publisher_packages) = part.packages.get(publisher) {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Found {} package stems for publisher {}", publisher_packages.len(), publisher);
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Process each package stem
|
|
|
|
|
for (stem, versions) in publisher_packages {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Processing package stem: {}", stem);
|
|
|
|
|
println!("Found {} versions for stem {}", versions.len(), stem);
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Process each package version
|
|
|
|
|
for version_entry in versions {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Processing version: {}", version_entry.version);
|
|
|
|
|
println!("Actions: {:?}", version_entry.actions);
|
|
|
|
|
|
2025-08-04 22:01:38 +02:00
|
|
|
// Create the FMRI
|
|
|
|
|
let version = if !version_entry.version.is_empty() {
|
|
|
|
|
match crate::fmri::Version::parse(&version_entry.version) {
|
2025-08-04 23:01:04 +02:00
|
|
|
Ok(v) => {
|
|
|
|
|
println!("Parsed version: {:?}", v);
|
|
|
|
|
Some(v)
|
|
|
|
|
},
|
2025-08-04 22:01:38 +02:00
|
|
|
Err(e) => {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Failed to parse version '{}': {}", version_entry.version, e);
|
2025-08-04 22:01:38 +02:00
|
|
|
warn!("Failed to parse version '{}': {}", version_entry.version, e);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Empty version string");
|
2025-08-04 22:01:38 +02:00
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let fmri = Fmri::with_publisher(publisher, stem, version);
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Created FMRI: {}", fmri);
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Create the key for the catalog table (stem@version)
|
|
|
|
|
let catalog_key = format!("{}@{}", stem, version_entry.version);
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Catalog key: {}", catalog_key);
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Create the key for the obsoleted table (full FMRI including publisher)
|
|
|
|
|
let obsoleted_key = fmri.to_string();
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Obsoleted key: {}", obsoleted_key);
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Check if we already have this package in the catalog
|
|
|
|
|
let existing_manifest = if let Ok(bytes) = catalog_table.get(catalog_key.as_str()) {
|
|
|
|
|
if let Some(bytes) = bytes {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Found existing manifest for {}", catalog_key);
|
2025-08-04 22:01:38 +02:00
|
|
|
Some(serde_json::from_slice::<Manifest>(bytes.value())?)
|
|
|
|
|
} else {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("No existing manifest found for {}", catalog_key);
|
2025-08-04 22:01:38 +02:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Error getting manifest for {}", catalog_key);
|
2025-08-04 22:01:38 +02:00
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create or update the manifest
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Creating or updating manifest");
|
2025-08-04 22:01:38 +02:00
|
|
|
let manifest = self.create_or_update_manifest(existing_manifest, version_entry, stem, publisher)?;
|
|
|
|
|
|
|
|
|
|
// Check if the package is obsolete
|
|
|
|
|
let is_obsolete = self.is_package_obsolete(&manifest);
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Package is obsolete: {}", is_obsolete);
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Serialize the manifest
|
|
|
|
|
let manifest_bytes = serde_json::to_vec(&manifest)?;
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Serialized manifest size: {} bytes", manifest_bytes.len());
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Store the package in the appropriate table
|
|
|
|
|
if is_obsolete {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Storing obsolete package in obsoleted table");
|
2025-08-04 22:01:38 +02:00
|
|
|
// Store obsolete packages in the obsoleted table with the full FMRI as key
|
|
|
|
|
// We don't store any meaningful values in the obsoleted table as per requirements,
|
|
|
|
|
// but we need to provide a valid byte slice
|
|
|
|
|
let empty_bytes: &[u8] = &[0u8; 0];
|
2025-08-04 23:01:04 +02:00
|
|
|
match obsoleted_table.insert(obsoleted_key.as_str(), empty_bytes) {
|
|
|
|
|
Ok(_) => println!("Successfully inserted into obsoleted table"),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("Failed to insert into obsoleted table: {}", e);
|
|
|
|
|
return Err(CatalogError::Database(format!("Failed to insert into obsoleted table: {}", e)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 22:01:38 +02:00
|
|
|
} else {
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Storing non-obsolete package in catalog table");
|
2025-08-04 22:01:38 +02:00
|
|
|
// Store non-obsolete packages in the catalog table with stem@version as a key
|
2025-08-04 23:01:04 +02:00
|
|
|
match catalog_table.insert(catalog_key.as_str(), manifest_bytes.as_slice()) {
|
|
|
|
|
Ok(_) => println!("Successfully inserted into catalog table"),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("Failed to insert into catalog table: {}", e);
|
|
|
|
|
return Err(CatalogError::Database(format!("Failed to insert into catalog table: {}", e)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 23:01:04 +02:00
|
|
|
} else {
|
|
|
|
|
println!("No packages found for publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-04 23:01:04 +02:00
|
|
|
println!("Finished processing catalog part for publisher: {}", publisher);
|
2025-08-04 22:01:38 +02:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create or update a manifest from a package version entry
|
|
|
|
|
fn create_or_update_manifest(
|
|
|
|
|
&self,
|
|
|
|
|
existing_manifest: Option<Manifest>,
|
|
|
|
|
version_entry: &PackageVersionEntry,
|
|
|
|
|
stem: &str,
|
|
|
|
|
publisher: &str,
|
|
|
|
|
) -> Result<Manifest> {
|
|
|
|
|
// Start with the existing manifest or create a new one
|
|
|
|
|
let mut manifest = existing_manifest.unwrap_or_else(Manifest::new);
|
|
|
|
|
|
2025-08-04 23:45:41 +02:00
|
|
|
// Parse and add actions from the version entry
|
|
|
|
|
if let Some(actions) = &version_entry.actions {
|
|
|
|
|
for action_str in actions {
|
|
|
|
|
// Parse each action string to extract attributes
|
|
|
|
|
if action_str.starts_with("set ") {
|
|
|
|
|
// Format is typically "set name=pkg.key value=value"
|
|
|
|
|
if let Some(name_part) = action_str.split_whitespace().nth(1) {
|
|
|
|
|
if name_part.starts_with("name=") {
|
|
|
|
|
// Extract the key (after "name=")
|
|
|
|
|
let key = &name_part[5..];
|
|
|
|
|
|
|
|
|
|
// Extract the value (after "value=")
|
|
|
|
|
if let Some(value_part) = action_str.split_whitespace().nth(2) {
|
|
|
|
|
if value_part.starts_with("value=") {
|
|
|
|
|
let mut value = &value_part[6..];
|
|
|
|
|
|
|
|
|
|
// Remove quotes if present
|
|
|
|
|
if value.starts_with('"') && value.ends_with('"') {
|
|
|
|
|
value = &value[1..value.len()-1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add or update the attribute in the manifest
|
|
|
|
|
let attr_index = manifest.attributes.iter().position(|attr| attr.key == key);
|
|
|
|
|
if let Some(index) = attr_index {
|
|
|
|
|
manifest.attributes[index].values = vec![value.to_string()];
|
|
|
|
|
} else {
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = key.to_string();
|
|
|
|
|
attr.values = vec![value.to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-04 22:01:38 +02:00
|
|
|
|
|
|
|
|
// Ensure the manifest has the correct FMRI attribute
|
|
|
|
|
// Create a Version object from the version string
|
|
|
|
|
let version = if !version_entry.version.is_empty() {
|
|
|
|
|
match crate::fmri::Version::parse(&version_entry.version) {
|
|
|
|
|
Ok(v) => Some(v),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
// Map the FmriError to a CatalogError
|
|
|
|
|
return Err(CatalogError::Repository(
|
|
|
|
|
crate::repository::RepositoryError::Other(
|
|
|
|
|
format!("Invalid version format: {}", e)
|
|
|
|
|
)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create the FMRI with publisher, stem, and version
|
|
|
|
|
let fmri = Fmri::with_publisher(publisher, stem, version);
|
|
|
|
|
self.ensure_fmri_attribute(&mut manifest, &fmri);
|
|
|
|
|
|
|
|
|
|
Ok(manifest)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Ensure the manifest has the correct FMRI attribute
|
|
|
|
|
fn ensure_fmri_attribute(&self, manifest: &mut Manifest, fmri: &Fmri) {
|
|
|
|
|
// Check if the manifest already has an FMRI attribute
|
|
|
|
|
let has_fmri = manifest.attributes.iter().any(|attr| attr.key == "pkg.fmri");
|
|
|
|
|
|
|
|
|
|
// If not, add it
|
|
|
|
|
if !has_fmri {
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = "pkg.fmri".to_string();
|
|
|
|
|
attr.values = vec![fmri.to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Check if a package is obsolete
|
|
|
|
|
fn is_package_obsolete(&self, manifest: &Manifest) -> bool {
|
|
|
|
|
// Check for the pkg.obsolete attribute
|
|
|
|
|
manifest.attributes.iter().any(|attr| {
|
|
|
|
|
attr.key == "pkg.obsolete" && attr.values.get(0).map_or(false, |v| v == "true")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Query the catalog for packages matching a pattern
|
|
|
|
|
pub fn query_packages(&self, pattern: Option<&str>) -> Result<Vec<PackageInfo>> {
|
|
|
|
|
// Open the database
|
|
|
|
|
let db = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Begin a read transaction
|
|
|
|
|
let tx = db.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the catalog table
|
|
|
|
|
let catalog_table = tx.open_table(CATALOG_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the obsoleted table
|
|
|
|
|
let obsoleted_table = tx.open_table(OBSOLETED_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
let mut results = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Process the catalog table (non-obsolete packages)
|
|
|
|
|
// Iterate through all entries in the table
|
|
|
|
|
for entry_result in catalog_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate catalog table: {}", e)))? {
|
|
|
|
|
let (key, value) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from catalog table: {}", e)))?;
|
|
|
|
|
let key_str = key.value();
|
|
|
|
|
|
|
|
|
|
// Skip if the key doesn't match the pattern
|
|
|
|
|
if let Some(pattern) = pattern {
|
|
|
|
|
if !key_str.contains(pattern) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the key to get stem and version
|
|
|
|
|
let parts: Vec<&str> = key_str.split('@').collect();
|
|
|
|
|
if parts.len() != 2 {
|
|
|
|
|
warn!("Invalid key format: {}", key_str);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let stem = parts[0];
|
|
|
|
|
let version = parts[1];
|
|
|
|
|
|
|
|
|
|
// Deserialize the manifest
|
|
|
|
|
let manifest: Manifest = serde_json::from_slice(value.value())?;
|
|
|
|
|
|
|
|
|
|
// Extract the publisher from the FMRI attribute
|
|
|
|
|
let publisher = manifest.attributes.iter()
|
|
|
|
|
.find(|attr| attr.key == "pkg.fmri")
|
|
|
|
|
.map(|attr| {
|
|
|
|
|
if let Some(fmri_str) = attr.values.get(0) {
|
|
|
|
|
// Parse the FMRI string
|
|
|
|
|
match Fmri::parse(fmri_str) {
|
|
|
|
|
Ok(fmri) => fmri.publisher.unwrap_or_else(|| "unknown".to_string()),
|
|
|
|
|
Err(_) => "unknown".to_string(),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
"unknown".to_string()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
|
|
|
|
|
|
// Create a Version object from the version string
|
|
|
|
|
let version_obj = if !version.is_empty() {
|
|
|
|
|
match crate::fmri::Version::parse(version) {
|
|
|
|
|
Ok(v) => Some(v),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create the FMRI with publisher, stem, and version
|
|
|
|
|
let fmri = Fmri::with_publisher(&publisher, stem, version_obj);
|
|
|
|
|
|
|
|
|
|
// Add to results (non-obsolete)
|
|
|
|
|
results.push(PackageInfo {
|
|
|
|
|
fmri,
|
|
|
|
|
obsolete: false,
|
|
|
|
|
publisher,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process the obsoleted table (obsolete packages)
|
|
|
|
|
// Iterate through all entries in the table
|
|
|
|
|
for entry_result in obsoleted_table.iter().map_err(|e| CatalogError::Database(format!("Failed to iterate obsoleted table: {}", e)))? {
|
|
|
|
|
let (key, _) = entry_result.map_err(|e| CatalogError::Database(format!("Failed to get entry from obsoleted table: {}", e)))?;
|
|
|
|
|
let key_str = key.value();
|
|
|
|
|
|
|
|
|
|
// Skip if the key doesn't match the pattern
|
|
|
|
|
if let Some(pattern) = pattern {
|
|
|
|
|
if !key_str.contains(pattern) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse the key to get the FMRI
|
|
|
|
|
match Fmri::parse(key_str) {
|
|
|
|
|
Ok(fmri) => {
|
|
|
|
|
// Extract the publisher
|
|
|
|
|
let publisher = fmri.publisher.clone().unwrap_or_else(|| "unknown".to_string());
|
|
|
|
|
|
|
|
|
|
// Add to results (obsolete)
|
|
|
|
|
results.push(PackageInfo {
|
|
|
|
|
fmri,
|
|
|
|
|
obsolete: true,
|
|
|
|
|
publisher,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
warn!("Failed to parse FMRI from obsoleted table key: {}: {}", key_str, e);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(results)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get a manifest from the catalog
|
|
|
|
|
pub fn get_manifest(&self, fmri: &Fmri) -> Result<Option<Manifest>> {
|
|
|
|
|
// Open the database
|
|
|
|
|
let db = Database::open(&self.db_path)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open database: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Begin a read transaction
|
|
|
|
|
let tx = db.begin_read()
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to begin transaction: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the catalog table
|
|
|
|
|
let catalog_table = tx.open_table(CATALOG_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open catalog table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Open the obsoleted table
|
|
|
|
|
let obsoleted_table = tx.open_table(OBSOLETED_TABLE)
|
|
|
|
|
.map_err(|e| CatalogError::Database(format!("Failed to open obsoleted table: {}", e)))?;
|
|
|
|
|
|
|
|
|
|
// Create the key for the catalog table (stem@version)
|
|
|
|
|
let catalog_key = format!("{}@{}", fmri.stem(), fmri.version());
|
|
|
|
|
|
|
|
|
|
// Create the key for the obsoleted table (full FMRI including publisher)
|
|
|
|
|
let obsoleted_key = fmri.to_string();
|
|
|
|
|
|
|
|
|
|
// Try to get the manifest from the catalog table
|
|
|
|
|
if let Ok(Some(bytes)) = catalog_table.get(catalog_key.as_str()) {
|
|
|
|
|
return Ok(Some(serde_json::from_slice(bytes.value())?));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the package is in the obsoleted table
|
|
|
|
|
if let Ok(Some(_)) = obsoleted_table.get(obsoleted_key.as_str()) {
|
|
|
|
|
// The package is obsolete, but we don't store the manifest in the obsoleted table
|
|
|
|
|
// We could return a minimal manifest with just the FMRI and obsolete flag
|
|
|
|
|
let mut manifest = Manifest::new();
|
|
|
|
|
|
|
|
|
|
// Add the FMRI attribute
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = "pkg.fmri".to_string();
|
|
|
|
|
attr.values = vec![fmri.to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
|
|
|
|
|
// Add the obsolete attribute
|
|
|
|
|
let mut attr = crate::actions::Attr::default();
|
|
|
|
|
attr.key = "pkg.obsolete".to_string();
|
|
|
|
|
attr.values = vec!["true".to_string()];
|
|
|
|
|
manifest.attributes.push(attr);
|
|
|
|
|
|
|
|
|
|
return Ok(Some(manifest));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manifest not found
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|