use std::fmt;
use sha2::{Digest, Sha256};
use crate::storage::error::StoragePathError;
use crate::storage::filename::Extension;
use crate::storage::path::StoragePath;
pub const DIGEST_HEX_LEN: usize = 64;
#[derive(Clone)]
pub struct ContentHasher {
hasher: Sha256,
bytes: u64,
}
impl ContentHasher {
#[must_use]
pub fn new() -> Self {
Self {
hasher: Sha256::new(),
bytes: 0,
}
}
pub fn update(&mut self, chunk: &[u8]) {
self.hasher.update(chunk);
self.bytes = self.bytes.saturating_add(chunk.len() as u64);
}
#[must_use]
pub fn byte_len(&self) -> u64 {
self.bytes
}
#[must_use]
pub fn finish(self, extension: Extension) -> ContentAddress {
ContentAddress {
digest: hex_lower(&self.hasher.finalize()),
bytes: self.bytes,
extension,
}
}
}
impl Default for ContentHasher {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for ContentHasher {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ContentHasher")
.field("bytes", &self.bytes)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ContentAddress {
digest: String,
bytes: u64,
extension: Extension,
}
impl ContentAddress {
#[must_use]
pub fn of(bytes: &[u8], extension: Extension) -> Self {
let mut hasher = ContentHasher::new();
hasher.update(bytes);
hasher.finish(extension)
}
#[must_use]
pub fn digest(&self) -> &str {
&self.digest
}
#[must_use]
pub fn byte_len(&self) -> u64 {
self.bytes
}
#[must_use]
pub fn extension(&self) -> &Extension {
&self.extension
}
#[must_use]
pub fn path(&self) -> StoragePath {
StoragePath::new(&self.to_string())
.expect("a hex digest and an ASCII-alphanumeric extension are always a valid key")
}
pub fn path_under(&self, prefix: &str) -> Result<StoragePath, StoragePathError> {
let prefix = prefix.trim_end_matches('/');
StoragePath::new(&format!("{prefix}/{self}"))
}
}
impl fmt::Display for ContentAddress {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}/{}/{}.{}",
&self.digest[0..2],
&self.digest[2..4],
self.digest,
self.extension
)
}
}
fn hex_lower(bytes: &[u8]) -> String {
use fmt::Write as _;
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn png() -> Extension {
Extension::parse("png").unwrap()
}
#[test]
fn matches_the_known_sha256_of_abc() {
let address = ContentAddress::of(b"abc", png());
assert_eq!(
address.digest(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(address.digest().len(), DIGEST_HEX_LEN);
}
#[test]
fn hashes_the_empty_object() {
let address = ContentAddress::of(b"", png());
assert_eq!(
address.digest(),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(address.byte_len(), 0);
}
#[test]
fn streaming_matches_one_shot() {
let mut hasher = ContentHasher::new();
for chunk in [&b"a"[..], b"b", b"c"] {
hasher.update(chunk);
}
assert_eq!(hasher.finish(png()), ContentAddress::of(b"abc", png()));
}
#[test]
fn counts_bytes_as_it_goes() {
let mut hasher = ContentHasher::new();
hasher.update(&[0u8; 10]);
assert_eq!(hasher.byte_len(), 10);
hasher.update(&[0u8; 5]);
assert_eq!(hasher.finish(png()).byte_len(), 15);
}
#[test]
fn the_key_fans_out_two_levels() {
let address = ContentAddress::of(b"abc", png());
assert_eq!(
address.path().as_str(),
"ba/78/ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad.png"
);
}
#[test]
fn a_prefix_is_joined_without_a_double_slash() {
let address = ContentAddress::of(b"abc", png());
let under = address.path_under("avatars/").unwrap();
assert!(under.as_str().starts_with("avatars/ba/78/"));
assert_eq!(under, address.path_under("avatars").unwrap());
}
#[test]
fn a_hostile_prefix_is_refused_by_storage_path() {
let address = ContentAddress::of(b"abc", png());
assert!(address.path_under("../../etc").is_err());
assert!(address.path_under("/absolute").is_err());
}
#[test]
fn the_extension_is_normalized_before_it_reaches_the_key() {
let address = ContentAddress::of(b"abc", Extension::parse("PNG").unwrap());
assert!(address.path().as_str().ends_with(".png"));
}
#[test]
fn different_bytes_give_different_keys() {
assert_ne!(
ContentAddress::of(b"a", png()).path(),
ContentAddress::of(b"b", png()).path()
);
}
}