use serde::{Deserialize, Serialize, Serializer};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Sha256DigestError {
#[non_exhaustive]
WrongLength {
got: u32,
},
#[non_exhaustive]
NonHexLowercase {
at: u32,
byte: u8,
},
}
impl fmt::Display for Sha256DigestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WrongLength { got } => {
write!(
f,
"sha256 digest must be exactly 64 lowercase hex chars (got {got})"
)
}
Self::NonHexLowercase { at, byte } => {
write!(
f,
"sha256 digest contains a non-lowercase-hex byte 0x{byte:02x} at byte {at}"
)
}
}
}
}
impl std::error::Error for Sha256DigestError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
#[serde(try_from = "String")]
#[non_exhaustive]
pub struct Sha256(String);
impl Serialize for Sha256 {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&self.0)
}
}
impl Sha256 {
pub fn from_hex(s: &str) -> Result<Self, Sha256DigestError> {
Self::validate(s)?;
Ok(Self(s.to_owned()))
}
fn validate(s: &str) -> Result<(), Sha256DigestError> {
let bytes = s.as_bytes();
if bytes.len() != 64 {
return Err(Sha256DigestError::WrongLength {
got: u32::try_from(bytes.len()).unwrap_or(u32::MAX),
});
}
for (i, b) in bytes.iter().enumerate() {
let ok = b.is_ascii_digit() || (b'a'..=b'f').contains(b);
if !ok {
return Err(Sha256DigestError::NonHexLowercase {
at: u32::try_from(i).unwrap_or(u32::MAX),
byte: *b,
});
}
}
Ok(())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for Sha256 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Sha256 {
fn as_ref(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for Sha256 {
type Error = Sha256DigestError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::validate(&s)?;
Ok(Self(s))
}
}
impl TryFrom<&str> for Sha256 {
type Error = Sha256DigestError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::from_hex(s)
}
}
impl std::str::FromStr for Sha256 {
type Err = Sha256DigestError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hex(s)
}
}
impl From<&[u8; 32]> for Sha256 {
fn from(b: &[u8; 32]) -> Self {
use std::fmt::Write as _;
let mut out = String::with_capacity(64);
for byte in b {
write!(out, "{byte:02x}").expect("write! to String is infallible");
}
Self(out)
}
}
impl From<[u8; 32]> for Sha256 {
fn from(b: [u8; 32]) -> Self {
Self::from(&b)
}
}
impl PartialEq<str> for Sha256 {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Sha256 {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl std::borrow::Borrow<str> for Sha256 {
fn borrow(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
#[test]
fn from_hex_accepts_valid_lowercase_64_chars() {
let d = Sha256::from_hex(VALID).expect("valid digest");
assert_eq!(d.as_str(), VALID);
assert_eq!(format!("{d}"), VALID);
}
#[test]
fn from_hex_rejects_uppercase() {
let upper = VALID.to_ascii_uppercase();
let err = Sha256::from_hex(&upper).expect_err("uppercase rejected");
match err {
Sha256DigestError::NonHexLowercase { at: 0, byte: 0x45 } => {}
other => panic!("expected NonHexLowercase {{ at: 0, byte: 0x45 }}, got {other:?}"),
}
}
#[test]
fn from_hex_rejects_uppercase_mid_string() {
let mut s = String::from(&VALID[..31]);
s.push('A');
s.push_str(&VALID[32..]);
let err = Sha256::from_hex(&s).expect_err("uppercase mid-string rejected");
match err {
Sha256DigestError::NonHexLowercase { at: 31, byte: 0x41 } => {}
other => panic!("expected NonHexLowercase {{ at: 31, byte: 0x41 }}, got {other:?}"),
}
}
#[test]
fn from_hex_rejects_short_length() {
let s = &VALID[..63];
let err = Sha256::from_hex(s).expect_err("63 chars rejected");
assert_eq!(err, Sha256DigestError::WrongLength { got: 63 });
}
#[test]
fn from_hex_rejects_long_length() {
let mut s = String::from(VALID);
s.push('0');
let err = Sha256::from_hex(&s).expect_err("65 chars rejected");
assert_eq!(err, Sha256DigestError::WrongLength { got: 65 });
}
#[test]
fn from_hex_rejects_empty() {
let err = Sha256::from_hex("").expect_err("empty rejected");
assert_eq!(err, Sha256DigestError::WrongLength { got: 0 });
}
#[test]
fn from_hex_rejects_non_hex_character() {
let mut s = String::from(&VALID[..63]);
s.push('g');
let err = Sha256::from_hex(&s).expect_err("non-hex 'g' rejected");
assert_eq!(
err,
Sha256DigestError::NonHexLowercase { at: 63, byte: 0x67 }
);
}
#[test]
fn from_borrowed_array_formats_canonical_lowercase_hex() {
let bytes: [u8; 32] = [
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
0x78, 0x52, 0xb8, 0x55,
];
let d = Sha256::from(&bytes);
assert_eq!(d.as_str(), VALID);
}
#[test]
fn from_owned_array_delegates_to_borrowed_path() {
let bytes: [u8; 32] = [
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f,
0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b,
0x78, 0x52, 0xb8, 0x55,
];
let d: Sha256 = bytes.into();
assert_eq!(d.as_str(), VALID);
assert_eq!(d, Sha256::from(&bytes));
}
#[test]
fn from_array_deadbeef_pattern() {
let mut bytes = [0u8; 32];
bytes[0] = 0xde;
bytes[1] = 0xad;
bytes[2] = 0xbe;
bytes[3] = 0xef;
let d = Sha256::from(&bytes);
assert!(d.as_str().starts_with("deadbeef"));
assert_eq!(d.as_str().len(), 64);
for c in d.as_str().chars().skip(8) {
assert_eq!(c, '0');
}
}
#[test]
fn serialize_emits_bare_hex_string() {
let d = Sha256::from_hex(VALID).unwrap();
let json = serde_json::to_string(&d).unwrap();
assert_eq!(json, format!("\"{VALID}\""));
}
#[test]
fn deserialize_accepts_valid_hex_string() {
let json = format!("\"{VALID}\"");
let d: Sha256 = serde_json::from_str(&json).unwrap();
assert_eq!(d.as_str(), VALID);
}
#[test]
fn deserialize_rejects_uppercase() {
let json = format!("\"{}\"", VALID.to_ascii_uppercase());
let err = serde_json::from_str::<Sha256>(&json)
.expect_err("uppercase digest rejected at deserialize");
let msg = err.to_string();
assert!(
msg.contains("non-lowercase-hex"),
"expected lowercase-hex error, got: {msg}"
);
}
#[test]
fn deserialize_rejects_wrong_length() {
let json = "\"abc\"";
let err =
serde_json::from_str::<Sha256>(json).expect_err("short digest rejected at deserialize");
let msg = err.to_string();
assert!(
msg.contains("64") && msg.contains("got 3"),
"expected wrong-length error mentioning 64 and got 3, got: {msg}"
);
}
#[test]
fn round_trip_through_json_value() {
let d = Sha256::from_hex(VALID).unwrap();
let v: serde_json::Value = serde_json::to_value(&d).unwrap();
assert_eq!(v, serde_json::Value::String(VALID.to_string()));
let d2: Sha256 = serde_json::from_value(v).unwrap();
assert_eq!(d, d2);
}
#[test]
fn into_inner_yields_owned_string() {
let d = Sha256::from_hex(VALID).unwrap();
let s: String = d.into_inner();
assert_eq!(s, VALID);
}
#[test]
fn as_ref_str_borrows_inner() {
let d = Sha256::from_hex(VALID).unwrap();
let s: &str = d.as_ref();
assert_eq!(s, VALID);
}
#[test]
fn from_str_works() {
let d: Sha256 = VALID.parse().unwrap();
assert_eq!(d.as_str(), VALID);
}
#[test]
fn try_from_string_moves_buffer_not_clones() {
let input = VALID.to_owned();
let input_ptr = input.as_ptr();
let d: Sha256 = input.try_into().expect("valid digest");
assert_eq!(
d.as_str().as_ptr(),
input_ptr,
"TryFrom<String> must move the owned buffer; pointer mismatch \
indicates a re-allocation"
);
}
#[test]
fn try_from_string_validates() {
let d: Sha256 = VALID.to_string().try_into().unwrap();
assert_eq!(d.as_str(), VALID);
let err: Result<Sha256, _> = "bogus".to_string().try_into();
assert!(err.is_err());
}
#[test]
fn partial_eq_str_compares_against_string_slice() {
let d = Sha256::from_hex(VALID).unwrap();
assert!(d == *VALID);
assert!(d != *"deadbeef");
}
#[test]
fn partial_eq_ref_str_compares_against_borrowed_slice() {
let d = Sha256::from_hex(VALID).unwrap();
let s: &str = VALID;
assert!(d == s);
let other: &str = "0000000000000000000000000000000000000000000000000000000000000000";
assert!(d != other);
}
#[test]
fn borrow_str_enables_hashmap_lookup_by_str_key() {
use std::borrow::Borrow;
use std::collections::HashMap;
let d = Sha256::from_hex(VALID).unwrap();
let borrowed: &str = d.borrow();
assert_eq!(borrowed, VALID);
let mut m: HashMap<Sha256, &'static str> = HashMap::new();
m.insert(d, "value");
assert_eq!(m.get(VALID), Some(&"value"));
}
#[test]
fn error_display_includes_position_and_byte() {
let err = Sha256DigestError::NonHexLowercase { at: 17, byte: 0x41 };
let msg = err.to_string();
assert!(msg.contains("byte 17"), "{msg}");
assert!(msg.contains("0x41"), "{msg}");
let err = Sha256DigestError::WrongLength { got: 65 };
assert!(err.to_string().contains("65"));
}
}