coven_database/
file_hashes.rs1use crate::DbError;
4use coven_crypto::{ContentHash, ContentHasher};
5use coven_format::chunks::CHUNK_SIZE;
6
7#[derive(Debug, Eq, PartialEq)]
11pub struct FileHashes {
12 pub(crate) content: ContentHash,
13 chunks: Vec<ContentHash>,
14}
15
16pub(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
22pub struct FileHasher {
24 content: ContentHasher,
25 chunk: ContentHasher,
26 in_chunk: usize,
27 chunks: Vec<ContentHash>,
28}
29
30impl FileHasher {
31 #[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 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 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 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 pub(crate) fn chunks(&self) -> &[ContentHash] {
83 &self.chunks
84 }
85}
86
87#[cfg(test)]
88#[path = "file_hashes_tests.rs"]
89mod tests;