use std::fmt;
use std::io::{self, Read};
use std::path::Path;
#[cfg(feature = "hash-sha256")]
mod sha256;
#[cfg(feature = "hash-sha256")]
pub use sha256::{sha256_bytes, sha256_file, sha256_reader, Sha256Digest, Sha256Hasher};
#[cfg(feature = "fs")]
mod tree;
#[cfg(feature = "fs")]
pub use tree::{blake3_tree, TreeHashOptions};
pub const BLAKE3_DIGEST_LENGTH: usize = 32;
const READ_BUFFER_LENGTH: usize = 64 * 1024;
const PARALLEL_HASH_THRESHOLD_BYTES: u64 = 128 * 1024;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Blake3Digest([u8; BLAKE3_DIGEST_LENGTH]);
impl Blake3Digest {
pub const fn from_bytes(bytes: [u8; BLAKE3_DIGEST_LENGTH]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; BLAKE3_DIGEST_LENGTH] {
&self.0
}
pub fn to_hex(self) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut rendered = String::with_capacity(BLAKE3_DIGEST_LENGTH * 2);
for byte in self.0 {
rendered.push(HEX[usize::from(byte >> 4)] as char);
rendered.push(HEX[usize::from(byte & 0x0f)] as char);
}
rendered
}
pub fn from_hex(hex: &str) -> Result<Self, Blake3HexDecodeError> {
let hex = hex.as_bytes();
if hex.len() != BLAKE3_DIGEST_LENGTH * 2 {
return Err(Blake3HexDecodeError::InvalidLength { length: hex.len() });
}
let mut bytes = [0_u8; BLAKE3_DIGEST_LENGTH];
for (target, pair) in bytes.iter_mut().zip(hex.chunks_exact(2)) {
let high = hex_digit(pair[0]).ok_or(Blake3HexDecodeError::InvalidCharacter)?;
let low = hex_digit(pair[1]).ok_or(Blake3HexDecodeError::InvalidCharacter)?;
*target = (high << 4) | low;
}
Ok(Self(bytes))
}
}
fn hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
impl fmt::Display for Blake3Digest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.to_hex())
}
}
impl std::str::FromStr for Blake3Digest {
type Err = Blake3HexDecodeError;
fn from_str(hex: &str) -> Result<Self, Self::Err> {
Self::from_hex(hex)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Blake3HexDecodeError {
InvalidLength {
length: usize,
},
InvalidCharacter,
}
impl fmt::Display for Blake3HexDecodeError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength { length } => {
write!(
formatter,
"BLAKE3 hex digest must be {} characters, got {length}",
BLAKE3_DIGEST_LENGTH * 2
)
}
Self::InvalidCharacter => {
formatter.write_str("BLAKE3 hex digest contained a non-hexadecimal character")
}
}
}
}
impl std::error::Error for Blake3HexDecodeError {}
pub fn blake3_bytes(bytes: &[u8]) -> Blake3Digest {
Blake3Digest(*blake3::hash(bytes).as_bytes())
}
#[derive(Clone)]
pub struct Blake3Hasher(blake3::Hasher);
impl Blake3Hasher {
pub fn new() -> Self {
Self(blake3::Hasher::new())
}
pub fn new_derive_key(context: &str) -> Self {
Self(blake3::Hasher::new_derive_key(context))
}
pub(crate) fn update_parallel(&mut self, bytes: &[u8]) -> &mut Self {
self.0.update_rayon(bytes);
self
}
pub fn update(&mut self, bytes: &[u8]) -> &mut Self {
self.0.update(bytes);
self
}
pub fn finalize(&self) -> Blake3Digest {
Blake3Digest(*self.0.finalize().as_bytes())
}
}
impl Default for Blake3Hasher {
fn default() -> Self {
Self::new()
}
}
impl fmt::Debug for Blake3Hasher {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("Blake3Hasher")
.finish_non_exhaustive()
}
}
impl io::Write for Blake3Hasher {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.update(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct Blake3ReadOptions {
maximum_bytes: Option<u64>,
memory_map: bool,
}
impl Blake3ReadOptions {
pub const fn new() -> Self {
Self {
maximum_bytes: None,
memory_map: false,
}
}
pub const fn maximum_bytes(mut self, maximum_bytes: u64) -> Self {
self.maximum_bytes = Some(maximum_bytes);
self
}
pub const fn maximum_byte_count(self) -> Option<u64> {
self.maximum_bytes
}
pub const fn memory_map(mut self, memory_map: bool) -> Self {
self.memory_map = memory_map;
self
}
pub const fn memory_map_requested(self) -> bool {
self.memory_map
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Blake3HashErrorKind {
Open,
Read,
SizeLimitExceeded {
maximum_bytes: u64,
},
}
#[derive(Debug)]
pub struct Blake3HashError {
kind: Blake3HashErrorKind,
source: Option<io::Error>,
}
impl Blake3HashError {
fn open(source: io::Error) -> Self {
Self {
kind: Blake3HashErrorKind::Open,
source: Some(source),
}
}
fn read(source: io::Error) -> Self {
Self {
kind: Blake3HashErrorKind::Read,
source: Some(source),
}
}
fn size_limit_exceeded(maximum_bytes: u64) -> Self {
Self {
kind: Blake3HashErrorKind::SizeLimitExceeded { maximum_bytes },
source: None,
}
}
pub const fn kind(&self) -> Blake3HashErrorKind {
self.kind
}
pub fn io_error_kind(&self) -> Option<io::ErrorKind> {
self.source.as_ref().map(io::Error::kind)
}
}
impl fmt::Display for Blake3HashError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
Blake3HashErrorKind::Open => formatter.write_str("failed to open BLAKE3 input"),
Blake3HashErrorKind::Read => formatter.write_str("failed to read BLAKE3 input"),
Blake3HashErrorKind::SizeLimitExceeded { maximum_bytes } => {
write!(
formatter,
"BLAKE3 input exceeds the {maximum_bytes}-byte limit"
)
}
}
}
}
impl std::error::Error for Blake3HashError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|source| source as &(dyn std::error::Error + 'static))
}
}
pub fn blake3_reader(
mut reader: impl Read,
options: Blake3ReadOptions,
) -> Result<Blake3Digest, Blake3HashError> {
let mut hasher = Blake3Hasher::new();
let mut buffer = [0_u8; READ_BUFFER_LENGTH];
let mut bytes_hashed = 0_u64;
loop {
let read_length = next_read_length(options.maximum_bytes, bytes_hashed);
let read = reader
.read(&mut buffer[..read_length])
.map_err(Blake3HashError::read)?;
if read == 0 {
return Ok(hasher.finalize());
}
let read = u64::try_from(read).expect("buffer length always fits in u64");
if let Some(maximum_bytes) = options.maximum_bytes {
if read > maximum_bytes.saturating_sub(bytes_hashed) {
return Err(Blake3HashError::size_limit_exceeded(maximum_bytes));
}
bytes_hashed += read;
}
hasher.update(&buffer[..usize::try_from(read).expect("read length fits usize")]);
}
}
pub fn blake3_file(
path: impl AsRef<Path>,
options: Blake3ReadOptions,
) -> Result<Blake3Digest, Blake3HashError> {
let file = std::fs::File::open(path).map_err(Blake3HashError::open)?;
let mapped_digest = if options.memory_map {
blake3_file_memory_mapped(&file, options)?
} else {
None
};
match mapped_digest {
Some(digest) => Ok(digest),
None => blake3_reader(file, options),
}
}
pub fn blake3_open_file(
file: &std::fs::File,
options: Blake3ReadOptions,
) -> Result<Blake3Digest, Blake3HashError> {
let mapped_digest = if options.memory_map {
blake3_file_memory_mapped(file, options)?
} else {
None
};
match mapped_digest {
Some(digest) => Ok(digest),
None => blake3_reader(file, options),
}
}
fn blake3_file_memory_mapped(
file: &std::fs::File,
options: Blake3ReadOptions,
) -> Result<Option<Blake3Digest>, Blake3HashError> {
let length = file.metadata().map_err(Blake3HashError::read)?.len();
if length == 0 {
return Ok(None);
}
if let Some(maximum_bytes) = options.maximum_bytes {
if length > maximum_bytes {
return Err(Blake3HashError::size_limit_exceeded(maximum_bytes));
}
}
let Ok(mapped) = (unsafe { memmap2::Mmap::map(file) }) else {
return Ok(None);
};
let mut hasher = Blake3Hasher::new();
if length >= PARALLEL_HASH_THRESHOLD_BYTES {
hasher.update_parallel(&mapped);
} else {
hasher.update(&mapped);
}
Ok(Some(hasher.finalize()))
}
fn next_read_length(maximum_bytes: Option<u64>, bytes_hashed: u64) -> usize {
let Some(maximum_bytes) = maximum_bytes else {
return READ_BUFFER_LENGTH;
};
let remaining = maximum_bytes.saturating_sub(bytes_hashed);
let one_more_than_remaining =
remaining.min(u64::try_from(READ_BUFFER_LENGTH - 1).expect("buffer length fits u64")) + 1;
usize::try_from(one_more_than_remaining).expect("bounded buffer length fits usize")
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use std::str::FromStr as _;
#[test]
fn blake3_bytes_is_deterministic_and_input_sensitive() {
assert_eq!(blake3_bytes(b"hello world"), blake3_bytes(b"hello world"));
assert_ne!(blake3_bytes(b"hello"), blake3_bytes(b"world"));
}
#[test]
fn digest_round_trips_through_bytes() {
let digest = blake3_bytes(b"round trip");
let round_tripped = Blake3Digest::from_bytes(*digest.as_bytes());
assert_eq!(digest, round_tripped);
}
#[test]
fn digest_round_trips_through_hex() {
let digest = blake3_bytes(b"hex round trip");
let hex = digest.to_hex();
assert_eq!(Blake3Digest::from_hex(&hex).expect("valid hex"), digest);
assert_eq!(Blake3Digest::from_str(&hex).expect("valid hex"), digest);
assert_eq!(
Blake3Digest::from_hex(&hex.to_uppercase()).expect("uppercase hex accepted"),
digest
);
}
#[test]
fn digest_from_hex_rejects_wrong_length() {
let error = Blake3Digest::from_hex("ab").expect_err("too short");
assert_eq!(error, Blake3HexDecodeError::InvalidLength { length: 2 });
let too_long = "a".repeat(BLAKE3_DIGEST_LENGTH * 2 + 1);
let error = Blake3Digest::from_hex(&too_long).expect_err("too long");
assert_eq!(
error,
Blake3HexDecodeError::InvalidLength {
length: BLAKE3_DIGEST_LENGTH * 2 + 1
}
);
}
#[test]
fn digest_from_hex_rejects_non_hex_characters() {
let mut invalid = "0".repeat(BLAKE3_DIGEST_LENGTH * 2);
invalid.replace_range(0..1, "g");
let error = Blake3Digest::from_hex(&invalid).expect_err("non-hex character");
assert_eq!(error, Blake3HexDecodeError::InvalidCharacter);
}
#[test]
fn hasher_matches_blake3_bytes_for_a_single_update() {
let mut hasher = Blake3Hasher::new();
hasher.update(b"streamed input");
assert_eq!(hasher.finalize(), blake3_bytes(b"streamed input"));
}
#[test]
fn hasher_matches_blake3_bytes_across_many_chunked_updates() {
let payload: Vec<u8> = (0..(200 * 1024)).map(|index| (index % 251) as u8).collect();
let mut hasher = Blake3Hasher::new();
for chunk in payload.chunks(7 * 1024 + 1) {
hasher.update(chunk);
}
assert_eq!(hasher.finalize(), blake3_bytes(&payload));
}
#[test]
fn hasher_finalize_does_not_consume_or_reset_the_running_state() {
let mut hasher = Blake3Hasher::new();
hasher.update(b"first");
let first_digest = hasher.finalize();
assert_eq!(
hasher.finalize(),
first_digest,
"finalize must be repeatable"
);
hasher.update(b"second");
let combined_digest = hasher.finalize();
assert_eq!(combined_digest, blake3_bytes(b"firstsecond"));
assert_ne!(combined_digest, first_digest);
}
#[test]
fn hasher_write_matches_update() {
let mut via_write = Blake3Hasher::new();
via_write
.write_all(b"written through io::Write")
.expect("write never fails for an in-memory hasher");
let mut via_update = Blake3Hasher::new();
via_update.update(b"written through io::Write");
assert_eq!(via_write.finalize(), via_update.finalize());
}
#[test]
fn hasher_default_matches_new() {
let mut default_hasher = Blake3Hasher::default();
let mut new_hasher = Blake3Hasher::new();
default_hasher.update(b"same input");
new_hasher.update(b"same input");
assert_eq!(default_hasher.finalize(), new_hasher.finalize());
}
#[test]
fn derive_key_is_domain_separated_and_differs_from_plain_hashing() {
let mut context_a_first = Blake3Hasher::new_derive_key("context-a");
context_a_first.update(b"same input");
let mut context_a_second = Blake3Hasher::new_derive_key("context-a");
context_a_second.update(b"same input");
assert_eq!(
context_a_first.finalize(),
context_a_second.finalize(),
"the same context must be deterministic"
);
let mut context_b = Blake3Hasher::new_derive_key("context-b");
context_b.update(b"same input");
assert_ne!(
context_a_first.finalize(),
context_b.finalize(),
"different contexts must derive different keys"
);
let mut plain = Blake3Hasher::new();
plain.update(b"same input");
assert_ne!(
context_a_first.finalize(),
plain.finalize(),
"derive-key mode must differ from plain hashing"
);
}
#[test]
fn blake3_reader_matches_blake3_bytes() {
let payload = b"reader payload".to_vec();
let digest = blake3_reader(payload.as_slice(), Blake3ReadOptions::new())
.expect("unbounded reader hashing succeeds");
assert_eq!(digest, blake3_bytes(&payload));
}
#[test]
fn blake3_reader_enforces_the_maximum_byte_count() {
let payload = vec![0_u8; 100];
let error = blake3_reader(
payload.as_slice(),
Blake3ReadOptions::new().maximum_bytes(99),
)
.expect_err("oversized input must be rejected");
assert_eq!(
error.kind(),
Blake3HashErrorKind::SizeLimitExceeded { maximum_bytes: 99 }
);
}
#[test]
fn read_options_default_matches_new() {
assert_eq!(Blake3ReadOptions::default(), Blake3ReadOptions::new());
assert_eq!(Blake3ReadOptions::new().maximum_byte_count(), None);
assert!(!Blake3ReadOptions::new().memory_map_requested());
assert!(Blake3ReadOptions::new()
.memory_map(true)
.memory_map_requested());
}
#[test]
fn blake3_file_matches_blake3_bytes_without_memory_map() {
let temporary = tempfile::NamedTempFile::new().expect("temporary file");
let payload = b"file payload without memory mapping".to_vec();
std::fs::write(temporary.path(), &payload).expect("write fixture file");
let digest =
blake3_file(temporary.path(), Blake3ReadOptions::new()).expect("buffered file hash");
assert_eq!(digest, blake3_bytes(&payload));
}
#[test]
fn blake3_file_matches_blake3_bytes_with_memory_map() {
let temporary = tempfile::NamedTempFile::new().expect("temporary file");
let payload: Vec<u8> = (0..(256 * 1024)).map(|index| (index % 251) as u8).collect();
std::fs::write(temporary.path(), &payload).expect("write fixture file");
let digest = blake3_file(temporary.path(), Blake3ReadOptions::new().memory_map(true))
.expect("memory-mapped file hash");
assert_eq!(digest, blake3_bytes(&payload));
}
#[test]
fn blake3_file_memory_map_falls_back_for_an_empty_file() {
let temporary = tempfile::NamedTempFile::new().expect("temporary file");
let digest = blake3_file(temporary.path(), Blake3ReadOptions::new().memory_map(true))
.expect("empty file falls back to the buffered path");
assert_eq!(digest, blake3_bytes(b""));
}
#[test]
fn blake3_file_memory_map_enforces_the_maximum_byte_count() {
let temporary = tempfile::NamedTempFile::new().expect("temporary file");
let payload = vec![7_u8; 4096];
std::fs::write(temporary.path(), &payload).expect("write fixture file");
let payload_length = u64::try_from(payload.len()).expect("fixture length fits u64");
let error = blake3_file(
temporary.path(),
Blake3ReadOptions::new()
.memory_map(true)
.maximum_bytes(payload_length - 1),
)
.expect_err("oversized mapped file must be rejected");
assert_eq!(
error.kind(),
Blake3HashErrorKind::SizeLimitExceeded {
maximum_bytes: payload_length - 1
}
);
let digest = blake3_file(
temporary.path(),
Blake3ReadOptions::new()
.memory_map(true)
.maximum_bytes(payload_length),
)
.expect("input at exactly the limit is accepted");
assert_eq!(digest, blake3_bytes(&payload));
}
#[test]
fn blake3_file_reports_a_missing_file_as_an_open_failure() {
let error = blake3_file(
"kernal-api-hash-fixture-that-does-not-exist",
Blake3ReadOptions::new(),
)
.expect_err("missing file must fail to open");
assert_eq!(error.kind(), Blake3HashErrorKind::Open);
}
}