#![forbid(unsafe_code)]
use sha2::{Digest, Sha256};
use std::error::Error;
use std::fmt;
pub const SHORT_ARTIFACT_HEX_LEN: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArtifactIdentity {
pub artifact_id: String,
pub sha256: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidSha256;
impl fmt::Display for InvalidSha256 {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("expected exactly 64 hexadecimal SHA-256 characters")
}
}
impl Error for InvalidSha256 {}
fn normalize_sha256(value: &str) -> Result<String, InvalidSha256> {
if value.len() != 64 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return Err(InvalidSha256);
}
Ok(value.to_ascii_lowercase())
}
pub fn sha256_hex(bytes: impl AsRef<[u8]>) -> String {
let digest = Sha256::digest(bytes.as_ref());
format!("{digest:x}")
}
pub fn artifact_identity(content: &str) -> ArtifactIdentity {
let sha256 = sha256_hex(content.as_bytes());
let artifact_id = format!("a_{}", &sha256[..SHORT_ARTIFACT_HEX_LEN]);
ArtifactIdentity {
artifact_id,
sha256,
}
}
pub fn artifact_id_from_sha256(sha256: &str) -> Result<String, InvalidSha256> {
let normalized = normalize_sha256(sha256)?;
Ok(format!("a_{}", &normalized[..SHORT_ARTIFACT_HEX_LEN]))
}
pub fn full_artifact_id_from_sha256(sha256: &str) -> Result<String, InvalidSha256> {
let normalized = normalize_sha256(sha256)?;
Ok(format!("a_{normalized}"))
}
pub fn verify_sha256(content: &str, expected_sha256: &str) -> bool {
normalize_sha256(expected_sha256)
.map(|expected| sha256_hex(content.as_bytes()) == expected)
.unwrap_or(false)
}
pub fn artifact_id_matches(content: &str, artifact_id: &str) -> bool {
let identity = artifact_identity(content);
artifact_id == identity.artifact_id || artifact_id == format!("a_{}", identity.sha256)
}
pub fn strictly_smaller_chars(raw: &str, candidate: &str) -> bool {
candidate.chars().count() < raw.chars().count()
}
#[cfg(test)]
mod tests {
use super::*;
const HELLO_SHA256: &str = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
#[test]
fn identity_matches_python_layout() {
let identity = artifact_identity("hello");
assert_eq!(identity.sha256, HELLO_SHA256);
assert_eq!(identity.artifact_id, "a_2cf24dba5fb0a30e26e83b2ac5b9e29e");
}
#[test]
fn digest_to_short_and_full_ids() {
assert_eq!(
artifact_id_from_sha256(HELLO_SHA256).unwrap(),
"a_2cf24dba5fb0a30e26e83b2ac5b9e29e"
);
assert_eq!(
full_artifact_id_from_sha256(&HELLO_SHA256.to_ascii_uppercase()).unwrap(),
format!("a_{HELLO_SHA256}")
);
}
#[test]
fn malformed_digest_is_rejected() {
assert_eq!(artifact_id_from_sha256("nope"), Err(InvalidSha256));
assert!(!verify_sha256("hello", "nope"));
}
#[test]
fn verification_and_id_matching_work() {
assert!(verify_sha256("hello", HELLO_SHA256));
assert!(artifact_id_matches(
"hello",
"a_2cf24dba5fb0a30e26e83b2ac5b9e29e"
));
assert!(artifact_id_matches("hello", &format!("a_{HELLO_SHA256}")));
assert!(!artifact_id_matches(
"goodbye",
&format!("a_{HELLO_SHA256}")
));
}
#[test]
fn strict_smaller_uses_characters_not_utf8_bytes() {
assert!(strictly_smaller_chars("éé", "é"));
assert!(!strictly_smaller_chars("é", "é"));
assert!(!strictly_smaller_chars("é", "ab"));
}
}