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.
This commit is contained in:
Till Wegmueller 2025-12-09 12:12:57 +01:00
parent c4910bb434
commit a948f87e6f
No known key found for this signature in database
3 changed files with 103 additions and 6 deletions

12
Cargo.lock generated
View file

@ -1483,6 +1483,7 @@ dependencies = [
"serde", "serde",
"serde_cbor", "serde_cbor",
"serde_json", "serde_json",
"sha1",
"sha2", "sha2",
"sha3", "sha3",
"strum", "strum",
@ -2773,6 +2774,17 @@ dependencies = [
"serde", "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]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"

View file

@ -24,6 +24,8 @@ maplit = "1"
object = "0.37" object = "0.37"
goblin = "0.8" goblin = "0.8"
sha2 = "0.10" sha2 = "0.10"
# For SHA-1 signatures required by legacy catalog format
sha1 = "0.10"
sha3 = "0.10" sha3 = "0.10"
pest = "2.1.3" pest = "2.1.3"
pest_derive = "2.1.0" pest_derive = "2.1.0"

View file

@ -661,9 +661,52 @@ impl ReadableRepository for FileBackend {
} }
// Load the repository configuration // Load the repository configuration
let config_path = path.join(REPOSITORY_CONFIG_FILENAME); // Prefer pkg6.repository (JSON). If absent, try legacy pkg5.repository (INI)
let config_data = fs::read_to_string(&config_path).map_err(|e| RepositoryError::ConfigReadError(format!("{}: {}", config_path.display(), e)))?; let config6_path = path.join(REPOSITORY_CONFIG_FILENAME);
let config: RepositoryConfig = serde_json::from_str(&config_data)?; 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 { Ok(FileBackend {
path: path.to_path_buf(), path: path.to_path_buf(),
@ -2083,10 +2126,11 @@ impl FileBackend {
// Parse the manifest using parse_file which handles JSON correctly // Parse the manifest using parse_file which handles JSON correctly
let manifest = Manifest::parse_file(&manifest_path)?; let manifest = Manifest::parse_file(&manifest_path)?;
// Calculate SHA-256 hash of the manifest (as a substitute for SHA-1) // Calculate SHA-1 hash of the manifest for legacy catalog signature compatibility
let mut hasher = sha2::Sha256::new(); let mut hasher = sha1::Sha1::new();
hasher.update(manifest_content.as_bytes()); 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 // Add to base entries
base_entries.push((fmri.clone(), None, signature.clone())); base_entries.push((fmri.clone(), None, signature.clone()));
@ -2299,6 +2343,45 @@ impl FileBackend {
Ok(()) Ok(())
} }
/// Save an update log file to the publisher's catalog directory.
///
/// The file name must follow the legacy pattern: `update.<logdate>.<locale>`
/// 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 /// Generate the file path for a given hash using the new directory structure with publisher
/// This is a wrapper around the construct_file_path_with_publisher helper method /// This is a wrapper around the construct_file_path_with_publisher helper method
fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf { fn generate_file_path_with_publisher(&self, publisher: &str, hash: &str) -> PathBuf {