use csaf_models::settings::Settings;
use sha2::{Digest as Sha2Digest, Sha256, Sha512};
use sha3::Sha3_512;
use crate::fs::DataDir;
#[doc(hidden)]
pub use generic_array::typenum::Unsigned as _GenericArrayUnsigned;
use crate::error::Result;
#[must_use]
pub fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
hex_encode(&result)
}
#[must_use]
pub fn sha512_hex(data: &[u8]) -> String {
let mut hasher = Sha512::new();
hasher.update(data);
let result = hasher.finalize();
hex_encode(&result)
}
#[must_use]
pub fn sha3_512_hex(data: &[u8]) -> String {
let mut hasher = Sha3_512::new();
hasher.update(data);
let result = hasher.finalize();
hex_encode(&result)
}
#[must_use]
pub fn generate_hashes(data: &[u8]) -> (String, String) {
(sha256_hex(data), sha3_512_hex(data))
}
#[must_use]
pub fn generate_all_hashes(data: &[u8]) -> (String, String, String) {
(sha256_hex(data), sha512_hex(data), sha3_512_hex(data))
}
#[derive(Debug, Clone, Copy)]
pub struct SidecarHashes {
pub sha256: bool,
pub sha512: bool,
pub sha3_512: bool,
}
impl SidecarHashes {
#[must_use]
pub const fn from_settings(settings: &Settings) -> Self {
Self {
sha256: settings.sidecar_sha256,
sha512: settings.sidecar_sha512,
sha3_512: settings.sidecar_sha3_512,
}
}
}
pub fn write_sidecar_files(
dir: &DataDir,
rel: &str,
data: &[u8],
hashes: SidecarHashes,
) -> Result<()> {
let filename = rel.rsplit('/').next().unwrap_or(rel);
if hashes.sha256 {
dir.write(
&format!("{rel}.sha-256"),
format!("{} {filename}\n", sha256_hex(data)).as_bytes(),
)?;
}
if hashes.sha512 {
dir.write(
&format!("{rel}.sha-512"),
format!("{} {filename}\n", sha512_hex(data)).as_bytes(),
)?;
}
if hashes.sha3_512 {
dir.write(
&format!("{rel}.sha3-512"),
format!("{} {filename}\n", sha3_512_hex(data)).as_bytes(),
)?;
}
Ok(())
}
pub fn write_sidecar_files_for(
dir: &DataDir,
rel: &str,
data: &[u8],
hashes: SidecarHashes,
) -> Result<Vec<String>> {
let filename = rel.rsplit('/').next().unwrap_or(rel);
let mut written = Vec::new();
if hashes.sha256 {
let sidecar = format!("{rel}.sha-256");
dir.write(
&sidecar,
format!("{} {filename}\n", sha256_hex(data)).as_bytes(),
)?;
written.push(sidecar);
}
if hashes.sha512 {
let sidecar = format!("{rel}.sha-512");
dir.write(
&sidecar,
format!("{} {filename}\n", sha512_hex(data)).as_bytes(),
)?;
written.push(sidecar);
}
if hashes.sha3_512 {
let sidecar = format!("{rel}.sha3-512");
dir.write(
&sidecar,
format!("{} {filename}\n", sha3_512_hex(data)).as_bytes(),
)?;
written.push(sidecar);
}
Ok(written)
}
fn hex_encode(bytes: &[u8]) -> String {
use std::fmt::Write as _;
let mut hex = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(hex, "{b:02x}");
}
hex
}
#[cfg(test)]
#[allow(clippy::case_sensitive_file_extension_comparisons)]
mod tests {
use super::*;
#[test]
fn test_sha256_known_value() {
let hash = sha256_hex(b"");
assert_eq!(
hash,
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn test_sha256_hello() {
let hash = sha256_hex(b"hello");
assert_eq!(
hash,
"2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
);
}
#[test]
fn test_sha512_known_value() {
let hash = sha512_hex(b"");
assert_eq!(
hash,
"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce\
47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
);
}
#[test]
fn test_sha512_hello() {
let hash = sha512_hex(b"hello");
assert_eq!(
hash,
"9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca7\
2323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043"
);
}
#[test]
fn test_sha3_512_known_value() {
let hash = sha3_512_hex(b"");
assert_eq!(
hash,
"a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615\
b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
);
}
#[test]
fn test_generate_hashes_legacy() {
let (sha256, sha3) = generate_hashes(b"test data");
assert_eq!(sha256.len(), 64); assert_eq!(sha3.len(), 128); }
#[test]
fn test_generate_all_hashes_triplet() {
let (sha256, sha512, sha3) = generate_all_hashes(b"test data");
assert_eq!(sha256.len(), 64);
assert_eq!(sha512.len(), 128);
assert_eq!(sha3.len(), 128);
assert_ne!(sha256, sha512);
assert_ne!(sha512, sha3);
assert_ne!(sha256, sha3);
}
#[test]
fn test_deterministic() {
let data = b"CSAF document content";
let (h1_256, h1_512, h1_3) = generate_all_hashes(data);
let (h2_256, h2_512, h2_3) = generate_all_hashes(data);
assert_eq!(h1_256, h2_256);
assert_eq!(h1_512, h2_512);
assert_eq!(h1_3, h2_3);
}
#[test]
fn test_write_sidecar_files() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let data = b"{\"test\": true}";
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("test.json", data).expect("write json");
write_sidecar_files(
&dd,
"test.json",
data,
SidecarHashes {
sha256: true,
sha512: true,
sha3_512: true,
},
)
.expect("sidecar write failed");
let sha256_path = dir.path().join("test.json.sha-256");
let sha512_path = dir.path().join("test.json.sha-512");
let sha3_path = dir.path().join("test.json.sha3-512");
assert!(sha256_path.exists());
assert!(sha512_path.exists());
assert!(sha3_path.exists());
let sha256_content = std::fs::read_to_string(&sha256_path).expect("read failed");
assert!(sha256_content.contains("test.json"));
assert!(sha256_content.contains(" "));
assert!(!dir.path().join("test.json.sha256").exists());
assert!(!dir.path().join("test.json.sha512").exists());
}
#[test]
fn test_write_only_sha256() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let data = b"{}";
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("test.json", data).expect("write json");
write_sidecar_files(
&dd,
"test.json",
data,
SidecarHashes {
sha256: true,
sha512: false,
sha3_512: false,
},
)
.expect("sidecar write failed");
assert!(dir.path().join("test.json.sha-256").exists());
assert!(!dir.path().join("test.json.sha-512").exists());
assert!(!dir.path().join("test.json.sha3-512").exists());
}
#[test]
fn test_write_only_sha512() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let data = b"{}";
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("test.json", data).expect("write json");
write_sidecar_files(
&dd,
"test.json",
data,
SidecarHashes {
sha256: false,
sha512: true,
sha3_512: false,
},
)
.expect("sidecar write failed");
assert!(!dir.path().join("test.json.sha-256").exists());
assert!(dir.path().join("test.json.sha-512").exists());
assert!(!dir.path().join("test.json.sha3-512").exists());
}
#[test]
fn test_write_sidecar_files_for_redb() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let data = b"dummy-redb-bytes";
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("csaf.redb", data).expect("write redb");
let written = write_sidecar_files_for(
&dd,
"csaf.redb",
data,
SidecarHashes {
sha256: true,
sha512: true,
sha3_512: true,
},
)
.expect("sidecar write failed");
assert_eq!(
written,
vec![
"csaf.redb.sha-256".to_string(),
"csaf.redb.sha-512".to_string(),
"csaf.redb.sha3-512".to_string(),
]
);
assert!(dir.path().join("csaf.redb.sha-256").exists());
assert!(dir.path().join("csaf.redb.sha-512").exists());
assert!(dir.path().join("csaf.redb.sha3-512").exists());
let sha_content =
std::fs::read_to_string(dir.path().join("csaf.redb.sha-256")).expect("read");
assert!(sha_content.contains("csaf.redb"));
assert!(sha_content.contains(" "));
assert!(sha_content.contains(&sha256_hex(data)));
let sha512_content =
std::fs::read_to_string(dir.path().join("csaf.redb.sha-512")).expect("read");
assert!(sha512_content.contains(&sha512_hex(data)));
}
#[test]
fn test_write_sidecar_files_for_skip_sha3() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let data = b"dummy-sqlite";
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("csaf.sqlite", data).expect("write sqlite");
let written = write_sidecar_files_for(
&dd,
"csaf.sqlite",
data,
SidecarHashes {
sha256: true,
sha512: true,
sha3_512: false,
},
)
.expect("sidecar write");
assert_eq!(
written,
vec![
"csaf.sqlite.sha-256".to_string(),
"csaf.sqlite.sha-512".to_string(),
]
);
assert!(!dir.path().join("csaf.sqlite.sha3-512").exists());
}
#[test]
fn test_write_sidecar_files_for_skip_all_but_sha3() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let data = b"x";
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("evidence.bin", data).expect("write evidence");
let written = write_sidecar_files_for(
&dd,
"evidence.bin",
data,
SidecarHashes {
sha256: false,
sha512: false,
sha3_512: true,
},
)
.expect("sidecar write");
assert_eq!(written, vec!["evidence.bin.sha3-512".to_string()]);
assert!(dir.path().join("evidence.bin.sha3-512").exists());
}
#[test]
fn test_extension_uses_hyphenated_form_only() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let dd = DataDir::open(dir.path()).expect("open base");
dd.write("payload.json", b"payload").expect("write json");
write_sidecar_files(
&dd,
"payload.json",
b"payload",
SidecarHashes {
sha256: true,
sha512: true,
sha3_512: true,
},
)
.expect("sidecar write");
for entry in std::fs::read_dir(dir.path()).expect("readdir") {
let name = entry.unwrap().file_name().to_string_lossy().to_string();
assert!(
name.ends_with(".json")
|| name.ends_with(".json.sha-256")
|| name.ends_with(".json.sha-512")
|| name.ends_with(".json.sha3-512"),
"unexpected sidecar extension: {name}"
);
assert!(
!(name.ends_with(".sha256") || name.ends_with(".sha512")),
"legacy unhyphenated form leaked: {name}"
);
}
}
#[test]
fn test_sidecar_matches_csaf_file() {
let json = include_str!("../../../test/csaf/2026/003/ndaal-sa-2026-003.json");
let (sha256, sha512, sha3) = generate_all_hashes(json.as_bytes());
assert_eq!(sha256.len(), 64);
assert_eq!(sha512.len(), 128);
assert_eq!(sha3.len(), 128);
let (sha256_again, sha512_again, sha3_again) = generate_all_hashes(json.as_bytes());
assert_eq!(sha256, sha256_again);
assert_eq!(sha512, sha512_again);
assert_eq!(sha3, sha3_again);
}
}