upstream-rs 2.6.0

Fetch package updates directly from the source.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result, anyhow};
use serde::{Deserialize, Serialize};

use crate::services::trust::{CosignPublicKey, MinisignPublicKey, TrustedSignatureKeys};
use crate::utils::filesystem::atomic_ops::write_atomic;

pub const TRUST_STORAGE_VERSION: u32 = 1;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct TrustStorageFile {
    version: u32,
    minisign_public_keys: Vec<MinisignPublicKey>,
    cosign_public_keys: Vec<CosignPublicKey>,
}

impl Default for TrustStorageFile {
    fn default() -> Self {
        Self {
            version: TRUST_STORAGE_VERSION,
            minisign_public_keys: Vec::new(),
            cosign_public_keys: Vec::new(),
        }
    }
}

pub struct KeyMergeSummary {
    pub imported: usize,
    pub deduped: usize,
    pub total: usize,
}

pub struct SignatureKeyMergeSummary {
    pub minisign: KeyMergeSummary,
    pub cosign: KeyMergeSummary,
}

#[derive(Debug)]
pub struct TrustStorage {
    file: TrustStorageFile,
    trust_file: PathBuf,
}

impl TrustStorage {
    pub fn new(trust_file: &Path) -> Result<Self> {
        let mut storage = Self {
            file: TrustStorageFile::default(),
            trust_file: trust_file.to_path_buf(),
        };
        storage.load()?;
        Ok(storage)
    }

    pub fn load(&mut self) -> Result<()> {
        if !self.trust_file.exists() {
            self.file = TrustStorageFile::default();
            return Ok(());
        }

        match fs::read_to_string(&self.trust_file) {
            Ok(json) => {
                if json.trim().is_empty() {
                    self.file = TrustStorageFile::default();
                    return Ok(());
                }
                let file: TrustStorageFile = serde_json::from_str(&json).with_context(|| {
                    format!(
                        "Failed to parse trust storage '{}'",
                        self.trust_file.display()
                    )
                })?;
                if file.version != TRUST_STORAGE_VERSION {
                    return Err(anyhow!(
                        "Unsupported trust storage version {} in '{}'. Expected version {}.",
                        file.version,
                        self.trust_file.display(),
                        TRUST_STORAGE_VERSION
                    ));
                }
                self.file = file;
                Ok(())
            }
            Err(e) => Err(anyhow!("Warning: Failed to load trust storage: {}", e)),
        }
    }

    pub fn save(&self) -> Result<()> {
        let json = serde_json::to_string_pretty(&self.file)
            .context("Failed to serialize trust storage")?;
        write_atomic(&self.trust_file, json.as_bytes()).with_context(|| {
            format!(
                "Failed to write trust storage to '{}'",
                self.trust_file.display()
            )
        })
    }

    pub fn ensure_exists(&self) -> Result<()> {
        if !self.trust_file.exists() {
            self.save()?;
        }
        Ok(())
    }

    pub fn trusted_signature_keys(&self) -> TrustedSignatureKeys {
        TrustedSignatureKeys {
            minisign_public_keys: self.file.minisign_public_keys.clone(),
            cosign_public_keys: self.file.cosign_public_keys.clone(),
        }
    }

    pub fn merge_trusted_minisign_keys(
        &mut self,
        keys: &[MinisignPublicKey],
    ) -> Result<KeyMergeSummary> {
        let mut imported = 0_usize;
        let mut deduped = 0_usize;

        for key in keys {
            let normalized = key.key.trim();
            if normalized.is_empty() {
                continue;
            }
            let duplicate = self
                .file
                .minisign_public_keys
                .iter()
                .any(|existing| existing.key.trim().eq_ignore_ascii_case(normalized));
            if duplicate {
                deduped += 1;
                continue;
            }

            self.file.minisign_public_keys.push(MinisignPublicKey {
                id: key.id.clone(),
                key: normalized.to_string(),
            });
            imported += 1;
        }

        self.save()?;

        Ok(KeyMergeSummary {
            imported,
            deduped,
            total: self.file.minisign_public_keys.len(),
        })
    }

    pub fn merge_trusted_cosign_keys(
        &mut self,
        keys: &[CosignPublicKey],
    ) -> Result<KeyMergeSummary> {
        let mut imported = 0_usize;
        let mut deduped = 0_usize;

        for key in keys {
            let normalized = key.key.trim();
            if normalized.is_empty() {
                continue;
            }
            let duplicate = self
                .file
                .cosign_public_keys
                .iter()
                .any(|existing| existing.key.trim() == normalized);
            if duplicate {
                deduped += 1;
                continue;
            }

            self.file.cosign_public_keys.push(CosignPublicKey {
                id: key.id.clone(),
                key: normalized.to_string(),
            });
            imported += 1;
        }

        self.save()?;

        Ok(KeyMergeSummary {
            imported,
            deduped,
            total: self.file.cosign_public_keys.len(),
        })
    }

    pub fn merge_trusted_keys(
        &mut self,
        minisign_keys: &[MinisignPublicKey],
        cosign_keys: &[CosignPublicKey],
    ) -> Result<SignatureKeyMergeSummary> {
        let minisign = self.merge_trusted_minisign_keys(minisign_keys)?;
        let cosign = self.merge_trusted_cosign_keys(cosign_keys)?;
        Ok(SignatureKeyMergeSummary { minisign, cosign })
    }
}

#[cfg(test)]
mod tests {
    use super::{TRUST_STORAGE_VERSION, TrustStorage};
    use crate::services::trust::{CosignPublicKey, MinisignPublicKey};
    use std::path::{Path, PathBuf};
    use std::time::{SystemTime, UNIX_EPOCH};
    use std::{fs, io};

    fn temp_trust_file(name: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_nanos())
            .unwrap_or(0);
        std::env::temp_dir()
            .join(format!("upstream-trust-storage-test-{name}-{nanos}"))
            .join("trust.json")
    }

    fn cleanup(path: &Path) -> io::Result<()> {
        if let Some(parent) = path.parent() {
            fs::remove_dir_all(parent)?;
        }
        Ok(())
    }

    #[test]
    fn new_starts_empty_when_file_missing() {
        let path = temp_trust_file("missing");
        let storage = TrustStorage::new(&path).expect("create storage");
        let keys = storage.trusted_signature_keys();
        assert!(keys.minisign_public_keys.is_empty());
        assert!(keys.cosign_public_keys.is_empty());
    }

    #[test]
    fn merge_keys_dedupes_and_round_trips() {
        let path = temp_trust_file("merge");
        let mut storage = TrustStorage::new(&path).expect("create storage");
        let summary = storage
            .merge_trusted_keys(
                &[
                    MinisignPublicKey {
                        id: Some("one".to_string()),
                        key: "RWabc".to_string(),
                    },
                    MinisignPublicKey {
                        id: Some("dupe".to_string()),
                        key: "rwABC".to_string(),
                    },
                ],
                &[CosignPublicKey {
                    id: None,
                    key: "-----BEGIN PUBLIC KEY-----\nkey\n-----END PUBLIC KEY-----".to_string(),
                }],
            )
            .expect("merge keys");

        assert_eq!(summary.minisign.imported, 1);
        assert_eq!(summary.minisign.deduped, 1);
        assert_eq!(summary.cosign.imported, 1);

        let storage = TrustStorage::new(&path).expect("reload");
        let keys = storage.trusted_signature_keys();
        assert_eq!(keys.minisign_public_keys.len(), 1);
        assert_eq!(keys.cosign_public_keys.len(), 1);
        cleanup(&path).expect("cleanup");
    }

    #[test]
    fn ensure_exists_writes_empty_trust_storage() {
        let path = temp_trust_file("ensure");
        let storage = TrustStorage::new(&path).expect("create storage");
        storage.ensure_exists().expect("ensure exists");

        let value: serde_json::Value =
            serde_json::from_slice(&fs::read(&path).expect("read trust file"))
                .expect("parse trust file");
        assert_eq!(
            value["version"].as_u64(),
            Some(TRUST_STORAGE_VERSION as u64)
        );
        assert_eq!(
            value["minisign_public_keys"].as_array().map(Vec::len),
            Some(0)
        );
        assert_eq!(
            value["cosign_public_keys"].as_array().map(Vec::len),
            Some(0)
        );
        cleanup(&path).expect("cleanup");
    }

    #[test]
    fn rejects_unsupported_version() {
        let path = temp_trust_file("bad-version");
        fs::create_dir_all(path.parent().expect("parent")).expect("create parent");
        fs::write(
            &path,
            r#"{"version":2,"minisign_public_keys":[],"cosign_public_keys":[]}"#,
        )
        .expect("write trust file");

        let err = TrustStorage::new(&path).expect_err("unsupported version");
        assert!(
            err.to_string()
                .contains("Unsupported trust storage version")
        );
        cleanup(&path).expect("cleanup");
    }
}