diff --git a/Cargo.lock b/Cargo.lock index 23cd20d..89efeee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/libips/Cargo.toml b/libips/Cargo.toml index ddb807c..271321c 100644 --- a/libips/Cargo.toml +++ b/libips/Cargo.toml @@ -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" diff --git a/libips/src/fmri.rs b/libips/src/fmri.rs index 8bb4830..36931b3 100644 --- a/libips/src/fmri.rs +++ b/libips/src/fmri.rs @@ -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 /// diff --git a/libips/src/repository/catalog.rs b/libips/src/repository/catalog.rs new file mode 100644 index 0000000..1d8c363 --- /dev/null +++ b/libips/src/repository/catalog.rs @@ -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 { + 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::::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, +} + +/// 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, +} + +/// 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>, + + /// 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, + + /// Available update logs + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub updates: HashMap, + + /// 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>(&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>(path: P) -> Result { + 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>, + + /// Optional SHA-1 signature of the package manifest + #[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")] + pub signature_sha1: Option, +} + +/// 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>, + + /// Packages by publisher and stem + pub packages: HashMap>>, +} + +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>, signature: Option) { + 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>(&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>(path: P) -> Result { + 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>>, + + /// Optional SHA-1 signature of the package manifest + #[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")] + pub signature_sha1: Option, +} + +/// 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>, + + /// Updates by publisher and stem + pub updates: HashMap>>, +} + +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>>, + signature: Option, + ) { + 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>(&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>(path: P) -> Result { + 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, + + /// Update logs + update_logs: HashMap, +} + +impl CatalogManager { + /// Create a new catalog manager + pub fn new>(catalog_dir: P) -> Result { + 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) + } +} \ No newline at end of file diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index ec6da1c..5f88585 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -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, } /// 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 diff --git a/libips/src/repository/mod.rs b/libips/src/repository/mod.rs index 454e47e..353049f 100644 --- a/libips/src/repository/mod.rs +++ b/libips/src/repository/mod.rs @@ -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"; diff --git a/libips/src/repository/rest_backend.rs b/libips/src/repository/rest_backend.rs index a9abbc0..03debd0 100644 --- a/libips/src/repository/rest_backend.rs +++ b/libips/src/repository/rest_backend.rs @@ -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 diff --git a/pkg6repo/src/main.rs b/pkg6repo/src/main.rs index d3ae538..bfb78cb 100644 --- a/pkg6repo/src/main.rs +++ b/pkg6repo/src/main.rs @@ -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(())