swactor/crates/datastore/src/content_hash.rs
Zachery Aaron Shores-Chmielewski 19fabb707e refactor: consolidate crate functions (#50)
Remove co-dependencies for different modules found in `crates` and migrate the development history to a new repository. The docs were stale, and largely not getting used, so simply deleted for now. When code stabilizes more, they will become useful again.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-24 09:12:28 +00:00

101 lines
2.8 KiB
Rust

//! Blake3-based content hash for blob addressing.
use std::fmt;
use serde::{Deserialize, Serialize};
/// A blake3 content hash (32 bytes).
///
/// The primary identifier for blobs and the DHT key. XOR distance for DHT
/// routing, compact Debug/Display for logging.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContentHash(pub [u8; 32]);
impl ContentHash {
/// Compute the blake3 hash of the given data.
pub fn of(data: &[u8]) -> Self {
let hash = blake3::hash(data);
ContentHash(*hash.as_bytes())
}
/// XOR distance between two content hashes (Kademlia metric).
pub fn xor_distance(&self, other: &ContentHash) -> [u8; 32] {
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = self.0[i] ^ other.0[i];
}
out
}
/// Number of leading zero bits in the XOR distance to `other`.
/// Returns 0..=256. Used to select the k-bucket index in the metadata DHT.
pub fn xor_leading_zeros(&self, other: &ContentHash) -> u32 {
let dist = self.xor_distance(other);
let mut zeros = 0u32;
for byte in dist {
if byte == 0 {
zeros += 8;
} else {
zeros += byte.leading_zeros();
break;
}
}
zeros
}
/// Parse a 64-character hex string into a ContentHash.
/// Returns `None` if the string is not exactly 64 hex characters.
pub fn from_hex(hex: &str) -> Option<Self> {
if hex.len() != 64 {
return None;
}
let mut bytes = [0u8; 32];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let hi = hex_digit(chunk[0])?;
let lo = hex_digit(chunk[1])?;
bytes[i] = (hi << 4) | lo;
}
Some(ContentHash(bytes))
}
/// Encode as lowercase hex string.
pub fn to_hex(&self) -> String {
let mut s = String::with_capacity(64);
for b in &self.0 {
use fmt::Write;
write!(s, "{:02x}", b).unwrap();
}
s
}
/// The zero hash (all zeroes). Used as a sentinel.
pub const ZERO: ContentHash = ContentHash([0u8; 32]);
}
impl fmt::Debug for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Hash(")?;
for b in &self.0[..4] {
write!(f, "{:02x}", b)?;
}
write!(f, "\u{2026})")
}
}
impl fmt::Display for ContentHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in &self.0[..8] {
write!(f, "{:02x}", b)?;
}
write!(f, "\u{2026}")
}
}
fn hex_digit(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}