1use choir_hash::ContentHash;
31use serde::{Deserialize, Serialize};
32
33pub const FORMAT_VERSION: u16 = 1;
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41pub struct ChunkerParams {
42 pub min: u32,
44 pub avg: u32,
46 pub max: u32,
48}
49
50impl Default for ChunkerParams {
51 fn default() -> Self {
53 Self {
54 min: 4 * 1024,
55 avg: 16 * 1024,
56 max: 64 * 1024,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Manifest {
64 pub format_version: u16,
66 pub params: ChunkerParams,
68 pub len: u64,
70 pub chunks: Vec<ContentHash>,
72}
73
74#[derive(Debug)]
76pub enum StoreError {
77 NotFound(ContentHash),
79 HashMismatch(ContentHash),
81 BadManifest(String),
83 Io(std::io::Error),
85}
86
87pub trait ChunkStore: Send {
92 fn put(&mut self, data: &[u8]) -> Result<ContentHash, StoreError>;
94
95 fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, StoreError>;
97
98 fn has(&self, hash: &ContentHash) -> bool;
100}
101
102#[derive(Default)]
104pub struct MemStore {
105 chunks: std::collections::HashMap<ContentHash, Vec<u8>>,
106}
107
108impl MemStore {
109 pub fn new() -> Self {
111 Self::default()
112 }
113
114 pub fn chunk_count(&self) -> usize {
116 self.chunks.len()
117 }
118}
119
120impl ChunkStore for MemStore {
121 fn put(&mut self, data: &[u8]) -> Result<ContentHash, StoreError> {
122 let hash = ContentHash::blake3(data);
123 self.chunks
124 .entry(hash.clone())
125 .or_insert_with(|| data.to_vec());
126 Ok(hash)
127 }
128
129 fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, StoreError> {
130 let data = self
131 .chunks
132 .get(hash)
133 .ok_or_else(|| StoreError::NotFound(hash.clone()))?;
134 if &ContentHash::blake3(data) != hash {
135 return Err(StoreError::HashMismatch(hash.clone()));
136 }
137 Ok(data.clone())
138 }
139
140 fn has(&self, hash: &ContentHash) -> bool {
141 self.chunks.contains_key(hash)
142 }
143}
144
145pub struct FsStore {
148 root: std::path::PathBuf,
149}
150
151impl FsStore {
152 pub fn open(root: &std::path::Path) -> Result<Self, StoreError> {
158 std::fs::create_dir_all(root).map_err(StoreError::Io)?;
159 Ok(Self {
160 root: root.to_path_buf(),
161 })
162 }
163
164 fn path_for(&self, hash: &ContentHash) -> std::path::PathBuf {
165 let hex = hash.to_hex();
166 self.root.join(&hex[3..5]).join(&hex)
168 }
169}
170
171impl ChunkStore for FsStore {
172 fn put(&mut self, data: &[u8]) -> Result<ContentHash, StoreError> {
173 let hash = ContentHash::blake3(data);
174 let path = self.path_for(&hash);
175 if !path.exists() {
176 std::fs::create_dir_all(path.parent().unwrap()).map_err(StoreError::Io)?;
177 let tmp = path.with_extension("tmp");
180 std::fs::write(&tmp, data).map_err(StoreError::Io)?;
181 std::fs::rename(&tmp, &path).map_err(StoreError::Io)?;
182 }
183 Ok(hash)
184 }
185
186 fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, StoreError> {
187 let path = self.path_for(hash);
188 let data = std::fs::read(&path).map_err(|e| {
189 if e.kind() == std::io::ErrorKind::NotFound {
190 StoreError::NotFound(hash.clone())
191 } else {
192 StoreError::Io(e)
193 }
194 })?;
195 if &ContentHash::blake3(&data) != hash {
196 return Err(StoreError::HashMismatch(hash.clone()));
197 }
198 Ok(data)
199 }
200
201 fn has(&self, hash: &ContentHash) -> bool {
202 self.path_for(hash).exists()
203 }
204}
205
206pub fn put_blob(
213 store: &mut dyn ChunkStore,
214 data: &[u8],
215 params: ChunkerParams,
216) -> Result<ContentHash, StoreError> {
217 let mut chunks = Vec::new();
218 for chunk in fastcdc::v2020::FastCDC::new(data, params.min, params.avg, params.max) {
219 chunks.push(store.put(&data[chunk.offset..chunk.offset + chunk.length])?);
220 }
221 let manifest = Manifest {
222 format_version: FORMAT_VERSION,
223 params,
224 len: data.len() as u64,
225 chunks,
226 };
227 let bytes = serde_json::to_vec(&manifest).expect("Manifest is always serializable");
228 store.put(&bytes)
229}
230
231pub fn get_blob(
239 store: &dyn ChunkStore,
240 manifest_hash: &ContentHash,
241) -> Result<Vec<u8>, StoreError> {
242 let manifest: Manifest = serde_json::from_slice(&store.get(manifest_hash)?)
243 .map_err(|e| StoreError::BadManifest(e.to_string()))?;
244 if manifest.format_version != FORMAT_VERSION {
245 return Err(StoreError::BadManifest(format!(
246 "unsupported manifest format version {} (expected {FORMAT_VERSION})",
247 manifest.format_version
248 )));
249 }
250 let mut data = Vec::with_capacity(manifest.len as usize);
251 for chunk_hash in &manifest.chunks {
252 data.extend_from_slice(&store.get(chunk_hash)?);
253 }
254 if data.len() as u64 != manifest.len {
255 return Err(StoreError::BadManifest(format!(
256 "reassembled {} bytes, manifest says {}",
257 data.len(),
258 manifest.len
259 )));
260 }
261 Ok(data)
262}