From a948f87e6f8f5375c2cc079878e6bcc0d0585c1b Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Tue, 9 Dec 2025 12:12:57 +0100 Subject: [PATCH] Add legacy repository support and SHA-1 signature handling - Introduced fallback for legacy `pkg5.repository` configuration in INI format alongside the existing `pkg6.repository` JSON format. - Enabled SHA-1 signature computation for compatibility with legacy catalog signatures. - Added methods to save update logs in legacy format and enhance catalog compatibility. - Updated dependencies to include `sha1` for hashing. --- Cargo.lock | 12 ++++ libips/Cargo.toml | 2 + libips/src/repository/file_backend.rs | 95 +++++++++++++++++++++++++-- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5da7ae..e7f711b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1483,6 +1483,7 @@ dependencies = [ "serde", "serde_cbor", "serde_json", + "sha1", "sha2", "sha3", "strum", @@ -2773,6 +2774,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" diff --git a/libips/Cargo.toml b/libips/Cargo.toml index e4cb2e8..52feee7 100644 --- a/libips/Cargo.toml +++ b/libips/Cargo.toml @@ -24,6 +24,8 @@ maplit = "1" object = "0.37" goblin = "0.8" sha2 = "0.10" +# For SHA-1 signatures required by legacy catalog format +sha1 = "0.10" sha3 = "0.10" pest = "2.1.3" pest_derive = "2.1.0" diff --git a/libips/src/repository/file_backend.rs b/libips/src/repository/file_backend.rs index 3ff3ae6..3387cd7 100644 --- a/libips/src/repository/file_backend.rs +++ b/libips/src/repository/file_backend.rs @@ -661,9 +661,52 @@ impl ReadableRepository for FileBackend { } // Load the repository configuration - let config_path = path.join(REPOSITORY_CONFIG_FILENAME); - let config_data = fs::read_to_string(&config_path).map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config_path.display(), e)))?; - let config: RepositoryConfig = serde_json::from_str(&config_data)?; + // Prefer pkg6.repository (JSON). If absent, try legacy pkg5.repository (INI) + let config6_path = path.join(REPOSITORY_CONFIG_FILENAME); + let config5_path = path.join("pkg5.repository"); + + let config: RepositoryConfig = if config6_path.exists() { + let config_data = fs::read_to_string(&config6_path) + .map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config6_path.display(), e)))?; + serde_json::from_str(&config_data)? + } else if config5_path.exists() { + // Minimal mapping for legacy INI: take publishers only from INI; do not scan disk. + let ini = Ini::load_from_file(&config5_path) + .map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config5_path.display(), e)))?; + + // Default repository version for legacy format is v4 + let mut cfg = RepositoryConfig::default(); + + // Try to read default publisher from [publisher] section (key: prefix) + if let Some(section) = ini.section(Some("publisher")) { + if let Some(prefix) = section.get("prefix") { + cfg.default_publisher = Some(prefix.to_string()); + cfg.publishers.push(prefix.to_string()); + } + } + + // If INI enumerates publishers in an optional [publishers] section as comma-separated list + if let Some(section) = ini.section(Some("publishers")) { + if let Some(list) = section.get("list") { + // replace list strictly by INI contents per requirements + cfg.publishers.clear(); + for p in list.split(',') { + let name = p.trim(); + if !name.is_empty() { + cfg.publishers.push(name.to_string()); + } + } + } + } + + cfg + } else { + return Err(RepositoryError::ConfigReadError(format!( + "No repository config found: expected {} or {}", + config6_path.display(), + config5_path.display() + ))); + }; Ok(FileBackend { path: path.to_path_buf(), @@ -2083,10 +2126,11 @@ impl FileBackend { // Parse the manifest using parse_file which handles JSON correctly let manifest = Manifest::parse_file(&manifest_path)?; - // Calculate SHA-256 hash of the manifest (as a substitute for SHA-1) - let mut hasher = sha2::Sha256::new(); + // Calculate SHA-1 hash of the manifest for legacy catalog signature compatibility + let mut hasher = sha1::Sha1::new(); hasher.update(manifest_content.as_bytes()); - let signature = format!("{:x}", hasher.finalize()); + let signature = hasher.finalize(); + let signature = format!("{:x}", signature); // Add to base entries base_entries.push((fmri.clone(), None, signature.clone())); @@ -2298,6 +2342,45 @@ impl FileBackend { info!("Catalog rebuilt for publisher: {}", publisher); Ok(()) } + + /// Save an update log file to the publisher's catalog directory. + /// + /// The file name must follow the legacy pattern: `update..` + /// for example: `update.20090524T042841Z.C`. + pub fn save_update_log( + &self, + publisher: &str, + log_filename: &str, + log: &crate::repository::catalog::UpdateLog, + ) -> Result<()> { + if log_filename.contains('/') || log_filename.contains('\\') { + return Err(RepositoryError::PathPrefixError(log_filename.to_string())); + } + + // Ensure catalog dir exists + let catalog_dir = Self::construct_catalog_path(&self.path, publisher); + std::fs::create_dir_all(&catalog_dir).map_err(|e| RepositoryError::DirectoryCreateError { path: catalog_dir.clone(), source: e })?; + + // Serialize JSON + let json = serde_json::to_vec_pretty(log) + .map_err(|e| RepositoryError::JsonSerializeError(format!("Update log serialize error: {}", e)))?; + + // Write atomically + let target = catalog_dir.join(log_filename); + let tmp = target.with_extension("tmp"); + { + let mut f = std::fs::File::create(&tmp) + .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + use std::io::Write as _; + f.write_all(&json) + .map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + f.flush().map_err(|e| RepositoryError::FileWriteError { path: tmp.clone(), source: e })?; + } + std::fs::rename(&tmp, &target) + .map_err(|e| RepositoryError::FileWriteError { path: target.clone(), source: e })?; + + Ok(()) + } /// Generate the file path for a given hash using the new directory structure with publisher /// This is a wrapper around the construct_file_path_with_publisher helper method