use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum HashAlgorithm {
Sha256,
Blake3,
}
impl HashAlgorithm {
pub fn prefix(&self) -> &'static str {
match self {
HashAlgorithm::Sha256 => "sha256",
HashAlgorithm::Blake3 => "blake3",
}
}
pub fn hash_len(&self) -> usize {
match self {
HashAlgorithm::Sha256 => 32,
HashAlgorithm::Blake3 => 32,
}
}
pub fn hex_len(&self) -> usize {
self.hash_len() * 2
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdError {
InvalidFormat(String),
InvalidLength { expected: usize, got: usize },
UnknownAlgorithm(String),
InvalidHex(String),
}
impl std::fmt::Display for IdError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IdError::InvalidFormat(s) => write!(f, "Invalid ID format: {s}"),
IdError::InvalidLength { expected, got } => {
write!(f, "Invalid ID length: expected {expected}, got {got}")
}
IdError::UnknownAlgorithm(alg) => write!(f, "Unknown hash algorithm: {alg}"),
IdError::InvalidHex(s) => write!(f, "Invalid hex characters: {s}"),
}
}
}
impl std::error::Error for IdError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ID {
repr: String,
algorithm: HashAlgorithm,
}
impl Default for ID {
fn default() -> Self {
Self {
repr: String::new(),
algorithm: HashAlgorithm::Sha256,
}
}
}
impl ID {
pub fn new(s: impl Into<String>) -> Self {
let repr = s.into();
let algorithm = Self::detect_algorithm(&repr);
Self { repr, algorithm }
}
pub fn from_bytes(data: impl AsRef<[u8]>) -> Self {
Self::from_bytes_with(data, HashAlgorithm::Sha256)
}
pub fn from_bytes_with(data: impl AsRef<[u8]>, algorithm: HashAlgorithm) -> Self {
let data = data.as_ref();
let hash_bytes = match algorithm {
HashAlgorithm::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
HashAlgorithm::Blake3 => blake3::hash(data).as_bytes().to_vec(),
};
let hex = hex::encode(&hash_bytes);
let repr = format!("{}:{}", algorithm.prefix(), hex);
Self { repr, algorithm }
}
pub fn parse(s: &str) -> Result<Self, IdError> {
if s.is_empty() {
return Ok(Self::default());
}
let Some(colon_pos) = s.find(':') else {
return Err(IdError::InvalidFormat(
"ID must have algorithm prefix (e.g., 'sha256:' or 'blake3:')".to_string(),
));
};
let (prefix, hex_part) = s.split_at(colon_pos);
let hex_part = &hex_part[1..];
let algorithm = match prefix {
"sha256" => HashAlgorithm::Sha256,
"blake3" => HashAlgorithm::Blake3,
_ => return Err(IdError::UnknownAlgorithm(prefix.to_string())),
};
Self::validate_hex_format(hex_part, algorithm)?;
Ok(Self {
repr: s.to_string(),
algorithm,
})
}
fn validate_hex_format(hex: &str, algorithm: HashAlgorithm) -> Result<(), IdError> {
let expected_len = algorithm.hex_len();
if hex.len() != expected_len {
return Err(IdError::InvalidLength {
expected: expected_len,
got: hex.len(),
});
}
if !hex
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
{
return Err(IdError::InvalidHex(hex.to_string()));
}
Ok(())
}
fn detect_algorithm(s: &str) -> HashAlgorithm {
if let Some(colon_pos) = s.find(':') {
let prefix = &s[..colon_pos];
match prefix {
"blake3" => HashAlgorithm::Blake3,
_ => HashAlgorithm::Sha256, }
} else {
HashAlgorithm::Sha256 }
}
pub fn as_str(&self) -> &str {
&self.repr
}
pub fn is_empty(&self) -> bool {
self.repr.is_empty()
}
pub fn algorithm(&self) -> HashAlgorithm {
self.algorithm
}
pub fn hex(&self) -> &str {
if let Some(colon_pos) = self.repr.find(':') {
&self.repr[colon_pos + 1..]
} else {
&self.repr
}
}
pub fn as_bytes(&self) -> Result<Vec<u8>, hex::FromHexError> {
hex::decode(self.hex())
}
}
impl From<String> for ID {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for ID {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl From<&ID> for ID {
fn from(id: &ID) -> Self {
id.clone()
}
}
impl AsRef<str> for ID {
fn as_ref(&self) -> &str {
&self.repr
}
}
impl std::fmt::Display for ID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", &self.repr)
}
}
impl std::ops::Deref for ID {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.repr
}
}
impl PartialEq<str> for ID {
fn eq(&self, other: &str) -> bool {
self.repr == other
}
}
impl PartialEq<&str> for ID {
fn eq(&self, other: &&str) -> bool {
self.repr == *other
}
}
impl PartialEq<String> for ID {
fn eq(&self, other: &String) -> bool {
&self.repr == other
}
}
impl PartialEq<ID> for str {
fn eq(&self, other: &ID) -> bool {
self == other.repr
}
}
impl PartialEq<ID> for &str {
fn eq(&self, other: &ID) -> bool {
*self == other.repr
}
}
impl PartialEq<ID> for String {
fn eq(&self, other: &ID) -> bool {
self == &other.repr
}
}
impl From<ID> for String {
fn from(id: ID) -> Self {
id.repr
}
}
impl PartialEq<&ID> for ID {
fn eq(&self, other: &&ID) -> bool {
self == *other
}
}
impl From<&ID> for String {
fn from(id: &ID) -> Self {
id.repr.clone()
}
}
impl Serialize for ID {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.repr.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ID {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Ok(Self::new(s))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sha256_prefixed_format() {
let data = b"hello world";
let id = ID::from_bytes(data);
assert!(id.as_str().starts_with("sha256:"));
assert_eq!(id.algorithm(), HashAlgorithm::Sha256);
assert_eq!(id.as_str().len(), 71); }
#[test]
fn test_blake3_prefixed_format() {
let data = b"hello world";
let id = ID::from_bytes_with(data, HashAlgorithm::Blake3);
assert!(id.as_str().starts_with("blake3:"));
assert_eq!(id.algorithm(), HashAlgorithm::Blake3);
}
#[test]
fn test_parse_sha256_prefixed() {
let hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
let prefixed = format!("sha256:{hex}");
let id = ID::parse(&prefixed).unwrap();
assert_eq!(id.algorithm(), HashAlgorithm::Sha256);
assert_eq!(id.hex(), hex);
assert_eq!(id.as_str(), prefixed);
}
#[test]
fn test_parse_prefixed_blake3() {
let hex = "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262";
let prefixed = format!("blake3:{hex}");
let id = ID::parse(&prefixed).unwrap();
assert_eq!(id.algorithm(), HashAlgorithm::Blake3);
assert_eq!(id.hex(), hex);
assert_eq!(id.as_str(), prefixed);
}
#[test]
fn test_from_bytes_deterministic() {
let id1 = ID::from_bytes("test_data_foo");
let id2 = ID::from_bytes("test_data_foo");
let id3 = ID::from_bytes("test_data_bar");
assert_eq!(id1, id2);
assert_ne!(id1, id3);
assert_eq!(id1.algorithm(), HashAlgorithm::Sha256);
}
#[test]
fn test_validation() {
assert!(ID::parse("deadbeef").is_err());
assert!(
ID::parse("deadbeef12345678901234567890123456789012345678901234567890123456").is_err()
);
assert!(
ID::parse("sha256:deadbeef123456789012345678901234567890123456789012345678901234567g")
.is_err()
);
assert!(
ID::parse("unknown:deadbeef12345678901234567890123456789012345678901234567890123456")
.is_err()
);
assert!(
ID::parse("sha256:deadbeef12345678901234567890123456789012345678901234567890123456")
.is_ok()
);
assert!(
ID::parse("blake3:deadbeef12345678901234567890123456789012345678901234567890123456")
.is_ok()
);
}
#[test]
fn test_serialization() {
let id = ID::from_bytes("test_data_serialization");
let json = serde_json::to_string(&id).unwrap();
let deserialized: ID = serde_json::from_str(&json).unwrap();
assert_eq!(id, deserialized);
assert_eq!(id.algorithm(), deserialized.algorithm());
}
}