use std::fmt;
use base64::{Engine, engine::general_purpose::STANDARD};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sha2::{Digest, Sha256};
use crate::error::TransparencyError;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct MerkleHash([u8; 32]);
impl MerkleHash {
pub const EMPTY: Self = Self([0u8; 32]);
pub fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn from_hex(s: &str) -> Result<Self, TransparencyError> {
let bytes = hex::decode(s).map_err(|e| TransparencyError::InvalidProof(e.to_string()))?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| TransparencyError::InvalidProof("hash must be 32 bytes".into()))?;
Ok(Self(arr))
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_base64(&self) -> String {
STANDARD.encode(self.0)
}
pub fn from_base64(s: &str) -> Result<Self, TransparencyError> {
let bytes = STANDARD
.decode(s)
.map_err(|e| TransparencyError::InvalidProof(format!("base64 decode: {e}")))?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|_| TransparencyError::InvalidProof("hash must be 32 bytes".into()))?;
Ok(Self(arr))
}
pub fn sha256(data: &[u8]) -> Self {
let digest = Sha256::digest(data);
let mut out = [0u8; 32];
out.copy_from_slice(&digest);
Self(out)
}
}
impl fmt::Debug for MerkleHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MerkleHash({})", self)
}
}
impl fmt::Display for MerkleHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in &self.0 {
write!(f, "{b:02x}")?;
}
Ok(())
}
}
impl Serialize for MerkleHash {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for MerkleHash {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::from_hex(&s).map_err(serde::de::Error::custom)
}
}
impl AsRef<[u8]> for MerkleHash {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct LogOrigin(String);
impl LogOrigin {
pub fn new(s: &str) -> Result<Self, TransparencyError> {
if s.is_empty() {
return Err(TransparencyError::InvalidOrigin("must not be empty".into()));
}
if !s.is_ascii() {
return Err(TransparencyError::InvalidOrigin("must be ASCII".into()));
}
if s.bytes().any(|b| b < 0x20) {
return Err(TransparencyError::InvalidOrigin(
"must not contain control characters".into(),
));
}
Ok(Self(s.to_string()))
}
#[allow(clippy::expect_used)] pub fn new_unchecked(s: &str) -> Self {
Self::new(s).expect("LogOrigin::new_unchecked called with invalid origin")
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for LogOrigin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl TryFrom<String> for LogOrigin {
type Error = TransparencyError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::new(&s)
}
}
impl From<LogOrigin> for String {
fn from(o: LogOrigin) -> Self {
o.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merkle_hash_hex_roundtrip() {
let bytes = [0xabu8; 32];
let h = MerkleHash::from_bytes(bytes);
let hex_str = h.to_string();
let h2 = MerkleHash::from_hex(&hex_str).unwrap();
assert_eq!(h, h2);
}
#[test]
fn merkle_hash_json_roundtrip() {
let h = MerkleHash::from_bytes([0x42u8; 32]);
let json = serde_json::to_string(&h).unwrap();
let h2: MerkleHash = serde_json::from_str(&json).unwrap();
assert_eq!(h, h2);
}
#[test]
fn log_origin_rejects_empty() {
assert!(LogOrigin::new("").is_err());
}
#[test]
fn log_origin_rejects_non_ascii() {
assert!(LogOrigin::new("日本語").is_err());
}
#[test]
fn log_origin_rejects_control_chars() {
assert!(LogOrigin::new("auths\x00log").is_err());
}
#[test]
fn log_origin_valid() {
let o = LogOrigin::new("auths.dev/log").unwrap();
assert_eq!(o.as_str(), "auths.dev/log");
}
}