use core::fmt;
use crate::{CHUNK_SIZE, HashBackend};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupHashError {
InputTooLarge,
CounterOverflow,
}
impl fmt::Display for GroupHashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::InputTooLarge => "group input length exceeds u64",
Self::CounterOverflow => "group chunk counter overflows u64",
})
}
}
impl core::error::Error for GroupHashError {}
fn leaf_count(len: u64) -> u64 {
(len / CHUNK_SIZE as u64 + u64::from(!len.is_multiple_of(CHUNK_SIZE as u64))).max(1)
}
fn validate_counter(len: u64, start: u64) -> Result<u64, GroupHashError> {
let leaves = leaf_count(len);
start
.checked_add(leaves - 1)
.ok_or(GroupHashError::CounterOverflow)?;
Ok(leaves)
}
pub struct GroupHasher<'a, B: HashBackend> {
backend: &'a B,
remaining: &'a [u8],
next_counter: u64,
leaves: u64,
processed: u64,
frontier: [Option<B::Hash>; 53],
occupied: usize,
reductions: u32,
root: bool,
complete: bool,
}
impl<B: HashBackend> fmt::Debug for GroupHasher<'_, B> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GroupHasher")
.field("remaining_bytes", &self.remaining.len())
.field("processed_leaves", &self.processed)
.field("leaves", &self.leaves)
.field("occupied", &self.occupied)
.field("is_complete", &self.complete)
.finish_non_exhaustive()
}
}
impl<'a, B: HashBackend> GroupHasher<'a, B> {
pub fn new(
backend: &'a B,
data: &'a [u8],
start_chunk: u64,
is_root: bool,
) -> Result<Self, GroupHashError> {
let len = u64::try_from(data.len()).map_err(|_| GroupHashError::InputTooLarge)?;
let leaves = validate_counter(len, start_chunk)?;
Ok(Self {
backend,
remaining: data,
next_counter: start_chunk,
leaves,
processed: 0,
frontier: core::array::from_fn(|_| None),
occupied: 0,
reductions: 0,
root: is_root,
complete: false,
})
}
pub fn is_complete(&self) -> bool {
self.complete
}
pub fn step(&mut self) -> Option<B::Hash> {
if self.complete {
return None;
}
if self.reductions != 0 {
let right = self.frontier[self.occupied - 1]
.take()
.expect("occupied right peak");
let left = self.frontier[self.occupied - 2]
.take()
.expect("occupied left peak");
let final_parent = self.processed == self.leaves && self.occupied == 2;
let hash = self
.backend
.parent_hash(&left, &right, self.root && final_parent);
self.occupied -= 1;
self.reductions -= 1;
if final_parent {
self.complete = true;
return Some(hash);
}
self.frontier[self.occupied - 1] = Some(hash);
} else {
let take = self.remaining.len().min(CHUNK_SIZE);
let hash = self.backend.chunk_hash(
&self.remaining[..take],
self.next_counter,
self.root && self.leaves == 1,
);
self.remaining = &self.remaining[take..];
self.processed += 1;
if self.leaves == 1 {
self.complete = true;
return Some(hash);
}
self.frontier[self.occupied] = Some(hash);
self.occupied += 1;
if self.processed == self.leaves {
self.reductions = (self.occupied - 1) as u32;
} else {
self.reductions = self.processed.trailing_zeros();
self.next_counter += 1; }
}
None
}
}
#[cfg(test)]
mod tests;