OXYGENHUB
# 7a716eb94dee oxygen / crates / oxy-core / src / store.rs
27.9 KiB · Rust · last changed Pack merging above 16 packs; modern CLI presentation (colour, spinners, --plain, NO_COLOR); oxy prompt with manifest-based dirty check and docs/prompt.md 1 month ago source blame history raw
d02e545d Andy McCall ↶ 1
//! Object store (spec §13.1, §13.3, §13.4): loose objects, one file per
2
//! object under `objects/loose/<hh>/<rest>`, and packs, `pack-<id>.pack`
3
//! with a sorted `pack-<id>.idx`, holding zstd-compressed objects. Reads
4
//! check loose objects first, then every pack; every read re-hashes.
5
//!
6
//! Pack file: `OXYPACK1` then entries of
7
//! `type u8 | flags u8 (bit 0: zstd) | stored_len u32 | raw_len u32 | ref 32 | body`.
8
//! Index file: `OXYIDX01` then `count u32` then sorted entries of
9
//! `ref 32 | offset u64 | stored_len u32 | raw_len u32 | type u8 | flags u8`.
ff0af1ee unknown 10
5b9c6ad4 Andy McCall ↶ 11
use crate::attest::Attest;
ff0af1ee unknown 12
use crate::hash::{hash_object, header, ObjectRef, ObjectType};
1983686f Andy McCall ↶ 13
use crate::identity::{Identity, Signature};
86e682c2 Andy McCall ↶ 14
use crate::objects::{Change, ObjectError, StateObject};
ff0af1ee unknown 15
use rand::RngCore;
d02e545d Andy McCall ↶ 16
use std::cell::RefCell;
ff0af1ee unknown 17
use std::fs;
d02e545d Andy McCall ↶ 18
use std::io::{self, Read, Seek, SeekFrom, Write};
ff0af1ee unknown 19
use std::path::{Path, PathBuf};
20
d02e545d Andy McCall ↶ 21
const PACK_MAGIC: &[u8; 8] = b"OXYPACK1";
22
const IDX_MAGIC: &[u8; 8] = b"OXYIDX01";
23
const IDX_ENTRY: usize = 32 + 8 + 4 + 4 + 1 + 1;
24
const FLAG_ZSTD: u8 = 1;
25
const ZSTD_LEVEL: i32 = 3;
26
27
#[derive(Clone, Copy, Debug)]
28
struct PackEntry {
29
    offset: u64,
30
    stored_len: u32,
31
    raw_len: u32,
32
    ty: ObjectType,
33
    flags: u8,
34
}
35
36
/// One pack's index, fully in memory and sorted by ObjectRef.
37
#[derive(Debug)]
38
struct PackIndex {
39
    path: PathBuf,
40
    refs: Vec<ObjectRef>,
41
    entries: Vec<PackEntry>,
42
}
43
44
impl PackIndex {
45
    fn load(idx_path: &Path, pack_path: &Path) -> Result<PackIndex> {
46
        let data = fs::read(idx_path)?;
47
        let bad = |m: &str| StoreError::Corrupt(ObjectRef::ZERO, format!("{}: {m}", idx_path.display()));
48
        if data.len() < 12 || &data[..8] != IDX_MAGIC {
49
            return Err(bad("bad index header"));
50
        }
51
        let count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
52
        if data.len() != 12 + count * IDX_ENTRY {
53
            return Err(bad("index length mismatch"));
54
        }
55
        let mut refs = Vec::with_capacity(count);
56
        let mut entries = Vec::with_capacity(count);
57
        for i in 0..count {
58
            let e = &data[12 + i * IDX_ENTRY..12 + (i + 1) * IDX_ENTRY];
59
            let r = ObjectRef::from_slice(&e[..32]).unwrap();
60
            if let Some(prev) = refs.last() {
61
                if r <= *prev {
62
                    return Err(bad("index not sorted"));
63
                }
64
            }
65
            refs.push(r);
66
            entries.push(PackEntry {
67
                offset: u64::from_le_bytes(e[32..40].try_into().unwrap()),
68
                stored_len: u32::from_le_bytes(e[40..44].try_into().unwrap()),
69
                raw_len: u32::from_le_bytes(e[44..48].try_into().unwrap()),
70
                ty: ObjectType::ALL.get(e[48] as usize).copied().ok_or_else(|| bad("unknown object type"))?,
71
                flags: e[49],
72
            });
73
        }
74
        Ok(PackIndex { path: pack_path.to_path_buf(), refs, entries })
75
    }
76
77
    fn find(&self, r: &ObjectRef) -> Option<PackEntry> {
78
        self.refs.binary_search(r).ok().map(|i| self.entries[i])
79
    }
80
81
    fn read(&self, r: &ObjectRef, e: PackEntry) -> Result<(ObjectType, Vec<u8>)> {
82
        let mut f = fs::File::open(&self.path)?;
83
        f.seek(SeekFrom::Start(e.offset))?;
84
        let mut stored = vec![0u8; e.stored_len as usize];
85
        f.read_exact(&mut stored)?;
86
        let body = if e.flags & FLAG_ZSTD != 0 {
87
            zstd::bulk::decompress(&stored, e.raw_len as usize).map_err(|err| StoreError::Corrupt(*r, format!("zstd: {err}")))?
88
        } else {
89
            stored
90
        };
91
        if body.len() != e.raw_len as usize || hash_object(e.ty, &body) != *r {
92
            return Err(StoreError::Corrupt(*r, "packed object does not match its reference".into()));
93
        }
94
        Ok((e.ty, body))
95
    }
96
}
97
98
fn type_index(ty: ObjectType) -> u8 {
99
    ObjectType::ALL.iter().position(|t| *t == ty).unwrap() as u8
100
}
101
102
/// What `pack_loose` did.
103
#[derive(Debug, Clone, Default, PartialEq, Eq)]
104
pub struct PackReport {
105
    pub objects: usize,
106
    pub raw_bytes: u64,
107
    pub packed_bytes: u64,
108
}
109
135212de Andy McCall ↶ 110
/// Packs tolerated before `merge_packs` folds them into one (spec §13.4).
111
pub const MAX_PACKS: usize = 16;
112
ba160f60 Andy McCall ↶ 113
/// What `prune` did.
114
#[derive(Debug, Clone, Default, PartialEq, Eq)]
115
pub struct PruneReport {
116
    pub objects: usize,
117
    pub bytes: u64,
118
    /// Unreachable objects left alone because they are younger than the grace period.
119
    pub deferred: usize,
120
}
121
ff0af1ee unknown 122
#[derive(Debug, thiserror::Error)]
123
pub enum StoreError {
124
    #[error("io: {0}")]
125
    Io(#[from] io::Error),
126
    #[error("corrupt object {0}: {1}")]
127
    Corrupt(ObjectRef, String),
128
    #[error("object {0}")]
129
    Object(#[from] ObjectError),
130
    #[error("object {0} is a {1}, expected {2}")]
131
    WrongType(ObjectRef, &'static str, &'static str),
132
    #[error("object {0} not found")]
133
    NotFound(ObjectRef),
134
}
135
136
pub type Result<T> = std::result::Result<T, StoreError>;
137
d02e545d Andy McCall ↶ 138
#[derive(Debug)]
ff0af1ee unknown 139
pub struct Store {
140
    dir: PathBuf,
d02e545d Andy McCall ↶ 141
    packs: RefCell<Vec<PackIndex>>,
ff0af1ee unknown 142
}
143
144
impl Store {
145
    pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> {
146
        let dir = dir.into();
147
        fs::create_dir_all(dir.join("loose"))?;
d02e545d Andy McCall ↶ 148
        let store = Store { dir, packs: RefCell::new(Vec::new()) };
149
        store.reload_packs().map_err(|e| io::Error::other(e.to_string()))?;
150
        Ok(store)
151
    }
152
153
    fn reload_packs(&self) -> Result<()> {
154
        let mut packs = Vec::new();
155
        let mut names: Vec<PathBuf> = fs::read_dir(&self.dir)?
156
            .filter_map(|e| e.ok().map(|e| e.path()))
157
            .filter(|p| p.extension().is_some_and(|x| x == "idx") && p.file_name().is_some_and(|n| n.to_string_lossy().starts_with("pack-")))
158
            .collect();
159
        names.sort();
160
        for idx in names {
161
            let pack = idx.with_extension("pack");
162
            if pack.is_file() {
163
                packs.push(PackIndex::load(&idx, &pack)?);
164
            }
165
        }
166
        *self.packs.borrow_mut() = packs;
167
        Ok(())
168
    }
169
170
    pub fn pack_count(&self) -> usize {
171
        self.packs.borrow().len()
172
    }
173
174
    fn find_packed(&self, r: &ObjectRef) -> Option<(usize, PackEntry)> {
175
        self.packs.borrow().iter().enumerate().find_map(|(i, p)| p.find(r).map(|e| (i, e)))
176
    }
177
178
    /// Loose objects on disk, with their paths.
179
    fn loose_objects(&self) -> io::Result<Vec<(ObjectRef, PathBuf)>> {
180
        let mut out = Vec::new();
181
        let loose = self.dir.join("loose");
182
        for d in fs::read_dir(&loose)? {
183
            let d = d?;
184
            if !d.file_type()?.is_dir() {
185
                continue;
186
            }
187
            let prefix = d.file_name().to_string_lossy().to_string();
188
            for f in fs::read_dir(d.path())? {
189
                let f = f?;
190
                let name = f.file_name().to_string_lossy().to_string();
191
                if let Some(r) = ObjectRef::from_hex(&format!("{prefix}{name}")) {
192
                    out.push((r, f.path()));
193
                }
194
            }
195
        }
196
        out.sort();
197
        Ok(out)
198
    }
199
200
    pub fn loose_count(&self) -> io::Result<usize> {
201
        Ok(self.loose_objects()?.len())
202
    }
203
204
    /// Move every loose object into a new pack (spec §13.4). Loose files are
205
    /// deleted only after the pack and its index are durable.
206
    pub fn pack_loose(&self) -> Result<PackReport> {
207
        let loose = self.loose_objects()?;
ba160f60 Andy McCall ↶ 208
        self.write_pack(&loose.iter().map(|(r, _)| *r).collect::<Vec<_>>(), &loose.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(), &[])
209
    }
210
135212de Andy McCall ↶ 211
    /// Fold every pack into one when there are more than `MAX_PACKS`
212
    /// (spec §13.4). Returns how many packs were merged.
213
    pub fn merge_packs(&self) -> Result<usize> {
214
        let (refs, paths): (Vec<ObjectRef>, Vec<PathBuf>) = {
215
            let packs = self.packs.borrow();
216
            if packs.len() <= MAX_PACKS {
217
                return Ok(0);
218
            }
219
            (packs.iter().flat_map(|p| p.refs.iter().copied()).collect(), packs.iter().map(|p| p.path.clone()).collect())
220
        };
221
        let n = paths.len();
222
        self.write_pack(&refs, &[], &paths)?;
223
        Ok(n)
224
    }
225
ba160f60 Andy McCall ↶ 226
    /// Delete every object not in `keep` (spec §21). Loose objects and packs
227
    /// younger than `grace` are left alone, since a concurrent writer may not
228
    /// have recorded its references yet. Packs holding unreachable objects
229
    /// are rewritten without them. Signature sidecars and seal-index entries
230
    /// of deleted objects go too.
231
    pub fn prune(&self, keep: &std::collections::HashSet<ObjectRef>, grace: std::time::Duration) -> Result<PruneReport> {
232
        let now = std::time::SystemTime::now();
233
        let old_enough = |m: &fs::Metadata| m.modified().map(|t| now.duration_since(t).unwrap_or_default() >= grace).unwrap_or(true);
234
        let mut report = PruneReport::default();
235
        // Loose objects.
236
        for (r, path) in self.loose_objects()? {
237
            if keep.contains(&r) {
238
                continue;
239
            }
240
            let meta = fs::metadata(&path)?;
241
            if !old_enough(&meta) {
242
                report.deferred += 1;
243
                continue;
244
            }
245
            report.bytes += meta.len();
246
            report.objects += 1;
247
            fs::remove_file(&path)?;
248
            let _ = fs::remove_dir(path.parent().unwrap());
249
            self.remove_sidecars(&r)?;
250
        }
251
        // Packs: rewrite those that hold unreachable objects.
252
        let mut keep_refs: Vec<ObjectRef> = Vec::new();
253
        let mut old_paths: Vec<PathBuf> = Vec::new();
254
        let mut rewrite = false;
255
        for p in self.packs.borrow().iter() {
256
            let meta = fs::metadata(&p.path)?;
257
            let unreachable = p.refs.iter().filter(|r| !keep.contains(r)).count();
258
            if unreachable == 0 {
259
                continue;
260
            }
261
            if !old_enough(&meta) {
262
                report.deferred += unreachable;
263
                continue;
264
            }
265
            rewrite = true;
266
            old_paths.push(p.path.clone());
267
            for (r, e) in p.refs.iter().zip(p.entries.iter()) {
268
                if keep.contains(r) {
269
                    keep_refs.push(*r);
270
                } else {
271
                    report.objects += 1;
272
                    report.bytes += e.stored_len as u64;
273
                }
274
            }
275
        }
276
        if rewrite {
277
            let dropped: Vec<ObjectRef> = {
278
                let packs = self.packs.borrow();
279
                packs.iter().filter(|p| old_paths.contains(&p.path)).flat_map(|p| p.refs.iter().copied()).filter(|r| !keep.contains(r)).collect()
280
            };
281
            self.write_pack(&keep_refs, &[], &old_paths)?;
282
            for r in dropped {
283
                self.remove_sidecars(&r)?;
284
            }
285
        }
286
        Ok(report)
287
    }
288
289
    fn remove_sidecars(&self, r: &ObjectRef) -> Result<()> {
290
        for path in [self.sig_path(r), self.seal_index_path(r)] {
291
            match fs::remove_file(&path) {
292
                Ok(()) => {
293
                    let _ = fs::remove_dir(path.parent().unwrap());
294
                }
295
                Err(e) if e.kind() == io::ErrorKind::NotFound => {}
296
                Err(e) => return Err(e.into()),
297
            }
298
        }
299
        Ok(())
300
    }
301
302
    /// Write `refs` into a new pack, then delete `loose_paths` and `old_packs`.
303
    fn write_pack(&self, refs: &[ObjectRef], loose_paths: &[PathBuf], old_packs: &[PathBuf]) -> Result<PackReport> {
304
        if refs.is_empty() {
305
            for p in old_packs {
306
                fs::remove_file(p)?;
307
                fs::remove_file(p.with_extension("idx"))?;
308
            }
309
            self.reload_packs()?;
d02e545d Andy McCall ↶ 310
            return Ok(PackReport::default());
311
        }
ba160f60 Andy McCall ↶ 312
        let mut refs: Vec<ObjectRef> = refs.to_vec();
313
        refs.sort();
314
        refs.dedup();
d02e545d Andy McCall ↶ 315
        let mut nonce = [0u8; 8];
316
        rand::rngs::OsRng.fill_bytes(&mut nonce);
317
        let id = hex::encode(nonce);
318
        let pack_path = self.dir.join(format!("pack-{id}.pack"));
319
        let idx_path = self.dir.join(format!("pack-{id}.idx"));
320
        let tmp_pack = self.dir.join(format!("tmp-{id}.pack"));
321
        let tmp_idx = self.dir.join(format!("tmp-{id}.idx"));
322
        let mut report = PackReport::default();
ba160f60 Andy McCall ↶ 323
        let mut index: Vec<(ObjectRef, PackEntry)> = Vec::with_capacity(refs.len());
d02e545d Andy McCall ↶ 324
        {
325
            let mut f = io::BufWriter::new(fs::File::create(&tmp_pack)?);
326
            f.write_all(PACK_MAGIC)?;
327
            let mut offset = PACK_MAGIC.len() as u64;
ba160f60 Andy McCall ↶ 328
            for r in &refs {
d02e545d Andy McCall ↶ 329
                let (ty, body) = self.get(r)?;
330
                let compressed = zstd::bulk::compress(&body, ZSTD_LEVEL).map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?;
331
                let (flags, stored) = if compressed.len() < body.len() { (FLAG_ZSTD, compressed) } else { (0, body.clone()) };
332
                let header_len = 1 + 1 + 4 + 4 + 32;
333
                f.write_all(&[type_index(ty), flags])?;
334
                f.write_all(&(stored.len() as u32).to_le_bytes())?;
335
                f.write_all(&(body.len() as u32).to_le_bytes())?;
336
                f.write_all(&r.0)?;
337
                f.write_all(&stored)?;
338
                index.push((*r, PackEntry { offset: offset + header_len, stored_len: stored.len() as u32, raw_len: body.len() as u32, ty, flags }));
339
                offset += header_len + stored.len() as u64;
340
                report.objects += 1;
341
                report.raw_bytes += body.len() as u64;
342
                report.packed_bytes += stored.len() as u64;
343
            }
344
            f.flush()?;
345
            f.get_ref().sync_all()?;
346
        }
347
        {
348
            let mut f = io::BufWriter::new(fs::File::create(&tmp_idx)?);
349
            f.write_all(IDX_MAGIC)?;
350
            f.write_all(&(index.len() as u32).to_le_bytes())?;
351
            for (r, e) in &index {
352
                f.write_all(&r.0)?;
353
                f.write_all(&e.offset.to_le_bytes())?;
354
                f.write_all(&e.stored_len.to_le_bytes())?;
355
                f.write_all(&e.raw_len.to_le_bytes())?;
356
                f.write_all(&[type_index(e.ty), e.flags])?;
357
            }
358
            f.flush()?;
359
            f.get_ref().sync_all()?;
360
        }
361
        fs::rename(&tmp_pack, &pack_path)?;
362
        fs::rename(&tmp_idx, &idx_path)?;
ba160f60 Andy McCall ↶ 363
        for p in old_packs {
364
            fs::remove_file(p)?;
365
            fs::remove_file(p.with_extension("idx"))?;
366
        }
d02e545d Andy McCall ↶ 367
        self.reload_packs()?;
ba160f60 Andy McCall ↶ 368
        for path in loose_paths {
d02e545d Andy McCall ↶ 369
            fs::remove_file(path)?;
370
            if let Some(dir) = path.parent() {
371
                let _ = fs::remove_dir(dir);
372
            }
373
        }
374
        Ok(report)
ff0af1ee unknown 375
    }
376
377
    pub fn dir(&self) -> &Path {
378
        &self.dir
379
    }
380
381
    fn path_for(&self, r: &ObjectRef) -> PathBuf {
382
        let h = r.to_hex();
383
        self.dir.join("loose").join(&h[..2]).join(&h[2..])
384
    }
385
386
    pub fn has(&self, r: &ObjectRef) -> bool {
d02e545d Andy McCall ↶ 387
        self.path_for(r).is_file() || self.find_packed(r).is_some()
ff0af1ee unknown 388
    }
389
390
    /// Store `body` as an object of type `ty`; idempotent.
391
    pub fn put(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
392
        let r = hash_object(ty, body);
393
        let path = self.path_for(&r);
d02e545d Andy McCall ↶ 394
        if path.is_file() || self.find_packed(&r).is_some() {
ff0af1ee unknown 395
            return Ok(r);
396
        }
397
        fs::create_dir_all(path.parent().unwrap())?;
398
        let mut nonce = [0u8; 8];
399
        rand::rngs::OsRng.fill_bytes(&mut nonce);
400
        let tmp = self.dir.join("loose").join(format!("tmp-{}", hex::encode(nonce)));
401
        {
402
            let mut f = fs::File::create(&tmp)?;
403
            f.write_all(&header(ty, body.len()))?;
404
            f.write_all(body)?;
405
            f.sync_all()?;
406
        }
407
        fs::rename(&tmp, &path)?;
408
        Ok(r)
409
    }
410
d02e545d Andy McCall ↶ 411
    /// Read and verify an object, loose or packed.
ff0af1ee unknown 412
    pub fn get(&self, r: &ObjectRef) -> Result<(ObjectType, Vec<u8>)> {
413
        let path = self.path_for(r);
414
        let data = match fs::read(&path) {
415
            Ok(d) => d,
d02e545d Andy McCall ↶ 416
            Err(e) if e.kind() == io::ErrorKind::NotFound => {
417
                return match self.find_packed(r) {
418
                    Some((i, e)) => self.packs.borrow()[i].read(r, e),
419
                    None => Err(StoreError::NotFound(*r)),
420
                };
421
            }
ff0af1ee unknown 422
            Err(e) => return Err(e.into()),
423
        };
424
        let nul = data.iter().position(|&b| b == 0).ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?;
425
        let head = std::str::from_utf8(&data[..nul]).map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?;
426
        let mut parts = head.split(' ');
427
        let (Some("oxy1"), Some(ty), Some(len), None) = (parts.next(), parts.next(), parts.next(), parts.next()) else {
428
            return Err(StoreError::Corrupt(*r, format!("bad header {head:?}")));
429
        };
430
        let ty = ObjectType::from_name(ty).ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?;
431
        let len: usize = len.parse().map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?;
432
        let body = &data[nul + 1..];
433
        if body.len() != len {
434
            return Err(StoreError::Corrupt(*r, format!("length {} != header {len}", body.len())));
435
        }
436
        if hash_object(ty, body) != *r {
437
            return Err(StoreError::Corrupt(*r, "hash mismatch".into()));
438
        }
439
        Ok((ty, body.to_vec()))
440
    }
441
442
    pub fn get_typed(&self, r: &ObjectRef, want: ObjectType) -> Result<Vec<u8>> {
443
        let (ty, body) = self.get(r)?;
444
        if ty != want {
445
            return Err(StoreError::WrongType(*r, ty.name(), want.name()));
446
        }
447
        Ok(body)
448
    }
449
450
    pub fn put_change(&self, c: &Change) -> Result<ObjectRef> {
451
        self.put(ObjectType::Change, &c.encode())
452
    }
453
454
    pub fn get_change(&self, r: &ObjectRef) -> Result<Change> {
455
        Ok(Change::decode(&self.get_typed(r, ObjectType::Change)?)?)
456
    }
457
458
    pub fn put_chunk(&self, bytes: &[u8]) -> Result<ObjectRef> {
459
        self.put(ObjectType::Chunk, bytes)
460
    }
461
86e682c2 Andy McCall ↶ 462
    pub fn put_state(&self, st: &StateObject) -> Result<ObjectRef> {
463
        self.put(ObjectType::State, &st.encode())
464
    }
465
466
    pub fn get_state(&self, r: &ObjectRef) -> Result<StateObject> {
467
        Ok(StateObject::decode(&self.get_typed(r, ObjectType::State)?)?)
468
    }
469
5b9c6ad4 Andy McCall ↶ 470
    pub fn put_attest(&self, a: &Attest) -> Result<ObjectRef> {
471
        self.put(ObjectType::Attest, &a.encode())
472
    }
473
474
    pub fn get_attest(&self, r: &ObjectRef) -> Result<Attest> {
475
        Ok(Attest::decode(&self.get_typed(r, ObjectType::Attest)?)?)
476
    }
477
478
    fn seal_index_path(&self, change: &ObjectRef) -> PathBuf {
479
        let h = change.to_hex();
480
        self.dir.join("seals").join(&h[..2]).join(&h[2..])
481
    }
482
483
    /// Record that seal attestation `attest` covers `change`.
484
    pub fn index_seal(&self, change: &ObjectRef, attest: &ObjectRef) -> Result<()> {
485
        let mut seals = self.seals_for(change)?;
486
        if seals.contains(attest) {
487
            return Ok(());
488
        }
489
        seals.push(*attest);
490
        seals.sort();
491
        let path = self.seal_index_path(change);
492
        fs::create_dir_all(path.parent().unwrap())?;
493
        let text: String = seals.iter().map(|s| format!("{}\n", s.to_hex())).collect();
494
        let tmp = path.with_extension("tmp");
495
        fs::write(&tmp, text)?;
496
        fs::rename(&tmp, &path)?;
497
        Ok(())
498
    }
499
500
    /// Seal attestations known to cover `change`.
501
    pub fn seals_for(&self, change: &ObjectRef) -> Result<Vec<ObjectRef>> {
502
        match fs::read_to_string(self.seal_index_path(change)) {
503
            Ok(t) => Ok(t.lines().filter_map(ObjectRef::from_hex).collect()),
504
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(vec![]),
505
            Err(e) => Err(e.into()),
506
        }
507
    }
508
1983686f Andy McCall ↶ 509
    pub fn put_identity(&self, id: &Identity) -> Result<ObjectRef> {
510
        self.put(ObjectType::Identity, &id.encode())
511
    }
512
513
    pub fn get_identity(&self, r: &ObjectRef) -> Result<Identity> {
514
        Ok(Identity::decode(&self.get_typed(r, ObjectType::Identity)?)?)
515
    }
516
517
    fn sig_path(&self, r: &ObjectRef) -> PathBuf {
518
        let h = r.to_hex();
519
        self.dir.join("sigs").join(&h[..2]).join(&h[2..])
520
    }
521
522
    /// Signatures stored alongside object `r` (spec §5.4). Empty if none.
523
    pub fn signatures(&self, r: &ObjectRef) -> Result<Vec<Signature>> {
524
        match fs::read(self.sig_path(r)) {
525
            Ok(b) => Ok(Signature::decode_list(&b)?),
526
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(vec![]),
527
            Err(e) => Err(e.into()),
528
        }
529
    }
530
531
    /// Attach a signature to object `r`, replacing any earlier one by the same key.
532
    pub fn put_signature(&self, r: &ObjectRef, sig: &Signature) -> Result<()> {
533
        let mut sigs = self.signatures(r)?;
534
        sigs.retain(|s| s.key != sig.key);
535
        sigs.push(sig.clone());
5b1114a0 Andy McCall ↶ 536
        sigs.sort_by_key(|s| s.key);
1983686f Andy McCall ↶ 537
        let path = self.sig_path(r);
538
        fs::create_dir_all(path.parent().unwrap())?;
539
        let mut nonce = [0u8; 8];
540
        rand::rngs::OsRng.fill_bytes(&mut nonce);
541
        let tmp = self.dir.join(format!("sigs-tmp-{}", hex::encode(nonce)));
542
        {
543
            let mut f = fs::File::create(&tmp)?;
544
            f.write_all(&Signature::encode_list(&sigs))?;
545
            f.sync_all()?;
546
        }
547
        fs::rename(&tmp, &path)?;
548
        Ok(())
549
    }
550
d02e545d Andy McCall ↶ 551
    /// Every object reference in the store, loose and packed (slow; for gc and tests).
ff0af1ee unknown 552
    pub fn list(&self) -> io::Result<Vec<ObjectRef>> {
d02e545d Andy McCall ↶ 553
        let mut out: Vec<ObjectRef> = self.loose_objects()?.into_iter().map(|(r, _)| r).collect();
554
        for p in self.packs.borrow().iter() {
555
            out.extend(p.refs.iter().copied());
ff0af1ee unknown 556
        }
557
        out.sort();
d02e545d Andy McCall ↶ 558
        out.dedup();
ff0af1ee unknown 559
        Ok(out)
560
    }
561
}
562
563
#[cfg(test)]
564
mod tests {
565
    use super::*;
566
567
    #[test]
568
    fn put_get_verify() {
569
        let dir = tempfile::tempdir().unwrap();
570
        let s = Store::open(dir.path().join("objects")).unwrap();
571
        let r = s.put(ObjectType::Chunk, b"hello").unwrap();
572
        assert!(s.has(&r));
573
        assert_eq!(s.get(&r).unwrap(), (ObjectType::Chunk, b"hello".to_vec()));
574
        assert_eq!(s.put(ObjectType::Chunk, b"hello").unwrap(), r);
575
        assert_eq!(s.list().unwrap(), vec![r]);
576
        assert!(matches!(s.get_typed(&r, ObjectType::Blob), Err(StoreError::WrongType(..))));
577
        assert!(matches!(s.get(&ObjectRef([9; 32])), Err(StoreError::NotFound(_))));
578
579
        // Tamper: flip a byte, read must fail.
580
        let path = s.path_for(&r);
581
        let mut data = fs::read(&path).unwrap();
582
        let last = data.len() - 1;
583
        data[last] ^= 1;
584
        fs::write(&path, data).unwrap();
585
        assert!(matches!(s.get(&r), Err(StoreError::Corrupt(..))));
586
    }
587
588
    #[test]
d02e545d Andy McCall ↶ 589
    fn packing_keeps_every_object_readable() {
590
        let dir = tempfile::tempdir().unwrap();
591
        let s = Store::open(dir.path()).unwrap();
592
        let mut refs = Vec::new();
593
        for i in 0..300u32 {
594
            let body = format!("object {i} ").repeat(1 + (i % 7) as usize);
595
            refs.push((s.put(ObjectType::Chunk, body.as_bytes()).unwrap(), body));
596
        }
597
        let c = crate::objects::tests::sample_change();
598
        let cr = s.put_change(&c).unwrap();
599
        assert_eq!(s.loose_count().unwrap(), 301);
600
        let report = s.pack_loose().unwrap();
601
        assert_eq!(report.objects, 301);
602
        assert!(report.packed_bytes < report.raw_bytes, "{report:?}");
603
        assert_eq!(s.loose_count().unwrap(), 0);
604
        assert_eq!(s.pack_count(), 1);
605
        for (r, body) in &refs {
606
            assert!(s.has(r));
607
            assert_eq!(s.get(r).unwrap(), (ObjectType::Chunk, body.as_bytes().to_vec()));
608
        }
609
        assert_eq!(s.get_change(&cr).unwrap(), c);
610
        assert_eq!(s.list().unwrap().len(), 301);
611
        // Re-putting a packed object is a no-op; new objects go loose and a
612
        // second pack coexists with the first.
613
        assert_eq!(s.put(ObjectType::Chunk, refs[0].1.as_bytes()).unwrap(), refs[0].0);
614
        assert_eq!(s.loose_count().unwrap(), 0);
615
        let extra = s.put(ObjectType::Chunk, b"after packing").unwrap();
616
        s.pack_loose().unwrap();
617
        assert_eq!(s.pack_count(), 2);
618
        assert_eq!(s.get(&extra).unwrap().1, b"after packing");
619
        // A reopened store sees the packs.
620
        let s2 = Store::open(dir.path()).unwrap();
621
        assert_eq!(s2.pack_count(), 2);
622
        assert_eq!(s2.get(&refs[5].0).unwrap().1, refs[5].1.as_bytes());
623
        assert!(matches!(s2.get(&ObjectRef([9; 32])), Err(StoreError::NotFound(_))));
624
        // Corrupting an object's body inside the pack is detected on read.
625
        let target = refs[5].0;
626
        let (pack_path, offset) = {
627
            let packs = s2.packs.borrow();
628
            let (i, e) = s2.find_packed(&target).unwrap();
629
            (packs[i].path.clone(), e.offset as usize)
630
        };
631
        let mut data = fs::read(&pack_path).unwrap();
632
        data[offset] ^= 0xff;
633
        fs::write(&pack_path, data).unwrap();
634
        let s3 = Store::open(dir.path()).unwrap();
635
        assert!(matches!(s3.get(&target), Err(StoreError::Corrupt(..))));
636
        assert_eq!(s3.get(&refs[6].0).unwrap().1, refs[6].1.as_bytes(), "other objects are unaffected");
637
    }
638
639
    #[test]
135212de Andy McCall ↶ 640
    fn many_packs_merge_into_one() {
641
        let dir = tempfile::tempdir().unwrap();
642
        let s = Store::open(dir.path()).unwrap();
643
        let mut refs = Vec::new();
644
        for i in 0..(MAX_PACKS + 2) {
645
            refs.push(s.put(ObjectType::Chunk, format!("pack {i}").as_bytes()).unwrap());
646
            s.pack_loose().unwrap();
647
        }
648
        assert_eq!(s.pack_count(), MAX_PACKS + 2);
649
        assert_eq!(s.merge_packs().unwrap(), MAX_PACKS + 2);
650
        assert_eq!(s.pack_count(), 1);
651
        assert_eq!(s.merge_packs().unwrap(), 0);
652
        for (i, r) in refs.iter().enumerate() {
653
            assert_eq!(s.get(r).unwrap().1, format!("pack {i}").as_bytes());
654
        }
655
        assert_eq!(Store::open(dir.path()).unwrap().pack_count(), 1);
656
    }
657
658
    #[test]
ba160f60 Andy McCall ↶ 659
    fn prune_removes_unreachable_loose_and_packed_objects() {
660
        use std::collections::HashSet;
661
        let dir = tempfile::tempdir().unwrap();
662
        let s = Store::open(dir.path()).unwrap();
663
        let keep_a = s.put(ObjectType::Chunk, b"keep a").unwrap();
664
        let drop_a = s.put(ObjectType::Chunk, b"drop a").unwrap();
665
        s.pack_loose().unwrap();
666
        let keep_b = s.put(ObjectType::Chunk, b"keep b").unwrap();
667
        let drop_b = s.put(ObjectType::Chunk, b"drop b").unwrap();
668
        let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
669
        let id = sk.identity("a", "b", 0);
670
        let key = s.put_identity(&id).unwrap();
671
        s.put_signature(&drop_b, &sk.sign(crate::identity::Domain::Change, &key, &drop_b)).unwrap();
672
        s.index_seal(&drop_a, &ObjectRef([3; 32])).unwrap();
673
        let keep: HashSet<ObjectRef> = [keep_a, keep_b, key].into_iter().collect();
674
        // Everything is too young under a grace period.
675
        let r = s.prune(&keep, std::time::Duration::from_secs(3600)).unwrap();
676
        assert_eq!((r.objects, r.deferred), (0, 2));
677
        let r = s.prune(&keep, std::time::Duration::ZERO).unwrap();
678
        assert_eq!(r.objects, 2, "{r:?}");
679
        assert!(s.has(&keep_a) && s.has(&keep_b) && s.has(&key));
680
        assert!(!s.has(&drop_a) && !s.has(&drop_b));
681
        assert!(s.signatures(&drop_b).unwrap().is_empty());
682
        assert!(s.seals_for(&drop_a).unwrap().is_empty());
683
        assert_eq!(s.pack_count(), 1);
684
        assert_eq!(s.get(&keep_a).unwrap().1, b"keep a");
685
        assert_eq!(s.list().unwrap().len(), 3);
686
    }
687
688
    #[test]
1983686f Andy McCall ↶ 689
    fn signatures_are_stored_beside_objects() {
690
        use crate::identity::{Domain, SecretKey};
691
        let dir = tempfile::tempdir().unwrap();
692
        let s = Store::open(dir.path()).unwrap();
693
        let r = s.put(ObjectType::Chunk, b"x").unwrap();
694
        assert!(s.signatures(&r).unwrap().is_empty());
695
        let sk = SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
696
        let id = sk.identity("a", "b", 0);
697
        let key = s.put_identity(&id).unwrap();
698
        assert_eq!(s.get_identity(&key).unwrap(), id);
699
        let sig = sk.sign(Domain::Change, &key, &r);
700
        s.put_signature(&r, &sig).unwrap();
701
        s.put_signature(&r, &sig).unwrap();
702
        assert_eq!(s.signatures(&r).unwrap(), vec![sig]);
703
        // The object itself is untouched by its signatures.
704
        assert_eq!(s.get(&r).unwrap().1, b"x");
705
    }
706
707
    #[test]
ff0af1ee unknown 708
    fn change_round_trip_through_store() {
709
        let dir = tempfile::tempdir().unwrap();
710
        let s = Store::open(dir.path()).unwrap();
711
        let c = crate::objects::tests::sample_change();
712
        let r = s.put_change(&c).unwrap();
713
        assert_eq!(r, c.hash());
714
        assert_eq!(s.get_change(&r).unwrap(), c);
715
    }
716
}