use std::path::Path;
use sha2::{Digest as Sha2Digest, Sha256};
use sha3::Sha3_512;
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 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))
}
pub fn write_sidecar_files(
json_path: &Path,
data: &[u8],
write_sha256: bool,
write_sha3_512: bool,
) -> Result<()> {
let filename = json_path
.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_default();
if write_sha256 {
let hash = sha256_hex(data);
let sidecar_path = json_path.with_extension("json.sha256");
let content = format!("{hash} {filename}\n");
std::fs::write(&sidecar_path, content)?;
}
if write_sha3_512 {
let hash = sha3_512_hex(data);
let sidecar_path = json_path.with_extension("json.sha3-512");
let content = format!("{hash} {filename}\n");
std::fs::write(&sidecar_path, content)?;
}
Ok(())
}
pub fn write_sidecar_files_for(
file_path: &Path,
data: &[u8],
write_sha256: bool,
write_sha3_512: bool,
) -> Result<(Option<std::path::PathBuf>, Option<std::path::PathBuf>)> {
let filename = file_path
.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_default();
let mut sha256_path = None;
let mut sha3_path = None;
if write_sha256 {
let hash = sha256_hex(data);
let mut sidecar = file_path.as_os_str().to_owned();
sidecar.push(".sha256");
let sidecar_path = std::path::PathBuf::from(sidecar);
let content = format!("{hash} {filename}\n");
std::fs::write(&sidecar_path, content)?;
sha256_path = Some(sidecar_path);
}
if write_sha3_512 {
let hash = sha3_512_hex(data);
let mut sidecar = file_path.as_os_str().to_owned();
sidecar.push(".sha3-512");
let sidecar_path = std::path::PathBuf::from(sidecar);
let content = format!("{hash} {filename}\n");
std::fs::write(&sidecar_path, content)?;
sha3_path = Some(sidecar_path);
}
Ok((sha256_path, sha3_path))
}
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)]
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_sha3_512_known_value() {
let hash = sha3_512_hex(b"");
assert_eq!(
hash,
"a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615\
b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26"
);
}
#[test]
fn test_generate_hashes() {
let (sha256, sha3) = generate_hashes(b"test data");
assert_eq!(sha256.len(), 64); assert_eq!(sha3.len(), 128); }
#[test]
fn test_deterministic() {
let data = b"CSAF document content";
let (h1_256, h1_512) = generate_hashes(data);
let (h2_256, h2_512) = generate_hashes(data);
assert_eq!(h1_256, h2_256);
assert_eq!(h1_512, h2_512);
}
#[test]
fn test_write_sidecar_files() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let json_path = dir.path().join("test.json");
let data = b"{\"test\": true}";
std::fs::write(&json_path, data).expect("write failed");
write_sidecar_files(&json_path, data, true, true).expect("sidecar write failed");
let sha256_path = dir.path().join("test.json.sha256");
let sha3_path = dir.path().join("test.json.sha3-512");
assert!(sha256_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(" ")); }
#[test]
fn test_write_only_sha256() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let json_path = dir.path().join("test.json");
let data = b"{}";
std::fs::write(&json_path, data).expect("write failed");
write_sidecar_files(&json_path, data, true, false).expect("sidecar write failed");
assert!(dir.path().join("test.json.sha256").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 redb_path = dir.path().join("csaf.redb");
let data = b"dummy-redb-bytes";
std::fs::write(&redb_path, data).expect("write failed");
let (s256, s3) =
write_sidecar_files_for(&redb_path, data, true, true).expect("sidecar write failed");
let s256 = s256.expect("sha256 path");
let s3 = s3.expect("sha3 path");
assert_eq!(s256.file_name().unwrap(), "csaf.redb.sha256");
assert_eq!(s3.file_name().unwrap(), "csaf.redb.sha3-512");
assert!(s256.exists());
assert!(s3.exists());
let sha_content = std::fs::read_to_string(&s256).expect("read");
assert!(sha_content.contains("csaf.redb"));
assert!(sha_content.contains(" "));
assert!(sha_content.contains(&sha256_hex(data)));
}
#[test]
fn test_write_sidecar_files_for_skip_one() {
let dir = tempfile::tempdir().expect("tmpdir failed");
let path = dir.path().join("csaf.sqlite");
let data = b"dummy-sqlite";
std::fs::write(&path, data).expect("write failed");
let (s256, s3) = write_sidecar_files_for(&path, data, true, false).expect("sidecar write");
assert!(s256.is_some());
assert!(s3.is_none());
assert!(!dir.path().join("csaf.sqlite.sha3-512").exists());
}
#[test]
fn test_sidecar_matches_csaf_file() {
let json = include_str!("../../../test/csaf/2026/003/ndaal-sa-2026-003.json");
let (sha256, sha3) = generate_hashes(json.as_bytes());
assert_eq!(sha256.len(), 64);
assert_eq!(sha3.len(), 128);
let (sha256_again, sha3_again) = generate_hashes(json.as_bytes());
assert_eq!(sha256, sha256_again);
assert_eq!(sha3, sha3_again);
}
}