diff --git a/libips/src/repository/catalog.rs b/libips/src/repository/catalog.rs index 54678dc..1b47828 100644 --- a/libips/src/repository/catalog.rs +++ b/libips/src/repository/catalog.rs @@ -5,6 +5,7 @@ use miette::Diagnostic; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::collections::BTreeMap; use std::fs; use std::io; @@ -205,23 +206,68 @@ pub struct PackageVersionEntry { } /// Catalog part (base, dependency, summary) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct CatalogPart { /// Packages by publisher and stem #[serde(flatten)] pub packages: BTreeMap>>, - /// Optional signature information - #[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")] - pub signature: Option>, + /// Metadata fields (keys starting with '_') + #[serde(flatten)] + pub metadata: BTreeMap, +} + +impl<'de> Deserialize<'de> for CatalogPart { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let all_entries = BTreeMap::::deserialize(deserializer)?; + let mut packages = BTreeMap::new(); + let mut metadata = BTreeMap::new(); + + for (k, v) in all_entries { + if k.starts_with('_') { + metadata.insert(k, v); + } else { + // Try to parse as package map + if let Ok(pkg_map) = serde_json::from_value(v.clone()) { + packages.insert(k, pkg_map); + } else { + // If it fails, treat as metadata + metadata.insert(k, v); + } + } + } + + Ok(CatalogPart { packages, metadata }) + } } impl CatalogPart { /// Create a new catalog part pub fn new() -> Self { CatalogPart { - signature: None, packages: BTreeMap::new(), + metadata: BTreeMap::new(), + } + } + + /// Get signature information if present + pub fn signature(&self) -> Option> { + self.metadata.get("_SIGNATURE").and_then(|v| { + serde_json::from_value::>(v.clone()).ok() + }) + } + + /// Set signature information + pub fn set_signature(&mut self, signature: Option>) { + if let Some(sig) = signature { + if let Ok(val) = serde_json::to_value(sig) { + self.metadata.insert("_SIGNATURE".to_string(), val); + } + } else { + self.metadata.remove("_SIGNATURE"); } } @@ -296,7 +342,7 @@ pub enum CatalogOperationType { } /// Package update entry in an update log -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct PackageUpdateEntry { /// Type of operation (add or remove) #[serde(rename = "op-type")] @@ -316,25 +362,116 @@ pub struct PackageUpdateEntry { /// Optional SHA-1 signature of the package manifest #[serde(rename = "signature-sha-1", skip_serializing_if = "Option::is_none")] pub signature_sha1: Option, + + /// Metadata fields (keys starting with '_') + #[serde(flatten)] + pub metadata: BTreeMap, +} + +impl<'de> Deserialize<'de> for PackageUpdateEntry { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Internal { + #[serde(rename = "op-type")] + op_type: CatalogOperationType, + #[serde(rename = "op-time")] + op_time: String, + version: String, + #[serde(rename = "signature-sha-1")] + signature_sha1: Option, + #[serde(flatten)] + all_entries: BTreeMap, + } + + let internal = Internal::deserialize(deserializer)?; + let mut catalog_parts = BTreeMap::new(); + let mut metadata = BTreeMap::new(); + + for (k, v) in internal.all_entries { + if k.starts_with('_') { + metadata.insert(k, v); + } else { + if let Ok(part_map) = serde_json::from_value(v.clone()) { + catalog_parts.insert(k, part_map); + } else { + metadata.insert(k, v); + } + } + } + + Ok(PackageUpdateEntry { + op_type: internal.op_type, + op_time: internal.op_time, + version: internal.version, + catalog_parts, + signature_sha1: internal.signature_sha1, + metadata, + }) + } } /// Update log -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct UpdateLog { /// Updates by publisher and stem pub updates: BTreeMap>>, - /// Optional signature information - #[serde(rename = "_SIGNATURE", skip_serializing_if = "Option::is_none")] - pub signature: Option>, + /// Metadata fields (keys starting with '_') + #[serde(flatten)] + pub metadata: BTreeMap, +} + +impl<'de> Deserialize<'de> for UpdateLog { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Internal { + #[serde(default)] + updates: BTreeMap>>, + #[serde(flatten)] + all_entries: BTreeMap, + } + + let internal = Internal::deserialize(deserializer)?; + let mut metadata = internal.all_entries; + metadata.remove("updates"); + + Ok(UpdateLog { + updates: internal.updates, + metadata, + }) + } } impl UpdateLog { /// Create a new update log pub fn new() -> Self { UpdateLog { - signature: None, updates: BTreeMap::new(), + metadata: BTreeMap::new(), + } + } + + /// Get signature information if present + pub fn signature(&self) -> Option> { + self.metadata.get("_SIGNATURE").and_then(|v| { + serde_json::from_value::>(v.clone()).ok() + }) + } + + /// Set signature information + pub fn set_signature(&mut self, signature: Option>) { + if let Some(sig) = signature { + if let Ok(val) = serde_json::to_value(sig) { + self.metadata.insert("_SIGNATURE".to_string(), val); + } + } else { + self.metadata.remove("_SIGNATURE"); } } @@ -364,6 +501,7 @@ impl UpdateLog { version: fmri.version(), catalog_parts, signature_sha1: signature, + metadata: BTreeMap::new(), }); } diff --git a/libips/src/repository/catalog_writer.rs b/libips/src/repository/catalog_writer.rs index e1d6151..0bd4ce4 100644 --- a/libips/src/repository/catalog_writer.rs +++ b/libips/src/repository/catalog_writer.rs @@ -117,18 +117,18 @@ pub(crate) fn write_catalog_attrs(path: &Path, attrs: &mut CatalogAttrs) -> Resu #[instrument(level = "debug", skip(part))] pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result { // Compute signature over content without _SIGNATURE - part.signature = None; + part.set_signature(None); let bytes_without_sig = serialize_python_style(&part)?; let sig = sha1_hex(&bytes_without_sig); let mut sig_map = std::collections::BTreeMap::new(); sig_map.insert("sha-1".to_string(), sig); - part.signature = Some(sig_map); + part.set_signature(Some(sig_map)); let final_bytes = serialize_python_style(&part)?; debug!(path = %path.display(), bytes = final_bytes.len(), "writing catalog part"); atomic_write_bytes(path, &final_bytes)?; Ok(part - .signature + .signature() .as_ref() .and_then(|m| m.get("sha-1").cloned()) .unwrap_or_default()) @@ -137,18 +137,18 @@ pub(crate) fn write_catalog_part(path: &Path, part: &mut CatalogPart) -> Result< #[instrument(level = "debug", skip(log))] pub(crate) fn write_update_log(path: &Path, log: &mut UpdateLog) -> Result { // Compute signature over content without _SIGNATURE - log.signature = None; + log.set_signature(None); let bytes_without_sig = serialize_python_style(&log)?; let sig = sha1_hex(&bytes_without_sig); let mut sig_map = std::collections::BTreeMap::new(); sig_map.insert("sha-1".to_string(), sig); - log.signature = Some(sig_map); + log.set_signature(Some(sig_map)); let final_bytes = serialize_python_style(&log)?; debug!(path = %path.display(), bytes = final_bytes.len(), "writing update log"); atomic_write_bytes(path, &final_bytes)?; Ok(log - .signature + .signature() .as_ref() .and_then(|m| m.get("sha-1").cloned()) .unwrap_or_default())