use std::io::{self, prelude::*};
use crc32fast::Hasher;
const HASH_BATCH_SIZE: usize = 32 * 1024;
pub struct CountingCrcWriter<W> {
inner: W,
hasher: Option<Hasher>,
pending: Vec<u8>,
count: u64,
}
impl<W> CountingCrcWriter<W> {
pub fn new(inner: W, calculate_crc: bool) -> Self {
Self::with_hasher(inner, calculate_crc.then(Hasher::new))
}
pub fn with_hasher(inner: W, hasher: Option<Hasher>) -> Self {
Self {
inner,
hasher,
pending: Vec::new(),
count: 0,
}
}
pub fn position(&self) -> u64 {
self.count
}
pub fn get_mut(&mut self) -> &mut W {
&mut self.inner
}
pub fn finalize(mut self) -> (W, Option<Hasher>) {
self.drain_pending();
(self.inner, self.hasher)
}
pub fn current_checksum(&self) -> u32 {
self.hasher
.clone()
.map(|mut hasher| {
if !self.pending.is_empty() {
hasher.update(&self.pending);
}
hasher.finalize()
})
.unwrap_or(0)
}
fn drain_pending(&mut self) {
if !self.pending.is_empty() {
self.hasher
.as_mut()
.expect("pending bytes only accumulate while hashing")
.update(&self.pending);
self.pending.clear();
}
}
}
impl<W: Write> Write for CountingCrcWriter<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let res = self.inner.write(buf)?;
self.count += res as u64;
if self.hasher.is_some() {
let written = &buf[..res];
if written.len() >= HASH_BATCH_SIZE {
self.drain_pending();
self.hasher.as_mut().expect("checked above").update(written);
} else {
self.pending.extend_from_slice(written);
if self.pending.len() >= HASH_BATCH_SIZE {
self.drain_pending();
}
}
}
Ok(res)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<W: Seek> Seek for CountingCrcWriter<W> {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.inner.seek(pos)
}
fn stream_position(&mut self) -> io::Result<u64> {
self.inner.stream_position()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reference_crc(data: &[u8]) -> u32 {
let mut hasher = Hasher::new();
hasher.update(data);
hasher.finalize()
}
#[test]
fn batched_hash_matches_reference_across_write_sizes() {
let data: Vec<u8> = (0..1_000_000u32).map(|i| (i % 251) as u8).collect();
for sizes in [
&[1usize, 2, 4, 8][..],
&[100][..],
&[HASH_BATCH_SIZE - 1][..],
&[HASH_BATCH_SIZE][..],
&[HASH_BATCH_SIZE + 1][..],
&[3, 100, HASH_BATCH_SIZE + 7, 8, HASH_BATCH_SIZE][..],
] {
let mut writer = CountingCrcWriter::new(Vec::new(), true);
let mut offset = 0;
let mut size_idx = 0;
while offset < data.len() {
let size = sizes[size_idx % sizes.len()].min(data.len() - offset);
writer.write_all(&data[offset..offset + size]).unwrap();
offset += size;
size_idx += 1;
}
assert_eq!(writer.position(), data.len() as u64);
assert_eq!(writer.current_checksum(), reference_crc(&data));
assert_eq!(writer.current_checksum(), reference_crc(&data));
let (inner, hasher) = writer.finalize();
assert_eq!(inner, data);
assert_eq!(hasher.unwrap().finalize(), reference_crc(&data));
}
}
#[test]
fn disabled_hasher_accumulates_nothing() {
let mut writer = CountingCrcWriter::new(Vec::new(), false);
writer.write_all(&[1, 2, 3]).unwrap();
assert_eq!(writer.current_checksum(), 0);
assert!(writer.pending.is_empty());
let (inner, hasher) = writer.finalize();
assert_eq!(inner, vec![1, 2, 3]);
assert!(hasher.is_none());
}
}