use crate::error::{Error, Result};
use crate::oid::{Oid, Sha256Stream};
use crate::store::PendingWrite;
pub struct VerifyingSink<'p> {
inner: Option<Box<dyn PendingWrite + 'p>>,
hasher: Option<Sha256Stream>,
expected: Oid,
declared: u64,
received: u64,
}
impl<'p> VerifyingSink<'p> {
pub fn new(inner: Box<dyn PendingWrite + 'p>, expected: Oid, declared: u64) -> Self {
VerifyingSink {
inner: Some(inner),
hasher: Some(Sha256Stream::new()),
expected,
declared,
received: 0,
}
}
pub fn accept(&mut self, data: &[u8]) -> Result<()> {
let next = self.received.saturating_add(data.len() as u64);
if next > self.declared {
return Err(Error::Corrupt(format!(
"object {} declared {} bytes and sent at least {next}",
self.expected, self.declared
)));
}
self.hasher
.as_mut()
.expect("a sink is used before it is committed")
.update(data);
if let Err(err) = self
.inner
.as_mut()
.expect("a sink is used before it is committed")
.write_all(data)
{
return Err(Error::store_io(format!("write {}", self.expected), err));
}
self.received = next;
Ok(())
}
pub fn received(&self) -> u64 {
self.received
}
pub fn commit(mut self) -> Result<()> {
let hasher = self.hasher.take().expect("committed once");
let inner = self.inner.take().expect("committed once");
if self.received != self.declared {
return Err(Error::Corrupt(format!(
"object {} declared {} bytes and sent {}",
self.expected, self.declared, self.received
)));
}
let actual = hasher.finish();
if actual != self.expected {
return Err(Error::Corrupt(format!(
"object was sent as {} but hashes to {actual}",
self.expected
)));
}
inner.commit()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::{LfsStore, MemLfsStore};
#[test]
fn the_verifier_stops_an_overrun_at_the_first_chunk_past_the_claim() {
let store = MemLfsStore::new();
let oid = Oid::parse(&"e".repeat(64)).unwrap();
let mut sink = VerifyingSink::new(store.begin_write(&oid, 4).unwrap(), oid, 4);
assert!(sink.accept(b"abcd").is_ok());
let err = sink.accept(b"e").expect_err("one byte past the claim");
assert_eq!(err.status(), 422);
}
#[test]
fn a_short_object_is_not_committed() {
let store = MemLfsStore::new();
let oid = Oid::parse(&"f".repeat(64)).unwrap();
let mut sink = VerifyingSink::new(store.begin_write(&oid, 10).unwrap(), oid, 10);
sink.accept(b"abc").unwrap();
assert_eq!(sink.received(), 3);
assert_eq!(sink.commit().unwrap_err().status(), 422);
assert!(store.is_empty());
}
#[test]
fn bytes_that_hash_to_something_else_are_not_committed() {
let store = MemLfsStore::new();
let lie = Oid::parse(&"a".repeat(64)).unwrap();
let mut sink = VerifyingSink::new(store.begin_write(&lie, 3).unwrap(), lie, 3);
sink.accept(b"abc").unwrap();
let err = sink.commit().expect_err("abc does not hash to aaaa…");
assert_eq!(err.status(), 422);
assert!(
err.to_string().contains("ba7816bf"),
"the diagnostic must name the digest that was actually computed: {err}"
);
assert!(store.is_empty());
let truth =
Oid::parse("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad").unwrap();
let mut sink = VerifyingSink::new(store.begin_write(&truth, 3).unwrap(), truth, 3);
sink.accept(b"abc").unwrap();
sink.commit().unwrap();
assert_eq!(store.get(&truth).unwrap(), b"abc");
}
}