use crate::types::{StoredHashes, parse_hex_array};
use crate::{EwfError, Result};
pub(crate) fn canonical_identifier(identifier: &str) -> Option<&'static str> {
["MD5", "SHA1", "SHA256"]
.into_iter()
.find(|candidate| identifier.eq_ignore_ascii_case(candidate))
}
pub(crate) fn validate_digest(identifier: &str, value: &str) -> Result<()> {
let valid = match canonical_identifier(identifier) {
Some("MD5") => parse_hex_array::<16>(value).is_some(),
Some("SHA1") => parse_hex_array::<20>(value).is_some(),
Some("SHA256") => parse_hex_array::<32>(value).is_some(),
_ => true,
};
if !valid {
return Err(EwfError::Malformed(format!(
"invalid {identifier} digest: expected hexadecimal of the required length"
)));
}
Ok(())
}
pub(crate) fn insert_stored_hash(
hashes: &mut StoredHashes,
identifier: &str,
value: &str,
) -> Result<()> {
let Some(identifier) = canonical_identifier(identifier) else {
hashes
.hash_values
.entry(identifier.into())
.or_insert_with(|| value.into());
return Ok(());
};
validate_digest(identifier, value)?;
if hashes
.hash_values
.get(identifier)
.is_some_and(|old| !old.eq_ignore_ascii_case(value))
{
return Err(EwfError::Malformed(format!(
"conflicting stored {identifier} digests"
)));
}
match identifier {
"MD5" => merge_typed(&mut hashes.md5, parse_hex_array(value), identifier)?,
"SHA1" => merge_typed(&mut hashes.sha1, parse_hex_array(value), identifier)?,
_ => {}
}
hashes
.hash_values
.entry(identifier.into())
.or_insert_with(|| value.into());
Ok(())
}
fn merge_typed<const N: usize>(
target: &mut Option<[u8; N]>,
source: Option<[u8; N]>,
identifier: &str,
) -> Result<()> {
if target.is_some() && *target != source {
return Err(EwfError::Malformed(format!(
"conflicting stored {identifier} digests"
)));
}
*target = source;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metadata::parse_xhash_data;
#[test]
fn supported_xhashes_reject_invalid_empty_and_conflicting_values() {
for value in ["", "abc", &"g".repeat(64)] {
let xml = format!("<xhash><SHA256>{value}</SHA256></xhash>");
assert!(parse_xhash_data(xml.as_bytes(), &mut StoredHashes::default()).is_err());
}
let xml = format!(
"<xhash><Sha256>{}</Sha256><sHa256>{}</sHa256></xhash>",
"a".repeat(64),
"b".repeat(64)
);
assert!(parse_xhash_data(xml.as_bytes(), &mut StoredHashes::default()).is_err());
}
#[test]
fn equivalent_references_are_accepted_and_binary_conflicts_rejected() {
let mut hashes = StoredHashes {
md5: Some([0xaa; 16]),
..StoredHashes::default()
};
insert_stored_hash(&mut hashes, "mD5", &"AA".repeat(16)).unwrap();
insert_stored_hash(&mut hashes, "MD5", &"aa".repeat(16)).unwrap();
assert!(insert_stored_hash(&mut hashes, "MD5", &"bb".repeat(16)).is_err());
assert_eq!(hashes.md5, Some([0xaa; 16]));
let mut typed_only = StoredHashes {
sha1: Some([0xaa; 20]),
..StoredHashes::default()
};
assert!(insert_stored_hash(&mut typed_only, "SHA1", &"bb".repeat(20)).is_err());
}
}