use std::fmt::{self, Display, Formatter};
use thiserror::Error;
use crate::codec::{Decode, DecodeError, Encode, EncodeError, decode_bytes_u8, encode_bytes_u8};
#[derive(Debug, Error)]
pub enum Error {
#[error("invalid hash length {length}, must be between 1 and 255 bytes")]
InvalidLength { length: usize },
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct DataHash(Vec<u8>);
impl DataHash {
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
}
impl TryFrom<Vec<u8>> for DataHash {
type Error = Error;
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
if value.is_empty() || value.len() > 255 {
return Err(Error::InvalidLength {
length: value.len(),
});
}
Ok(Self(value))
}
}
impl TryFrom<&[u8]> for DataHash {
type Error = Error;
fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
Self::try_from(value.to_vec())
}
}
impl Display for DataHash {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for byte in &self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
impl Encode for DataHash {
fn encode(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
encode_bytes_u8(&self.0, out)
}
}
impl Decode for DataHash {
fn decode(input: &mut &[u8]) -> Result<Self, DecodeError> {
let bytes = decode_bytes_u8(input)?;
if bytes.is_empty() {
return Err(DecodeError::EmptyHash);
}
Ok(Self(bytes))
}
}
impl Encode for Option<DataHash> {
fn encode(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
match self {
Some(hash) => hash.encode(out),
None => {
out.push(0);
Ok(())
}
}
}
}
impl Decode for Option<DataHash> {
fn decode(input: &mut &[u8]) -> Result<Self, DecodeError> {
let bytes = decode_bytes_u8(input)?;
if bytes.is_empty() {
return Ok(None);
}
Ok(Some(DataHash(bytes)))
}
}
#[cfg(feature = "md5")]
impl From<md5::Digest> for DataHash {
fn from(digest: md5::Digest) -> Self {
Self(digest.0.to_vec())
}
}
#[cfg(feature = "hex")]
impl hex::FromHex for DataHash {
type Error = hex::FromHexError;
fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
let bytes = Vec::from_hex(hex)?;
Self::try_from(bytes).map_err(|_| hex::FromHexError::InvalidStringLength)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for DataHash {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use hex::FromHex;
let hex = String::deserialize(deserializer)?;
DataHash::from_hex(&hex).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for DataHash {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&hex::encode(&self.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::{decode_payload, encode_payload};
#[test]
fn rejects_empty_and_oversized_hashes() {
assert!(matches!(
DataHash::try_from(vec![]).unwrap_err(),
Error::InvalidLength { length: 0 }
));
assert!(matches!(
DataHash::try_from(vec![0; 256]).unwrap_err(),
Error::InvalidLength { length: 256 }
));
}
#[test]
fn hash_is_u8_length_prefixed() {
let hash = DataHash::try_from(vec![1u8; 16]).unwrap();
let mut expected = vec![16];
expected.extend_from_slice(&[1; 16]);
assert_eq!(encode_payload(&hash).unwrap(), expected);
assert_eq!(decode_payload::<DataHash>(&expected).unwrap(), hash);
}
#[test]
fn non_optional_hash_rejects_zero_length() {
let error = decode_payload::<DataHash>(&[0]).unwrap_err();
assert!(matches!(error, DecodeError::EmptyHash));
}
#[test]
fn optional_hash_encodes_none_as_zero_byte() {
assert_eq!(encode_payload(&None::<DataHash>).unwrap(), [0]);
assert_eq!(decode_payload::<Option<DataHash>>(&[0]).unwrap(), None);
let hash = DataHash::try_from(vec![7u8; 4]).unwrap();
let encoded = encode_payload(&Some(hash.clone())).unwrap();
assert_eq!(encoded, [4, 7, 7, 7, 7]);
assert_eq!(
decode_payload::<Option<DataHash>>(&encoded).unwrap(),
Some(hash)
);
}
#[test]
fn displays_as_hex() {
let hash = DataHash::try_from(vec![0xde, 0xad, 0xbe, 0xef]).unwrap();
assert_eq!(hash.to_string(), "deadbeef");
}
#[cfg(feature = "md5")]
#[test]
fn converts_from_md5_digest() {
let hash = DataHash::from(md5::compute(b"bloop"));
assert_eq!(hash.as_bytes().len(), 16);
}
#[cfg(feature = "hex")]
#[test]
fn parses_from_hex() {
use hex::FromHex;
let hash = DataHash::from_hex("deadbeef").unwrap();
assert_eq!(hash.as_bytes(), [0xde, 0xad, 0xbe, 0xef]);
assert!(DataHash::from_hex("").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trips_as_hex_string() {
let hash = DataHash::try_from(vec![0xde, 0xad, 0xbe, 0xef]).unwrap();
let json = serde_json::to_string(&hash).unwrap();
assert_eq!(json, "\"deadbeef\"");
let parsed: DataHash = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, hash);
}
}