1use std::fs;
9use std::io::Write;
10use std::path::{Path, PathBuf};
11
12use crate::error::{Error, Result};
13use crate::hash::Hash;
14use crate::object::Object;
15use crate::wire;
16
17#[derive(Debug, Clone)]
19pub struct ObjectStore {
20 objects_dir: PathBuf,
21}
22
23impl ObjectStore {
24 pub fn open(gpp_dir: &Path) -> Self {
26 Self {
27 objects_dir: gpp_dir.join("objects"),
28 }
29 }
30
31 pub fn init(gpp_dir: &Path) -> Result<Self> {
33 let store = Self::open(gpp_dir);
34 fs::create_dir_all(&store.objects_dir)?;
35 Ok(store)
36 }
37
38 fn path_for(&self, id: &Hash) -> PathBuf {
40 let s = id.to_base32();
41 let (shard, rest) = s.split_at(2);
42 self.objects_dir.join(shard).join(rest)
43 }
44
45 pub fn contains(&self, id: &Hash) -> bool {
47 self.path_for(id).exists()
48 }
49
50 pub fn write<T: Object>(&self, object: &T) -> Result<Hash> {
52 let body = object.encode_body()?;
53 let id = Hash::of(&body);
54 let path = self.path_for(&id);
55 if path.exists() {
56 tracing::trace!(%id, "object already present, skipping write");
57 return Ok(id);
58 }
59
60 let frame = wire::encode(T::TYPE, &body)?;
61 let dir = path
62 .parent()
63 .expect("object path always has a shard parent");
64 fs::create_dir_all(dir)?;
65
66 let tmp = dir.join(format!(".tmp-{}", id.short()));
68 {
69 let mut f = fs::File::create(&tmp)?;
70 f.write_all(&frame)?;
71 f.sync_all()?;
72 }
73 fs::rename(&tmp, &path)?;
74 tracing::debug!(%id, bytes = frame.len(), "wrote object");
75 Ok(id)
76 }
77
78 pub fn read_raw(&self, id: &Hash) -> Result<Vec<u8>> {
81 match fs::read(self.path_for(id)) {
82 Ok(b) => Ok(b),
83 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(Error::NotFound(*id)),
84 Err(e) => Err(Error::Io(e)),
85 }
86 }
87
88 pub fn write_raw(&self, id: &Hash, frame: &[u8]) -> Result<()> {
92 let decoded = wire::decode(frame)?;
93 let computed = Hash::of(&decoded.body);
94 if computed != *id {
95 return Err(Error::HashMismatch {
96 expected: *id,
97 computed,
98 });
99 }
100 let path = self.path_for(id);
101 if path.exists() {
102 return Ok(());
103 }
104 let dir = path
105 .parent()
106 .expect("object path always has a shard parent");
107 fs::create_dir_all(dir)?;
108 let tmp = dir.join(format!(".tmp-{}", id.short()));
109 {
110 let mut f = fs::File::create(&tmp)?;
111 f.write_all(frame)?;
112 f.sync_all()?;
113 }
114 fs::rename(&tmp, &path)?;
115 Ok(())
116 }
117
118 pub fn iter_ids(&self) -> Vec<Hash> {
120 let mut out = Vec::new();
121 let Ok(shards) = fs::read_dir(&self.objects_dir) else {
122 return out;
123 };
124 for shard in shards.flatten() {
125 if !shard.path().is_dir() {
126 continue;
127 }
128 let shard_name = shard.file_name().to_string_lossy().into_owned();
129 let Ok(rest) = fs::read_dir(shard.path()) else {
130 continue;
131 };
132 for ent in rest.flatten() {
133 let fname = ent.file_name().to_string_lossy().into_owned();
134 if fname.starts_with(".tmp-") {
135 continue;
136 }
137 if let Ok(h) = Hash::from_base32(&format!("{shard_name}{fname}")) {
138 out.push(h);
139 }
140 }
141 }
142 out
143 }
144
145 pub fn read<T: Object>(&self, id: &Hash) -> Result<T> {
147 let path = self.path_for(id);
148 let frame = match fs::read(&path) {
149 Ok(b) => b,
150 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
151 return Err(Error::NotFound(*id));
152 }
153 Err(e) => return Err(Error::Io(e)),
154 };
155
156 let decoded = wire::decode(&frame)?;
157 if decoded.object_type != T::TYPE {
158 return Err(Error::TypeMismatch {
159 expected: T::TYPE.code(),
160 found: decoded.object_type.code(),
161 });
162 }
163
164 let computed = Hash::of(&decoded.body);
165 if computed != *id {
166 return Err(Error::HashMismatch {
167 expected: *id,
168 computed,
169 });
170 }
171 T::decode_body(&decoded.body)
172 }
173}
174
175pub fn flatten_tree(
178 store: &ObjectStore,
179 root: &Hash,
180) -> Result<std::collections::BTreeMap<String, Hash>> {
181 fn walk(
182 store: &ObjectStore,
183 tree_hash: &Hash,
184 prefix: &str,
185 out: &mut std::collections::BTreeMap<String, Hash>,
186 ) -> Result<()> {
187 let tree: crate::Tree = store.read(tree_hash)?;
188 for e in tree.entries {
189 let path = if prefix.is_empty() {
190 e.name.clone()
191 } else {
192 format!("{prefix}/{}", e.name)
193 };
194 match e.kind {
195 crate::EntryKind::Directory => walk(store, &e.hash, &path, out)?,
196 crate::EntryKind::File | crate::EntryKind::Symlink => {
197 out.insert(path, e.hash);
198 }
199 }
200 }
201 Ok(())
202 }
203 let mut out = std::collections::BTreeMap::new();
204 walk(store, root, "", &mut out)?;
205 Ok(out)
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::object::{Blob, EntryKind, Tree, TreeEntry};
212
213 fn store() -> (tempfile::TempDir, ObjectStore) {
214 let dir = tempfile::tempdir().unwrap();
215 let store = ObjectStore::init(dir.path()).unwrap();
216 (dir, store)
217 }
218
219 #[test]
220 fn write_then_read_blob() {
221 let (_d, s) = store();
222 let blob = Blob::new(b"content addressed!".to_vec());
223 let id = s.write(&blob).unwrap();
224 assert!(s.contains(&id));
225 assert_eq!(s.read::<Blob>(&id).unwrap(), blob);
226 }
227
228 #[test]
229 fn write_is_idempotent() {
230 let (_d, s) = store();
231 let blob = Blob::new(b"dup".to_vec());
232 assert_eq!(s.write(&blob).unwrap(), s.write(&blob).unwrap());
233 }
234
235 #[test]
236 fn write_then_read_tree() {
237 let (_d, s) = store();
238 let blob_id = s.write(&Blob::new(b"file".to_vec())).unwrap();
239 let tree = Tree::new(vec![TreeEntry {
240 name: "file.txt".into(),
241 kind: EntryKind::File,
242 hash: blob_id,
243 mode: 0o644,
244 size: 4,
245 }]);
246 let id = s.write(&tree).unwrap();
247 assert_eq!(s.read::<Tree>(&id).unwrap(), tree);
248 }
249
250 #[test]
251 fn missing_object_is_not_found() {
252 let (_d, s) = store();
253 let err = s.read::<Blob>(&Hash::of(b"nope")).unwrap_err();
254 assert!(matches!(err, Error::NotFound(_)));
255 }
256
257 #[test]
258 fn type_mismatch_is_rejected() {
259 let (_d, s) = store();
260 let id = s.write(&Blob::new(b"i am a blob".to_vec())).unwrap();
261 assert!(matches!(
262 s.read::<Tree>(&id).unwrap_err(),
263 Error::TypeMismatch { .. }
264 ));
265 }
266
267 #[test]
268 fn raw_transfer_roundtrips_and_enumerates() {
269 let (_d, src) = store();
270 let id = src.write(&Blob::new(b"sync me".to_vec())).unwrap();
271 let frame = src.read_raw(&id).unwrap();
272
273 let dst_dir = tempfile::tempdir().unwrap();
274 let dst = ObjectStore::init(dst_dir.path()).unwrap();
275 dst.write_raw(&id, &frame).unwrap();
276 assert_eq!(dst.read::<Blob>(&id).unwrap().content, b"sync me");
277
278 let ids = dst.iter_ids();
279 assert_eq!(ids, vec![id]);
280 dst.write_raw(&id, &frame).unwrap();
282 }
283
284 #[test]
285 fn write_raw_rejects_wrong_address() {
286 let (_d, s) = store();
287 let frame = wire::encode(crate::object::ObjectType::Blob, b"real").unwrap();
288 let wrong = Hash::of(b"not-the-body");
289 assert!(matches!(
290 s.write_raw(&wrong, &frame).unwrap_err(),
291 Error::HashMismatch { .. }
292 ));
293 }
294
295 #[test]
296 fn corrupted_object_is_detected() {
297 let (_d, s) = store();
298 let id = s.write(&Blob::new(b"tamper me".to_vec())).unwrap();
299 let path = s.path_for(&id);
300 let mut bytes = fs::read(&path).unwrap();
301 let n = bytes.len();
302 bytes[n - 5] ^= 0xff;
303 fs::write(&path, &bytes).unwrap();
304 assert!(s.read::<Blob>(&id).is_err());
305 }
306}