// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG, Cologne
// SPDX-FileCopyrightText: Author: Pierre Gronau <Pierre.Gronau@ndaal.eu>

//! Checksum sidecars for exported files.
//!
//! Each exported artifact may be accompanied by up to five checksum files
//! in GNU `shasum` format (`<hexdigest>␠␠<filename>\n`), one per algorithm —
//! the same set the ndaal CSAF distribution writes:
//! `.sha-256`, `.sha-512`, `.sha3-512`, `.blake3-512`, `.shake256-512`.

use std::fmt::Write as _;

use sha2::{Digest as _, Sha256, Sha512};
use sha3::digest::{ExtendableOutput as _, Update as _, XofReader as _};
use sha3::{Sha3_512, Shake256};

/// Which checksum sidecars to write for each exported file (default: all).
// One independent on/off flag per algorithm; a bitflag set would be less
// readable than five named booleans here.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Copy)]
pub struct SidecarHashes {
    /// `.sha-256` — SHA-2 256.
    pub sha256: bool,
    /// `.sha-512` — SHA-2 512.
    pub sha512: bool,
    /// `.sha3-512` — SHA-3 (Keccak) 512.
    pub sha3_512: bool,
    /// `.blake3-512` — BLAKE3 XOF, 64 bytes.
    pub blake3_512: bool,
    /// `.shake256-512` — SHAKE256 XOF (FIPS-202), 64 bytes.
    pub shake256_512: bool,
}

impl Default for SidecarHashes {
    fn default() -> Self {
        Self::all()
    }
}

impl SidecarHashes {
    /// Every sidecar enabled.
    #[must_use]
    pub const fn all() -> Self {
        Self {
            sha256: true,
            sha512: true,
            sha3_512: true,
            blake3_512: true,
            shake256_512: true,
        }
    }

    /// No sidecars enabled.
    #[must_use]
    pub const fn none() -> Self {
        Self {
            sha256: false,
            sha512: false,
            sha3_512: false,
            blake3_512: false,
            shake256_512: false,
        }
    }

    /// Whether at least one sidecar is enabled.
    #[must_use]
    pub const fn any(self) -> bool {
        self.sha256 || self.sha512 || self.sha3_512 || self.blake3_512 || self.shake256_512
    }

    /// Enable the sidecar named by its extension token (e.g. `sha-256`,
    /// `sha3-512`); unknown tokens are ignored. Starts from [`none`].
    ///
    /// [`none`]: SidecarHashes::none
    #[must_use]
    pub fn from_tokens<'a>(tokens: impl IntoIterator<Item = &'a str>) -> Self {
        let mut set = Self::none();
        for token in tokens {
            match token.trim() {
                "sha-256" | "sha256" => set.sha256 = true,
                "sha-512" | "sha512" => set.sha512 = true,
                "sha3-512" | "sha3" => set.sha3_512 = true,
                "blake3-512" | "blake3" => set.blake3_512 = true,
                "shake256-512" | "shake256" => set.shake256_512 = true,
                _ => {},
            }
        }
        set
    }
}

/// Lower-case hex encoding of a byte slice.
fn to_hex(bytes: &[u8]) -> String {
    bytes.iter().fold(
        String::with_capacity(bytes.len().saturating_mul(2)),
        |mut acc, byte| {
            // Writing to a String is infallible.
            let _ = write!(acc, "{byte:02x}");
            acc
        },
    )
}

/// SHA-256 hex digest.
#[must_use]
pub fn sha256_hex(data: &[u8]) -> String {
    to_hex(&Sha256::digest(data))
}

/// SHA-512 hex digest.
#[must_use]
pub fn sha512_hex(data: &[u8]) -> String {
    to_hex(&Sha512::digest(data))
}

/// SHA3-512 hex digest.
#[must_use]
pub fn sha3_512_hex(data: &[u8]) -> String {
    to_hex(&Sha3_512::digest(data))
}

/// BLAKE3 XOF squeezed to 64 bytes, hex-encoded.
#[must_use]
pub fn blake3_512_hex(data: &[u8]) -> String {
    let mut hasher = blake3::Hasher::new();
    hasher.update(data);
    let mut out = [0u8; 64];
    hasher.finalize_xof().fill(&mut out);
    to_hex(&out)
}

/// SHAKE256 XOF squeezed to 64 bytes, hex-encoded.
#[must_use]
pub fn shake256_512_hex(data: &[u8]) -> String {
    let mut hasher = Shake256::default();
    hasher.update(data);
    let mut out = [0u8; 64];
    hasher.finalize_xof().read(&mut out);
    to_hex(&out)
}

/// Write one sidecar file into `dir` and record its relative name.
///
/// # Errors
///
/// Propagates any cap-std write error.
fn write_one(
    dir: &cap_std::fs::Dir,
    rel: &str,
    ext: &str,
    hex: &str,
    written: &mut Vec<String>,
) -> std::io::Result<()> {
    let name = format!("{rel}.{ext}");
    dir.write(&name, format!("{hex}  {rel}\n").as_bytes())?;
    written.push(name);
    Ok(())
}

/// Write the enabled checksum sidecars for `data` next to the flat filename
/// `rel` inside `dir`. Returns the relative paths written, in order.
///
/// `rel` must be a plain filename (no directory separators) — the export
/// engine confines all writes to a single capability-scoped directory.
///
/// # Errors
///
/// Propagates any cap-std write error.
pub fn write_sidecars(
    dir: &cap_std::fs::Dir,
    rel: &str,
    data: &[u8],
    hashes: &SidecarHashes,
) -> std::io::Result<Vec<String>> {
    let mut written = Vec::new();
    if hashes.sha256 {
        write_one(dir, rel, "sha-256", &sha256_hex(data), &mut written)?;
    }
    if hashes.sha512 {
        write_one(dir, rel, "sha-512", &sha512_hex(data), &mut written)?;
    }
    if hashes.sha3_512 {
        write_one(dir, rel, "sha3-512", &sha3_512_hex(data), &mut written)?;
    }
    if hashes.blake3_512 {
        write_one(dir, rel, "blake3-512", &blake3_512_hex(data), &mut written)?;
    }
    if hashes.shake256_512 {
        write_one(
            dir,
            rel,
            "shake256-512",
            &shake256_512_hex(data),
            &mut written,
        )?;
    }
    Ok(written)
}

#[cfg(test)]
mod tests {
    #![allow(clippy::missing_panics_doc)] // tests assert; panicking is the failure mode

    use super::{
        blake3_512_hex, sha256_hex, sha3_512_hex, sha512_hex, shake256_512_hex, SidecarHashes,
    };

    #[test]
    fn sha256_known_answers() {
        assert_eq!(
            sha256_hex(b""),
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        );
        assert_eq!(
            sha256_hex(b"abc"),
            "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
        );
    }

    #[test]
    fn sha512_known_answer() {
        assert_eq!(
            sha512_hex(b"abc"),
            "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a\
             2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"
        );
    }

    #[test]
    fn sha3_512_known_answer() {
        // FIPS-202 SHA3-512("") — NIST test vector.
        assert_eq!(
            sha3_512_hex(b""),
            "a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a6\
             15b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
        );
    }

    #[test]
    fn xof_digests_are_64_bytes_and_deterministic() {
        for hex in [blake3_512_hex(b"abc"), shake256_512_hex(b"abc")] {
            assert_eq!(hex.len(), 128, "64-byte XOF => 128 hex chars");
        }
        assert_eq!(blake3_512_hex(b"abc"), blake3_512_hex(b"abc"));
        assert_eq!(shake256_512_hex(b"abc"), shake256_512_hex(b"abc"));
    }

    #[test]
    fn shake256_512_known_answer() {
        // FIPS-202 SHAKE256("", 512 bits) — NIST test vector.
        assert_eq!(
            shake256_512_hex(b""),
            "46b9dd2b0ba88d13233b3feb743eeb243fcd52ea62b81b82b50c27646ed5762f\
             d75dc4ddd8c0f200cb05019d67b592f6fc821c49479ab48640292eacb3b7c4be"
        );
    }

    #[test]
    fn digests_are_distinct() {
        let s256 = sha256_hex(b"abc");
        let s512 = sha512_hex(b"abc");
        let s3 = sha3_512_hex(b"abc");
        let b3 = blake3_512_hex(b"abc");
        let sk = shake256_512_hex(b"abc");
        let all = [&s256, &s512, &s3, &b3, &sk];
        for (i, a) in all.iter().enumerate() {
            for b in all.iter().skip(i.saturating_add(1)) {
                assert_ne!(a, b, "different algorithms must differ");
            }
        }
    }

    #[test]
    fn toggles_and_tokens() {
        assert!(SidecarHashes::all().any());
        assert!(!SidecarHashes::none().any());
        let set = SidecarHashes::from_tokens(["sha-256", "blake3-512", "bogus"]);
        assert!(set.sha256 && set.blake3_512);
        assert!(!set.sha512 && !set.sha3_512 && !set.shake256_512);
    }
}
