Introduce Catalog module with structs and functionality for managing package metadata, enhance file_backend and CLI to handle catalog operations, and update dependencies (chrono and others) to support new features.

This commit is contained in:
Till Wegmueller 2025-07-24 00:28:33 +02:00
parent 63f2d1da62
commit a0fcc13033
No known key found for this signature in database
8 changed files with 862 additions and 45 deletions

156
Cargo.lock generated
View file

@ -32,6 +32,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.19"
@ -246,6 +261,20 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "3.2.23"
@ -343,9 +372,9 @@ dependencies = [
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
@ -890,6 +919,30 @@ dependencies = [
"serde",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -955,10 +1008,11 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.61"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
@ -994,6 +1048,7 @@ name = "libips"
version = "0.1.2"
dependencies = [
"anyhow",
"chrono",
"diff-struct",
"flate2",
"lz4",
@ -1483,7 +1538,7 @@ dependencies = [
"reqwest",
"shellexpand",
"specfile",
"thiserror 1.0.40",
"thiserror 2.0.12",
"url",
"which",
]
@ -2002,7 +2057,7 @@ dependencies = [
"anyhow",
"pest",
"pest_derive",
"thiserror 1.0.40",
"thiserror 2.0.12",
]
[[package]]
@ -2547,26 +2602,27 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.84"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.84"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.104",
"wasm-bindgen-shared",
]
@ -2584,9 +2640,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.84"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -2594,22 +2650,25 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.84"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.104",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.84"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasmparser"
@ -2688,6 +2747,65 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.42.0"

View file

@ -34,3 +34,4 @@ semver = { version = "1.0.20", features = ["serde"] }
diff-struct = "0.5.3"
searchy = "0.5.0"
tantivy = { version = "0.24.2", features = ["mmap"] }
chrono = "0.4.41"

View file

@ -519,6 +519,19 @@ impl Fmri {
version,
}
}
/// Get the stem of the FMRI (the package name without version)
pub fn stem(&self) -> &str {
&self.name
}
/// Get the version of the FMRI as a string
pub fn version(&self) -> String {
match &self.version {
Some(v) => v.to_string(),
None => String::new(),
}
}
/// Parse an FMRI string into an Fmri
///

View file

@ -0,0 +1,457 @@
// This Source Code Form is subject to the terms of
// the Mozilla Public License, v. 2.0. If a copy of the
// MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/.
use anyhow::Result;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use crate::fmri::Fmri;
/// Format a SystemTime as an ISO-8601 'basic format' date in UTC
fn format_iso8601_basic(time: &SystemTime) -> String {
let datetime = convert_system_time_to_datetime(time);
format!("{}Z", datetime.format("%Y%m%dT%H%M%S.%f"))
}
/// Convert SystemTime to UTC DateTime, handling errors gracefully
fn convert_system_time_to_datetime(time: &SystemTime) -> chrono::DateTime<chrono::Utc> {
let duration = time
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0));
let secs = duration.as_secs() as i64;
let nanos = duration.subsec_nanos();
chrono::DateTime::from_timestamp(secs, nanos)
.unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
chrono::NaiveDateTime::default(),
chrono::Utc,
))
}
/// Catalog version
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CatalogVersion {
V1 = 1,
}
impl Default for CatalogVersion {
fn default() -> Self {
CatalogVersion::V1
}
}
/// Catalog part information
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogPartInfo {
/// Last modified timestamp in ISO-8601 'basic format' date in UTC
#[serde(rename = "last-modified")]
pub last_modified: String,
/// Optional SHA-1 signature of the catalog part
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
pub signature_sha1: Option<String>,
}
/// Update log information
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateLogInfo {
/// Last modified timestamp in ISO-8601 'basic format' date in UTC
#[serde(rename = "last-modified")]
pub last_modified: String,
/// Optional SHA-1 signature of the update log
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
pub signature_sha1: Option<String>,
}
/// Catalog attributes
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogAttrs {
/// Optional signature information
#[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")]
pub signature: Option<HashMap<String, String>>,
/// Creation timestamp in ISO-8601 'basic format' date in UTC
pub created: String,
/// Last modified timestamp in ISO-8601 'basic format' date in UTC
#[serde(rename = "last-modified")]
pub last_modified: String,
/// Number of unique package stems in the catalog
#[serde(rename = "package-count")]
pub package_count: usize,
/// Number of unique package versions in the catalog
#[serde(rename = "package-version-count")]
pub package_version_count: usize,
/// Available catalog parts
pub parts: HashMap<String, CatalogPartInfo>,
/// Available update logs
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub updates: HashMap<String, UpdateLogInfo>,
/// Catalog version
pub version: u32,
}
impl CatalogAttrs {
/// Create a new catalog attributes structure
pub fn new() -> Self {
let now = SystemTime::now();
let timestamp = format_iso8601_basic(&now);
CatalogAttrs {
signature: None,
created: timestamp.clone(),
last_modified: timestamp,
package_count: 0,
package_version_count: 0,
parts: HashMap::new(),
updates: HashMap::new(),
version: CatalogVersion::V1 as u32,
}
}
/// Save catalog attributes to a file
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
/// Load catalog attributes from a file
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let json = fs::read_to_string(path)?;
let attrs: CatalogAttrs = serde_json::from_str(&json)?;
Ok(attrs)
}
}
/// Package version entry in a catalog
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageVersionEntry {
/// Package version string
pub version: String,
/// Optional actions associated with this package version
#[serde(skip_serializing_if = "Option::is_none")]
pub actions: Option<Vec<String>>,
/// Optional SHA-1 signature of the package manifest
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
pub signature_sha1: Option<String>,
}
/// Catalog part (base, dependency, summary)
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CatalogPart {
/// Optional signature information
#[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")]
pub signature: Option<HashMap<String, String>>,
/// Packages by publisher and stem
pub packages: HashMap<String, HashMap<String, Vec<PackageVersionEntry>>>,
}
impl CatalogPart {
/// Create a new catalog part
pub fn new() -> Self {
CatalogPart {
signature: None,
packages: HashMap::new(),
}
}
/// Add a package to the catalog part
pub fn add_package(&mut self, publisher: &str, fmri: &Fmri, actions: Option<Vec<String>>, signature: Option<String>) {
let publisher_packages = self.packages.entry(publisher.to_string()).or_insert_with(HashMap::new);
let stem_versions = publisher_packages.entry(fmri.stem().to_string()).or_insert_with(Vec::new);
// Check if this version already exists
for entry in stem_versions.iter_mut() {
if !fmri.version().is_empty() && entry.version == fmri.version() {
// Update existing entry
if let Some(acts) = actions {
entry.actions = Some(acts);
}
if let Some(sig) = signature {
entry.signature_sha1 = Some(sig);
}
return;
}
}
// Add a new entry
stem_versions.push(PackageVersionEntry {
version: fmri.version(),
actions,
signature_sha1: signature,
});
// Sort versions (should be in ascending order)
stem_versions.sort_by(|a, b| a.version.cmp(&b.version));
}
/// Save a catalog part to a file
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
/// Load catalog part from a file
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let json = fs::read_to_string(path)?;
let part: CatalogPart = serde_json::from_str(&json)?;
Ok(part)
}
}
/// Operation type for catalog updates
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CatalogOperationType {
#[serde(rename = "add")]
Add,
#[serde(rename = "remove")]
Remove,
}
/// Package update entry in an update log
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageUpdateEntry {
/// Type of operation (add or remove)
#[serde(rename = "op-type")]
pub op_type: CatalogOperationType,
/// Timestamp of the operation in ISO-8601 'basic format' date in UTC
#[serde(rename = "op-time")]
pub op_time: String,
/// Package version string
pub version: String,
/// Catalog part entries
#[serde(flatten)]
pub catalog_parts: HashMap<String, HashMap<String, Vec<String>>>,
/// Optional SHA-1 signature of the package manifest
#[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")]
pub signature_sha1: Option<String>,
}
/// Update log
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateLog {
/// Optional signature information
#[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")]
pub signature: Option<HashMap<String, String>>,
/// Updates by publisher and stem
pub updates: HashMap<String, HashMap<String, Vec<PackageUpdateEntry>>>,
}
impl UpdateLog {
/// Create a new update log
pub fn new() -> Self {
UpdateLog {
signature: None,
updates: HashMap::new(),
}
}
/// Add a package update to the log
pub fn add_update(
&mut self,
publisher: &str,
fmri: &Fmri,
op_type: CatalogOperationType,
catalog_parts: HashMap<String, HashMap<String, Vec<String>>>,
signature: Option<String>,
) {
let publisher_updates = self.updates.entry(publisher.to_string()).or_insert_with(HashMap::new);
let stem_updates = publisher_updates.entry(fmri.stem().to_string()).or_insert_with(Vec::new);
let now = SystemTime::now();
let timestamp = format_iso8601_basic(&now);
stem_updates.push(PackageUpdateEntry {
op_type,
op_time: timestamp,
version: fmri.version(),
catalog_parts,
signature_sha1: signature,
});
}
/// Save update log to a file
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
/// Load update log from a file
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let json = fs::read_to_string(path)?;
let log: UpdateLog = serde_json::from_str(&json)?;
Ok(log)
}
}
/// Catalog manager
pub struct CatalogManager {
/// Path to the catalog directory
catalog_dir: PathBuf,
/// Catalog attributes
attrs: CatalogAttrs,
/// Catalog parts
parts: HashMap<String, CatalogPart>,
/// Update logs
update_logs: HashMap<String, UpdateLog>,
}
impl CatalogManager {
/// Create a new catalog manager
pub fn new<P: AsRef<Path>>(catalog_dir: P) -> Result<Self> {
let catalog_dir = catalog_dir.as_ref().to_path_buf();
// Create catalog directory if it doesn't exist
if !catalog_dir.exists() {
fs::create_dir_all(&catalog_dir)?;
}
// Try to load existing catalog attributes
let attrs_path = catalog_dir.join("catalog.attrs");
let attrs = if attrs_path.exists() {
CatalogAttrs::load(&attrs_path)?
} else {
CatalogAttrs::new()
};
Ok(CatalogManager {
catalog_dir,
attrs,
parts: HashMap::new(),
update_logs: HashMap::new(),
})
}
/// Get catalog attributes
pub fn attrs(&self) -> &CatalogAttrs {
&self.attrs
}
/// Get mutable catalog attributes
pub fn attrs_mut(&mut self) -> &mut CatalogAttrs {
&mut self.attrs
}
/// Get a catalog part
pub fn get_part(&self, name: &str) -> Option<&CatalogPart> {
self.parts.get(name)
}
/// Get a mutable catalog part
pub fn get_part_mut(&mut self, name: &str) -> Option<&mut CatalogPart> {
self.parts.get_mut(name)
}
/// Load a catalog part
pub fn load_part(&mut self, name: &str) -> Result<()> {
let part_path = self.catalog_dir.join(name);
if part_path.exists() {
let part = CatalogPart::load(&part_path)?;
self.parts.insert(name.to_string(), part);
Ok(())
} else {
Err(anyhow::anyhow!("Catalog part does not exist: {}", name))
}
}
/// Save a catalog part
pub fn save_part(&self, name: &str) -> Result<()> {
if let Some(part) = self.parts.get(name) {
let part_path = self.catalog_dir.join(name);
part.save(&part_path)?;
Ok(())
} else {
Err(anyhow::anyhow!("Catalog part not loaded: {}", name))
}
}
/// Create a new catalog part
pub fn create_part(&mut self, name: &str) -> &mut CatalogPart {
self.parts.entry(name.to_string()).or_insert_with(CatalogPart::new)
}
/// Save catalog attributes
pub fn save_attrs(&self) -> Result<()> {
let attrs_path = self.catalog_dir.join("catalog.attrs");
self.attrs.save(&attrs_path)?;
Ok(())
}
/// Create a new update log
pub fn create_update_log(&mut self, name: &str) -> &mut UpdateLog {
self.update_logs.entry(name.to_string()).or_insert_with(UpdateLog::new)
}
/// Save an update log
pub fn save_update_log(&self, name: &str) -> Result<()> {
if let Some(log) = self.update_logs.get(name) {
let log_path = self.catalog_dir.join(name);
log.save(&log_path)?;
// Update catalog attributes
let now = SystemTime::now();
let timestamp = format_iso8601_basic(&now);
let mut attrs = self.attrs.clone();
attrs.updates.insert(name.to_string(), UpdateLogInfo {
last_modified: timestamp,
signature_sha1: None,
});
let attrs_path = self.catalog_dir.join("catalog.attrs");
attrs.save(&attrs_path)?;
Ok(())
} else {
Err(anyhow::anyhow!("Update log not loaded: {}", name))
}
}
/// Load an update log
pub fn load_update_log(&mut self, name: &str) -> Result<()> {
let log_path = self.catalog_dir.join(name);
if log_path.exists() {
let log = UpdateLog::load(&log_path)?;
self.update_logs.insert(name.to_string(), log);
Ok(())
} else {
Err(anyhow::anyhow!("Update log does not exist: {}", name))
}
}
/// Get an update log
pub fn get_update_log(&self, name: &str) -> Option<&UpdateLog> {
self.update_logs.get(name)
}
/// Get a mutable update log
pub fn get_update_log_mut(&mut self, name: &str) -> Option<&mut UpdateLog> {
self.update_logs.get_mut(name)
}
}

View file

@ -16,6 +16,7 @@ use flate2::Compression as GzipCompression;
use lz4::EncoderBuilder;
use regex::Regex;
use std::collections::{HashMap, HashSet};
use std::cell::RefCell;
use serde::{Serialize, Deserialize};
use crate::actions::{Manifest, File as FileAction};
@ -69,16 +70,17 @@ impl SearchIndex {
let fmri = package.fmri.to_string();
// Add the package name as a term
self.add_term(&package.fmri.name, &fmri, &package.fmri.name);
self.add_term(package.fmri.stem(), &fmri, package.fmri.stem());
// Add the publisher as a term if available
if let Some(publisher) = &package.fmri.publisher {
self.add_term(publisher, &fmri, &package.fmri.name);
self.add_term(publisher, &fmri, package.fmri.stem());
}
// Add the version as a term if available
if let Some(version) = &package.fmri.version {
self.add_term(&version.to_string(), &fmri, &package.fmri.name);
let version = package.fmri.version();
if !version.is_empty() {
self.add_term(&version, &fmri, &package.fmri.stem());
}
// Add contents if available
@ -86,21 +88,21 @@ impl SearchIndex {
// Add files
if let Some(files) = &content.files {
for file in files {
self.add_term(file, &fmri, &package.fmri.name);
self.add_term(file, &fmri, package.fmri.stem());
}
}
// Add directories
if let Some(directories) = &content.directories {
for dir in directories {
self.add_term(dir, &fmri, &package.fmri.name);
self.add_term(dir, &fmri, package.fmri.stem());
}
}
// Add dependencies
if let Some(dependencies) = &content.dependencies {
for dep in dependencies {
self.add_term(dep, &fmri, &package.fmri.name);
self.add_term(dep, &fmri, package.fmri.stem());
}
}
}
@ -197,6 +199,8 @@ impl SearchIndex {
pub struct FileBackend {
pub path: PathBuf,
pub config: RepositoryConfig,
/// Catalog manager for handling catalog operations
catalog_manager: Option<crate::repository::catalog::CatalogManager>,
}
/// Format a SystemTime as an ISO 8601 timestamp string
@ -475,6 +479,7 @@ impl Repository for FileBackend {
let repo = FileBackend {
path: path.to_path_buf(),
config,
catalog_manager: None,
};
// Create the repository directories
@ -503,6 +508,7 @@ impl Repository for FileBackend {
Ok(FileBackend {
path: path.to_path_buf(),
config,
catalog_manager: None,
})
}
@ -700,14 +706,14 @@ impl Repository for FileBackend {
match Regex::new(pat) {
Ok(regex) => {
// Use regex matching
if !regex.is_match(&parsed_fmri.name) {
if !regex.is_match(parsed_fmri.stem()) {
continue;
}
},
Err(err) => {
// Log the error but fall back to simple string contains
eprintln!("Error compiling regex pattern '{}': {}", pat, err);
if !parsed_fmri.name.contains(pat) {
if !parsed_fmri.stem().contains(pat) {
continue;
}
}
@ -827,14 +833,14 @@ impl Repository for FileBackend {
match Regex::new(pat) {
Ok(regex) => {
// Use regex matching
if !regex.is_match(&parsed_fmri.name) {
if !regex.is_match(parsed_fmri.stem()) {
continue;
}
},
Err(err) => {
// Log the error but fall back to simple string contains
eprintln!("Error compiling regex pattern '{}': {}", pat, err);
if !parsed_fmri.name.contains(pat) {
if !parsed_fmri.stem().contains(pat) {
continue;
}
}
@ -842,10 +848,11 @@ impl Repository for FileBackend {
}
// Format the package identifier using the FMRI
pkg_id = if let Some(version) = &parsed_fmri.version {
format!("{}@{}", parsed_fmri.name, version)
let version = parsed_fmri.version();
pkg_id = if !version.is_empty() {
format!("{}@{}", parsed_fmri.stem(), version)
} else {
parsed_fmri.name.clone()
parsed_fmri.stem().to_string()
};
break;
@ -1093,7 +1100,7 @@ impl Repository for FileBackend {
.into_iter()
.filter(|pkg| {
// Match against package name
pkg.fmri.name.contains(query)
pkg.fmri.stem().contains(query)
})
.collect();
@ -1124,6 +1131,226 @@ impl FileBackend {
Ok(())
}
/// Get or initialize the catalog manager
fn get_catalog_manager(&mut self) -> Result<&mut crate::repository::catalog::CatalogManager> {
if self.catalog_manager.is_none() {
let catalog_dir = self.path.join("catalog");
self.catalog_manager = Some(crate::repository::catalog::CatalogManager::new(&catalog_dir)?);
}
Ok(self.catalog_manager.as_mut().unwrap())
}
/// URL encode a string for use in a filename
fn url_encode(s: &str) -> String {
let mut result = String::new();
for c in s.chars() {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => result.push(c),
' ' => result.push('+'),
_ => {
result.push('%');
result.push_str(&format!("{:02X}", c as u8));
}
}
}
result
}
/// Generate catalog parts for a publisher
fn generate_catalog_parts(&mut self, publisher: &str, create_update_log: bool) -> Result<()> {
println!("Generating catalog parts for publisher: {}", publisher);
// Collect package data first
let repo_path = self.path.clone();
let packages = self.list_packages(Some(publisher), None)?;
// Prepare data structures for catalog parts
let mut base_entries = Vec::new();
let mut dependency_entries = Vec::new();
let mut summary_entries = Vec::new();
let mut update_entries = Vec::new();
// Track package counts
let mut package_count = 0;
let mut package_version_count = 0;
// Process each package
for package in packages {
let fmri = &package.fmri;
let stem = fmri.stem();
// Skip if no version
if fmri.version().is_empty() {
continue;
}
// Get the package manifest
let pkg_dir = repo_path.join("pkg").join(publisher).join(stem);
if !pkg_dir.exists() {
continue;
}
// Get the package version
let version = fmri.version();
let encoded_version = Self::url_encode(&version);
let manifest_path = pkg_dir.join(encoded_version);
if !manifest_path.exists() {
continue;
}
// Read the manifest
let manifest_content = std::fs::read_to_string(&manifest_path)?;
let manifest = crate::actions::Manifest::parse_string(manifest_content.clone())?;
// Calculate SHA-256 hash of the manifest (as a substitute for SHA-1)
let mut hasher = sha2::Sha256::new();
hasher.update(manifest_content.as_bytes());
let signature = format!("{:x}", hasher.finalize());
// Add to base entries
base_entries.push((fmri.clone(), None, signature.clone()));
// Extract dependency actions
let mut dependency_actions = Vec::new();
for dep in &manifest.dependencies {
if let Some(dep_fmri) = &dep.fmri {
dependency_actions.push(format!("depend fmri={} type={}", dep_fmri, dep.dependency_type));
}
}
// Extract variant and facet actions
for attr in &manifest.attributes {
if attr.key.starts_with("variant.") || attr.key.starts_with("facet.") {
let values_str = attr.values.join(" value=");
dependency_actions.push(format!("set name={} value={}", attr.key, values_str));
}
}
// Add to dependency entries if there are dependency actions
if !dependency_actions.is_empty() {
dependency_entries.push((fmri.clone(), Some(dependency_actions.clone()), signature.clone()));
}
// Extract summary actions (set actions excluding variants and facets)
let mut summary_actions = Vec::new();
for attr in &manifest.attributes {
if !attr.key.starts_with("variant.") && !attr.key.starts_with("facet.") {
let values_str = attr.values.join(" value=");
summary_actions.push(format!("set name={} value={}", attr.key, values_str));
}
}
// Add to summary entries if there are summary actions
if !summary_actions.is_empty() {
summary_entries.push((fmri.clone(), Some(summary_actions.clone()), signature.clone()));
}
// Prepare update entry if needed
if create_update_log {
let mut catalog_parts = std::collections::HashMap::new();
// Add dependency actions to update entry
if !dependency_actions.is_empty() {
let mut actions = std::collections::HashMap::new();
actions.insert("actions".to_string(), dependency_actions);
catalog_parts.insert("catalog.dependency.C".to_string(), actions);
}
// Add summary actions to update entry
if !summary_actions.is_empty() {
let mut actions = std::collections::HashMap::new();
actions.insert("actions".to_string(), summary_actions);
catalog_parts.insert("catalog.summary.C".to_string(), actions);
}
// Add to update entries
update_entries.push((fmri.clone(), catalog_parts, signature));
}
// Update counts
package_count += 1;
package_version_count += 1;
}
// Now get the catalog manager and create the catalog parts
let catalog_manager = self.get_catalog_manager()?;
// Create and populate the base part
let base_part_name = "catalog.base.C".to_string();
let base_part = catalog_manager.create_part(&base_part_name);
for (fmri, actions, signature) in base_entries {
base_part.add_package(publisher, &fmri, actions, Some(signature));
}
catalog_manager.save_part(&base_part_name)?;
// Create and populate dependency part
let dependency_part_name = "catalog.dependency.C".to_string();
let dependency_part = catalog_manager.create_part(&dependency_part_name);
for (fmri, actions, signature) in dependency_entries {
dependency_part.add_package(publisher, &fmri, actions, Some(signature));
}
catalog_manager.save_part(&dependency_part_name)?;
// Create and populate summary part
let summary_part_name = "catalog.summary.C".to_string();
let summary_part = catalog_manager.create_part(&summary_part_name);
for (fmri, actions, signature) in summary_entries {
summary_part.add_package(publisher, &fmri, actions, Some(signature));
}
catalog_manager.save_part(&summary_part_name)?;
// Create and populate the update log if needed
if create_update_log {
let now = std::time::SystemTime::now();
let timestamp = format_iso8601_timestamp(&now);
let update_log_name = format!("update.{}Z.C", timestamp.split('.').next().unwrap());
let update_log = catalog_manager.create_update_log(&update_log_name);
for (fmri, catalog_parts, signature) in update_entries {
update_log.add_update(
publisher,
&fmri,
crate::repository::catalog::CatalogOperationType::Add,
catalog_parts,
Some(signature),
);
}
catalog_manager.save_update_log(&update_log_name)?;
}
// Update catalog attributes
let now = std::time::SystemTime::now();
let timestamp = format_iso8601_timestamp(&now);
let attrs = catalog_manager.attrs_mut();
attrs.last_modified = timestamp.clone();
attrs.package_count = package_count;
attrs.package_version_count = package_version_count;
// Add part information
attrs.parts.insert(base_part_name.clone(), crate::repository::catalog::CatalogPartInfo {
last_modified: timestamp.clone(),
signature_sha1: None,
});
attrs.parts.insert(dependency_part_name.clone(), crate::repository::catalog::CatalogPartInfo {
last_modified: timestamp.clone(),
signature_sha1: None,
});
attrs.parts.insert(summary_part_name.clone(), crate::repository::catalog::CatalogPartInfo {
last_modified: timestamp.clone(),
signature_sha1: None,
});
// Save catalog attributes
catalog_manager.save_attrs()?;
Ok(())
}
/// Build a search index for a publisher
fn build_search_index(&self, publisher: &str) -> Result<()> {
println!("Building search index for publisher: {}", publisher);
@ -1160,10 +1387,11 @@ impl FileBackend {
};
// Create a PackageContents struct
let package_id = if let Some(version) = &parsed_fmri.version {
format!("{}@{}", parsed_fmri.name, version)
let version = parsed_fmri.version();
let package_id = if !version.is_empty() {
format!("{}@{}", parsed_fmri.stem(), version)
} else {
parsed_fmri.name.clone()
parsed_fmri.stem().to_string()
};
// Extract content information

View file

@ -9,9 +9,11 @@ use std::collections::HashMap;
mod file_backend;
mod rest_backend;
mod catalog;
pub use file_backend::FileBackend;
pub use rest_backend::RestBackend;
pub use catalog::{CatalogManager, CatalogAttrs, CatalogPart, UpdateLog, CatalogOperationType};
/// Repository configuration filename
pub const REPOSITORY_CONFIG_FILENAME: &str = "pkg6.repository";

View file

@ -212,10 +212,11 @@ impl Repository for RestBackend {
// In a real implementation, we would get this information from the REST API
// Format the package identifier using the FMRI
let pkg_id = if let Some(version) = &pkg_info.fmri.version {
format!("{}@{}", pkg_info.fmri.name, version)
let version = pkg_info.fmri.version();
let pkg_id = if !version.is_empty() {
format!("{}@{}", pkg_info.fmri.stem(), version)
} else {
pkg_info.fmri.name.clone()
pkg_info.fmri.stem().to_string()
};
// Example content for each type

View file

@ -379,17 +379,14 @@ fn main() -> Result<()> {
// Print packages
for pkg_info in packages {
// Format version and publisher, handling optional fields
let version_str = match &pkg_info.fmri.version {
Some(version) => version.to_string(),
None => String::new(),
};
let version_str = pkg_info.fmri.version();
let publisher_str = match &pkg_info.fmri.publisher {
Some(publisher) => publisher.clone(),
None => String::new(),
};
println!("{:<30} {:<15} {:<10}", pkg_info.fmri.name, version_str, publisher_str);
println!("{:<30} {:<15} {:<10}", pkg_info.fmri.stem(), version_str, publisher_str);
}
Ok(())