lintian-brush 0.182.0

Automatic lintian issue fixer
use crate::declare_detector;
use crate::diagnostic::{Action, Diagnostic, FilesystemAction};
use crate::{Certainty, FixerError, FixerPreferences, LintianIssue, Visibility};
use debian_workspace::Workspace;
use sequoia_openpgp::armor::{Kind, Writer};
use sequoia_openpgp::cert::Cert;
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::serialize::Serialize;
use std::path::PathBuf;

const MAIN: &str = "debian/upstream/signing-key.asc";
const OTHERS: &[&str] = &[
    "debian/upstream/signing-key.pgp",
    "debian/upstream-signing-key.pgp",
];

fn merge_keys(key_data: Vec<Vec<u8>>) -> Result<String, Box<dyn std::error::Error>> {
    let mut all_certs = Vec::new();
    for data in key_data {
        match Cert::from_bytes(&data) {
            Ok(cert) => all_certs.push(cert),
            Err(_) => {
                use sequoia_openpgp::cert::CertParser;
                let parser = CertParser::from_bytes(&data)?;
                for cert_result in parser {
                    match cert_result {
                        Ok(cert) => all_certs.push(cert),
                        Err(e) => {
                            tracing::debug!("failed to parse one certificate: {}", e);
                        }
                    }
                }
            }
        }
    }
    if all_certs.is_empty() {
        return Err("No valid certificates found in any of the key files".into());
    }

    let mut output = Vec::new();
    {
        let mut writer = Writer::new(&mut output, Kind::PublicKey)?;
        for cert in all_certs {
            cert.serialize(&mut writer)?;
        }
        writer.finalize()?;
    }
    Ok(String::from_utf8(output)?)
}

pub fn detect(
    ws: &dyn Workspace,
    _preferences: &FixerPreferences,
) -> Result<Vec<Diagnostic>, FixerError> {
    // Walk other locations first then the main location, matching the
    // legacy ordering (this affects the LintianIssue's info list).
    let mut existing: Vec<(PathBuf, Vec<u8>)> = Vec::new();
    for rel in OTHERS {
        let p = PathBuf::from(rel);
        if let Some(data) = ws.read_file(&p)? {
            existing.push((p, data.into_owned()));
        }
    }
    let main_rel = PathBuf::from(MAIN);
    if let Some(data) = ws.read_file(&main_rel)? {
        existing.push((main_rel.clone(), data.into_owned()));
    }

    if existing.len() < 2 {
        return Ok(Vec::new());
    }

    let key_data: Vec<Vec<u8>> = existing.iter().map(|(_, d)| d.clone()).collect();
    let merged = merge_keys(key_data)
        .map_err(|e| FixerError::Other(format!("Failed to merge keys: {}", e)))?;

    let info: Vec<String> = existing
        .iter()
        .map(|(p, _)| p.display().to_string())
        .collect();
    let issue = LintianIssue::source_with_info(
        "public-upstream-keys-in-multiple-locations",
        Visibility::Info,
        info,
    );

    let mut actions: Vec<Action> = vec![Action::Filesystem(FilesystemAction::Write {
        file: main_rel,
        content: merged.into_bytes(),
    })];
    for rel in OTHERS {
        let p = PathBuf::from(rel);
        if ws.read_file(&p)?.is_some() {
            actions.push(Action::Filesystem(FilesystemAction::Delete { file: p }));
        }
    }

    Ok(vec![Diagnostic::with_actions(
        issue,
        "Upstream signing keys are in multiple locations.",
        "Merge upstream signing key files.",
        actions,
    )
    .with_certainty(Certainty::Certain)])
}

declare_detector! {
    name: "public-upstream-keys-in-multiple-locations",
    tags: ["public-upstream-keys-in-multiple-locations"],
    triggers: [
        debian_workspace::Trigger::File("debian/upstream/signing-key.asc"),
        debian_workspace::Trigger::File("debian/upstream/signing-key.pgp"),
        debian_workspace::Trigger::File("debian/upstream-signing-key.pgp"),
    ],
    detect: |ws, prefs| detect(ws, prefs),
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::detector::Detector;
    use crate::{FixerPreferences, Version};
    use std::fs;
    use std::path::Path;
    use tempfile::TempDir;

    fn run_apply(base: &Path) -> Result<crate::FixerResult, FixerError> {
        let version: Version = "1.0".parse().unwrap();
        let adapter = DetectorImpl;
        {
            let ws = debian_workspace::fs_workspace::FsWorkspace::new(
                base,
                Some("test".into()),
                Some(version.clone()),
            );
            adapter.apply(&ws, &FixerPreferences::default())
        }
    }

    #[test]
    fn test_single_key_file() {
        let tmp = TempDir::new().unwrap();
        let debian = tmp.path().join("debian");
        fs::create_dir(&debian).unwrap();
        fs::write(debian.join("upstream-signing-key.pgp"), b"dummy").unwrap();
        assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
    }

    #[test]
    fn test_no_key_files() {
        let tmp = TempDir::new().unwrap();
        let debian = tmp.path().join("debian");
        fs::create_dir(&debian).unwrap();
        assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
    }

    #[test]
    fn test_no_debian_dir() {
        let tmp = TempDir::new().unwrap();
        assert!(matches!(run_apply(tmp.path()), Err(FixerError::NoChanges)));
    }
}