Skip to main content

coven_database/
file_hashes.rs

1//! Whole-file and plaintext-chunk hashes captured from the same first read.
2
3use crate::DbError;
4use coven_crypto::{ContentHash, ContentHasher};
5use coven_format::chunks::CHUNK_SIZE;
6
7/// A file's content hash and the hash of each 64-KiB plaintext chunk, the
8/// last possibly shorter, from one read of its bytes: what an upload checks
9/// each chunk it sends against (§17.1, §17.9).
10#[derive(Debug, Eq, PartialEq)]
11pub struct FileHashes {
12    pub(crate) content: ContentHash,
13    chunks: Vec<ContentHash>,
14}
15
16/// A hash coven stored; one that isn't 32 bytes is damage.
17pub(crate) fn stored_hash(bytes: Vec<u8>) -> Result<ContentHash, DbError> {
18    let bytes = <[u8; 32]>::try_from(bytes).map_err(|_wrong_size| DbError::DamagedDatabase)?;
19    Ok(ContentHash::from_bytes(bytes))
20}
21
22/// Hashes a file's bytes as they pass, in order, into its `FileHashes`.
23pub struct FileHasher {
24    content: ContentHasher,
25    chunk: ContentHasher,
26    in_chunk: usize,
27    chunks: Vec<ContentHash>,
28}
29
30impl FileHasher {
31    /// A hasher that has seen no bytes.
32    #[expect(clippy::new_without_default, reason = "a hasher starts from no bytes, not a default")]
33    pub fn new() -> Self {
34        Self {
35            content: ContentHasher::new(),
36            chunk: ContentHasher::new(),
37            in_chunk: 0,
38            chunks: Vec::new(),
39        }
40    }
41
42    /// Hash the file's next bytes.
43    pub fn update(&mut self, mut bytes: &[u8]) {
44        self.content.update(bytes);
45        while !bytes.is_empty() {
46            let length = bytes.len().min(CHUNK_SIZE - self.in_chunk);
47            self.chunk.update(&bytes[..length]);
48            self.in_chunk += length;
49            bytes = &bytes[length..];
50            if self.in_chunk == CHUNK_SIZE {
51                self.finish_chunk();
52            }
53        }
54    }
55
56    fn finish_chunk(&mut self) {
57        let chunk = std::mem::replace(&mut self.chunk, ContentHasher::new());
58        self.chunks.push(chunk.finish());
59        self.in_chunk = 0;
60    }
61
62    /// The hashes of every byte seen.
63    pub fn finish(mut self) -> FileHashes {
64        if self.in_chunk != 0 {
65            self.finish_chunk();
66        }
67        FileHashes { content: self.content.finish(), chunks: self.chunks }
68    }
69}
70
71impl FileHashes {
72    /// Hashes as coven stored them: the content hash, then each chunk's in
73    /// order.
74    pub(crate) fn stored(content: Vec<u8>, chunks: Vec<Vec<u8>>) -> Result<Self, DbError> {
75        Ok(Self {
76            content: stored_hash(content)?,
77            chunks: chunks.into_iter().map(stored_hash).collect::<Result<_, _>>()?,
78        })
79    }
80
81    /// One hash per 64-KiB plaintext chunk, the last possibly shorter.
82    pub(crate) fn chunks(&self) -> &[ContentHash] {
83        &self.chunks
84    }
85}
86
87#[cfg(test)]
88#[path = "file_hashes_tests.rs"]
89mod tests;