OXYGENHUB
# e15dc29f109b oxygen / crates / oxy-core / src / store.rs
52.9 KiB · Rust · last changed Execution receipts: the receipt object and attestation 1 month ago source blame history raw
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`.
10
11
use crate::attest::Attest;
12
use crate::hash::{hash_object, header, ObjectRef, ObjectType};
13
use crate::identity::{Identity, Signature};
14
use crate::objects::{Change, ObjectError, StateObject};
15
use crate::receipt::Receipt;
16
use rand::RngCore;
17
use std::cell::RefCell;
18
use std::fs;
19
use std::io::{self, Read, Seek, SeekFrom, Write};
20
use std::path::{Path, PathBuf};
21
22
const PACK_MAGIC: &[u8; 8] = b"OXYPACK1";
23
const IDX_MAGIC: &[u8; 8] = b"OXYIDX01";
24
const IDX_ENTRY: usize = 32 + 8 + 4 + 4 + 1 + 1;
25
const FLAG_ZSTD: u8 = 1;
26
const ZSTD_LEVEL: i32 = 3;
27
/// Largest object a pack entry may claim; anything bigger is corruption,
28
/// and refusing it keeps a bad index from driving huge allocations.
29
const MAX_OBJECT_LEN: u32 = 1 << 30;
30
31
/// An object read from the store: its type and body.
32
pub type Object = (ObjectType, Vec<u8>);
33
34
#[derive(Clone, Copy, Debug)]
35
struct PackEntry {
36
    offset: u64,
37
    stored_len: u32,
38
    raw_len: u32,
39
    ty: ObjectType,
40
    flags: u8,
41
}
42
43
/// One pack's index, fully in memory and sorted by ObjectRef.
44
#[derive(Debug)]
45
struct PackIndex {
46
    path: PathBuf,
47
    refs: Vec<ObjectRef>,
48
    entries: Vec<PackEntry>,
49
}
50
51
impl PackIndex {
52
    fn load(idx_path: &Path, pack_path: &Path) -> Result<PackIndex> {
53
        let data = fs::read(idx_path)?;
54
        let bad =
55
            |m: &str| StoreError::Corrupt(ObjectRef::ZERO, format!("{}: {m}", idx_path.display()));
56
        if data.len() < 12 || &data[..8] != IDX_MAGIC {
57
            return Err(bad("bad index header"));
58
        }
59
        let count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
60
        if data.len() != 12 + count * IDX_ENTRY {
61
            return Err(bad("index length mismatch"));
62
        }
63
        let pack_len = fs::metadata(pack_path)?.len();
64
        let mut refs = Vec::with_capacity(count);
65
        let mut entries = Vec::with_capacity(count);
66
        for i in 0..count {
67
            let e = &data[12 + i * IDX_ENTRY..12 + (i + 1) * IDX_ENTRY];
68
            let r = ObjectRef::from_slice(&e[..32]).unwrap();
69
            if let Some(prev) = refs.last() {
70
                if r <= *prev {
71
                    return Err(bad("index not sorted"));
72
                }
73
            }
74
            refs.push(r);
75
            let entry = PackEntry {
76
                offset: u64::from_le_bytes(e[32..40].try_into().unwrap()),
77
                stored_len: u32::from_le_bytes(e[40..44].try_into().unwrap()),
78
                raw_len: u32::from_le_bytes(e[44..48].try_into().unwrap()),
79
                ty: ObjectType::ALL
80
                    .get(e[48] as usize)
81
                    .copied()
82
                    .ok_or_else(|| bad("unknown object type"))?,
83
                flags: e[49],
84
            };
85
            if entry.stored_len > MAX_OBJECT_LEN
86
                || entry.raw_len > MAX_OBJECT_LEN
87
                || entry.offset.saturating_add(entry.stored_len as u64) > pack_len
88
            {
89
                return Err(bad("index entry points outside the pack"));
90
            }
91
            entries.push(entry);
92
        }
93
        Ok(PackIndex {
94
            path: pack_path.to_path_buf(),
95
            refs,
96
            entries,
97
        })
98
    }
99
100
    fn find(&self, r: &ObjectRef) -> Option<PackEntry> {
101
        self.refs.binary_search(r).ok().map(|i| self.entries[i])
102
    }
103
104
    fn read(&self, r: &ObjectRef, e: PackEntry) -> Result<(ObjectType, Vec<u8>)> {
105
        let mut f = fs::File::open(&self.path)?;
106
        f.seek(SeekFrom::Start(e.offset))?;
107
        let mut stored = vec![0u8; e.stored_len as usize];
108
        f.read_exact(&mut stored)?;
109
        let body = if e.flags & FLAG_ZSTD != 0 {
110
            zstd::bulk::decompress(&stored, e.raw_len as usize)
111
                .map_err(|err| StoreError::Corrupt(*r, format!("zstd: {err}")))?
112
        } else {
113
            stored
114
        };
115
        if body.len() != e.raw_len as usize || hash_object(e.ty, &body) != *r {
116
            return Err(StoreError::Corrupt(
117
                *r,
118
                "packed object does not match its reference".into(),
119
            ));
120
        }
121
        Ok((e.ty, body))
122
    }
123
}
124
125
fn type_index(ty: ObjectType) -> u8 {
126
    ObjectType::ALL.iter().position(|t| *t == ty).unwrap() as u8
127
}
128
129
/// What `pack_loose` did.
130
#[derive(Debug, Clone, Default, PartialEq, Eq)]
131
pub struct PackReport {
132
    pub objects: usize,
133
    pub raw_bytes: u64,
134
    pub packed_bytes: u64,
135
}
136
137
/// Packs tolerated before `merge_packs` folds them into one (spec §13.4).
138
pub const MAX_PACKS: usize = 16;
139
140
/// What `prune` did.
141
#[derive(Debug, Clone, Default, PartialEq, Eq)]
142
pub struct PruneReport {
143
    pub objects: usize,
144
    pub bytes: u64,
145
    /// Unreachable objects left alone because they are younger than the grace period.
146
    pub deferred: usize,
147
}
148
149
#[derive(Debug, thiserror::Error)]
150
pub enum StoreError {
151
    #[error("io: {0}")]
152
    Io(#[from] io::Error),
153
    #[error("corrupt object {0}: {1}")]
154
    Corrupt(ObjectRef, String),
155
    #[error("object {0}")]
156
    Object(#[from] ObjectError),
157
    #[error("object {0} is a {1}, expected {2}")]
158
    WrongType(ObjectRef, &'static str, &'static str),
159
    #[error("object {0} not found")]
160
    NotFound(ObjectRef),
161
}
162
163
pub type Result<T> = std::result::Result<T, StoreError>;
164
165
#[derive(Debug)]
166
pub struct Store {
167
    dir: PathBuf,
168
    packs: RefCell<Vec<PackIndex>>,
169
    /// An open write batch (spec §13.4): objects go here instead of to loose
170
    /// files, and are written out with one `fsync` when the batch finishes.
171
    pending: RefCell<Option<Pending>>,
172
    batch_depth: std::cell::Cell<u32>,
173
}
174
175
/// Objects written since `begin_batch`. Small batches stay in memory and
176
/// become loose objects; once `SPILL_OBJECTS` or `SPILL_BYTES` is exceeded
177
/// the batch streams into a pack file instead.
178
#[derive(Debug)]
179
struct Pending {
180
    buffered: Vec<(ObjectType, Vec<u8>)>,
181
    buffered_bytes: usize,
182
    lookup: std::collections::HashMap<ObjectRef, PendingEntry>,
183
    pack: Option<PendingPack>,
184
    report: PackReport,
185
}
186
187
#[derive(Debug, Clone, Copy)]
188
enum PendingEntry {
189
    Buffered(usize),
190
    Packed(PackEntry),
191
}
192
193
#[derive(Debug)]
194
struct PendingPack {
195
    id: String,
196
    tmp_path: PathBuf,
197
    writer: io::BufWriter<fs::File>,
198
    offset: u64,
199
    index: Vec<(ObjectRef, PackEntry)>,
200
}
201
202
const SPILL_OBJECTS: usize = 256;
203
const SPILL_BYTES: usize = 8 << 20;
204
205
/// Finishes the enclosing batch when dropped; call `finish` to see errors.
206
pub struct Batch<'a> {
207
    store: &'a Store,
208
    done: bool,
209
}
210
211
impl Batch<'_> {
212
    pub fn finish(mut self) -> Result<()> {
213
        self.done = true;
214
        self.store.finish_batch()
215
    }
216
}
217
218
impl Drop for Batch<'_> {
219
    fn drop(&mut self) {
220
        if !self.done {
221
            let _ = self.store.finish_batch();
222
        }
223
    }
224
}
225
226
impl Store {
227
    pub fn open(dir: impl Into<PathBuf>) -> io::Result<Store> {
228
        let dir = dir.into();
229
        fs::create_dir_all(dir.join("loose"))?;
230
        let store = Store {
231
            dir,
232
            packs: RefCell::new(Vec::new()),
233
            pending: RefCell::new(None),
234
            batch_depth: std::cell::Cell::new(0),
235
        };
236
        store
237
            .reload_packs()
238
            .map_err(|e| io::Error::other(e.to_string()))?;
239
        Ok(store)
240
    }
241
242
    fn reload_packs(&self) -> Result<()> {
243
        let mut packs = Vec::new();
244
        let mut names: Vec<PathBuf> = fs::read_dir(&self.dir)?
245
            .filter_map(|e| e.ok().map(|e| e.path()))
246
            .filter(|p| {
247
                p.extension().is_some_and(|x| x == "idx")
248
                    && p.file_name()
249
                        .is_some_and(|n| n.to_string_lossy().starts_with("pack-"))
250
            })
251
            .collect();
252
        names.sort();
253
        for idx in names {
254
            let pack = idx.with_extension("pack");
255
            if pack.is_file() {
256
                packs.push(PackIndex::load(&idx, &pack)?);
257
            }
258
        }
259
        *self.packs.borrow_mut() = packs;
260
        Ok(())
261
    }
262
263
    pub fn pack_count(&self) -> usize {
264
        self.packs.borrow().len()
265
    }
266
267
    fn find_packed(&self, r: &ObjectRef) -> Option<(usize, PackEntry)> {
268
        self.packs
269
            .borrow()
270
            .iter()
271
            .enumerate()
272
            .find_map(|(i, p)| p.find(r).map(|e| (i, e)))
273
    }
274
275
    /// Loose objects on disk, with their paths.
276
    fn loose_objects(&self) -> io::Result<Vec<(ObjectRef, PathBuf)>> {
277
        let mut out = Vec::new();
278
        let loose = self.dir.join("loose");
279
        for d in fs::read_dir(&loose)? {
280
            let d = d?;
281
            if !d.file_type()?.is_dir() {
282
                continue;
283
            }
284
            let prefix = d.file_name().to_string_lossy().to_string();
285
            for f in fs::read_dir(d.path())? {
286
                let f = f?;
287
                let name = f.file_name().to_string_lossy().to_string();
288
                if let Some(r) = ObjectRef::from_hex(&format!("{prefix}{name}")) {
289
                    out.push((r, f.path()));
290
                }
291
            }
292
        }
293
        out.sort();
294
        Ok(out)
295
    }
296
297
    pub fn loose_count(&self) -> io::Result<usize> {
298
        Ok(self.loose_objects()?.len())
299
    }
300
301
    /// Move every loose object into a new pack (spec §13.4). Loose files are
302
    /// deleted only after the pack and its index are durable.
303
    pub fn pack_loose(&self) -> Result<PackReport> {
304
        let loose = self.loose_objects()?;
305
        self.write_pack(
306
            &loose.iter().map(|(r, _)| *r).collect::<Vec<_>>(),
307
            &loose.iter().map(|(_, p)| p.clone()).collect::<Vec<_>>(),
308
            &[],
309
        )
310
    }
311
312
    /// Fold every pack into one when there are more than `MAX_PACKS`
313
    /// (spec §13.4). Returns how many packs were merged.
314
    pub fn merge_packs(&self) -> Result<usize> {
315
        let (refs, paths): (Vec<ObjectRef>, Vec<PathBuf>) = {
316
            let packs = self.packs.borrow();
317
            if packs.len() <= MAX_PACKS {
318
                return Ok(0);
319
            }
320
            (
321
                packs.iter().flat_map(|p| p.refs.iter().copied()).collect(),
322
                packs.iter().map(|p| p.path.clone()).collect(),
323
            )
324
        };
325
        let n = paths.len();
326
        self.write_pack(&refs, &[], &paths)?;
327
        Ok(n)
328
    }
329
330
    /// Delete every object not in `keep` (spec §21). Loose objects and packs
331
    /// younger than `grace` are left alone, since a concurrent writer may not
332
    /// have recorded its references yet. Packs holding unreachable objects
333
    /// are rewritten without them. Signature sidecars and seal-index entries
334
    /// of deleted objects go too.
335
    pub fn prune(
336
        &self,
337
        keep: &std::collections::HashSet<ObjectRef>,
338
        grace: std::time::Duration,
339
    ) -> Result<PruneReport> {
340
        let now = std::time::SystemTime::now();
341
        let old_enough = |m: &fs::Metadata| {
342
            m.modified()
343
                .map(|t| now.duration_since(t).unwrap_or_default() >= grace)
344
                .unwrap_or(true)
345
        };
346
        let mut report = PruneReport::default();
347
        // Loose objects.
348
        for (r, path) in self.loose_objects()? {
349
            if keep.contains(&r) {
350
                continue;
351
            }
352
            let meta = fs::metadata(&path)?;
353
            if !old_enough(&meta) {
354
                report.deferred += 1;
355
                continue;
356
            }
357
            report.bytes += meta.len();
358
            report.objects += 1;
359
            fs::remove_file(&path)?;
360
            let _ = fs::remove_dir(path.parent().unwrap());
361
            self.remove_sidecars(&r)?;
362
        }
363
        // Packs: rewrite those that hold unreachable objects.
364
        let mut keep_refs: Vec<ObjectRef> = Vec::new();
365
        let mut old_paths: Vec<PathBuf> = Vec::new();
366
        let mut rewrite = false;
367
        for p in self.packs.borrow().iter() {
368
            let meta = fs::metadata(&p.path)?;
369
            let unreachable = p.refs.iter().filter(|r| !keep.contains(r)).count();
370
            if unreachable == 0 {
371
                continue;
372
            }
373
            if !old_enough(&meta) {
374
                report.deferred += unreachable;
375
                continue;
376
            }
377
            rewrite = true;
378
            old_paths.push(p.path.clone());
379
            for (r, e) in p.refs.iter().zip(p.entries.iter()) {
380
                if keep.contains(r) {
381
                    keep_refs.push(*r);
382
                } else {
383
                    report.objects += 1;
384
                    report.bytes += e.stored_len as u64;
385
                }
386
            }
387
        }
388
        if rewrite {
389
            let dropped: Vec<ObjectRef> = {
390
                let packs = self.packs.borrow();
391
                packs
392
                    .iter()
393
                    .filter(|p| old_paths.contains(&p.path))
394
                    .flat_map(|p| p.refs.iter().copied())
395
                    .filter(|r| !keep.contains(r))
396
                    .collect()
397
            };
398
            self.write_pack(&keep_refs, &[], &old_paths)?;
399
            for r in dropped {
400
                self.remove_sidecars(&r)?;
401
            }
402
        }
403
        Ok(report)
404
    }
405
406
    fn remove_sidecars(&self, r: &ObjectRef) -> Result<()> {
407
        let sidecars = [
408
            self.sig_path(r),
409
            self.index_path("seals", r),
410
            self.index_path("receipts", r),
411
        ];
412
        for path in sidecars {
413
            match fs::remove_file(&path) {
414
                Ok(()) => {
415
                    let _ = fs::remove_dir(path.parent().unwrap());
416
                }
417
                Err(e) if e.kind() == io::ErrorKind::NotFound => {}
418
                Err(e) => return Err(e.into()),
419
            }
420
        }
421
        Ok(())
422
    }
423
424
    /// Write `refs` into a new pack, then delete `loose_paths` and `old_packs`.
425
    /// Write the index for a complete, synced temporary pack file and move
426
    /// both into place; `index` must be sorted by ObjectRef.
427
    fn finish_pack_files(
428
        &self,
429
        id: &str,
430
        tmp_pack: &Path,
431
        index: &[(ObjectRef, PackEntry)],
432
    ) -> Result<()> {
433
        let pack_path = self.dir.join(format!("pack-{id}.pack"));
434
        let idx_path = self.dir.join(format!("pack-{id}.idx"));
435
        let tmp_idx = self.dir.join(format!("tmp-{id}.idx"));
436
        {
437
            let mut f = io::BufWriter::new(fs::File::create(&tmp_idx)?);
438
            f.write_all(IDX_MAGIC)?;
439
            f.write_all(&(index.len() as u32).to_le_bytes())?;
440
            for (r, e) in index {
441
                f.write_all(&r.0)?;
442
                f.write_all(&e.offset.to_le_bytes())?;
443
                f.write_all(&e.stored_len.to_le_bytes())?;
444
                f.write_all(&e.raw_len.to_le_bytes())?;
445
                f.write_all(&[type_index(e.ty), e.flags])?;
446
            }
447
            f.flush()?;
448
            f.get_ref().sync_all()?;
449
        }
450
        // The pack first: an index never names a pack that is not there.
451
        crate::durable::rename(tmp_pack, &pack_path)?;
452
        crate::durable::rename(&tmp_idx, &idx_path)?;
453
        self.reload_packs()
454
    }
455
456
    fn write_pack(
457
        &self,
458
        refs: &[ObjectRef],
459
        loose_paths: &[PathBuf],
460
        old_packs: &[PathBuf],
461
    ) -> Result<PackReport> {
462
        if refs.is_empty() {
463
            for p in old_packs {
464
                fs::remove_file(p)?;
465
                fs::remove_file(p.with_extension("idx"))?;
466
            }
467
            self.reload_packs()?;
468
            return Ok(PackReport::default());
469
        }
470
        let mut refs: Vec<ObjectRef> = refs.to_vec();
471
        refs.sort();
472
        refs.dedup();
473
        let mut nonce = [0u8; 8];
474
        rand::rngs::OsRng.fill_bytes(&mut nonce);
475
        let id = hex::encode(nonce);
476
        let tmp_pack = self.dir.join(format!("tmp-{id}.pack"));
477
        let mut report = PackReport::default();
478
        let mut index: Vec<(ObjectRef, PackEntry)> = Vec::with_capacity(refs.len());
479
        {
480
            let mut f = io::BufWriter::new(fs::File::create(&tmp_pack)?);
481
            f.write_all(PACK_MAGIC)?;
482
            let mut offset = PACK_MAGIC.len() as u64;
483
            for r in &refs {
484
                let (ty, body) = self.get(r)?;
485
                let (flags, stored) = if self.is_nocompress(r) {
486
                    (0, body.clone())
487
                } else {
488
                    let compressed = zstd::bulk::compress(&body, ZSTD_LEVEL)
489
                        .map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?;
490
                    if compressed.len() < body.len() {
491
                        (FLAG_ZSTD, compressed)
492
                    } else {
493
                        (0, body.clone())
494
                    }
495
                };
496
                let header_len = 1 + 1 + 4 + 4 + 32;
497
                f.write_all(&[type_index(ty), flags])?;
498
                f.write_all(&(stored.len() as u32).to_le_bytes())?;
499
                f.write_all(&(body.len() as u32).to_le_bytes())?;
500
                f.write_all(&r.0)?;
501
                f.write_all(&stored)?;
502
                index.push((
503
                    *r,
504
                    PackEntry {
505
                        offset: offset + header_len,
506
                        stored_len: stored.len() as u32,
507
                        raw_len: body.len() as u32,
508
                        ty,
509
                        flags,
510
                    },
511
                ));
512
                offset += header_len + stored.len() as u64;
513
                report.objects += 1;
514
                report.raw_bytes += body.len() as u64;
515
                report.packed_bytes += stored.len() as u64;
516
            }
517
            f.flush()?;
518
            f.get_ref().sync_all()?;
519
        }
520
        self.finish_pack_files(&id, &tmp_pack, &index)?;
521
        for p in old_packs {
522
            fs::remove_file(p)?;
523
            fs::remove_file(p.with_extension("idx"))?;
524
        }
525
        self.reload_packs()?;
526
        for path in loose_paths {
527
            fs::remove_file(path)?;
528
            if let Some(dir) = path.parent() {
529
                let _ = fs::remove_dir(dir);
530
            }
531
        }
532
        Ok(report)
533
    }
534
535
    pub fn dir(&self) -> &Path {
536
        &self.dir
537
    }
538
539
    pub(crate) fn path_for(&self, r: &ObjectRef) -> PathBuf {
540
        let h = r.to_hex();
541
        self.dir.join("loose").join(&h[..2]).join(&h[2..])
542
    }
543
544
    pub fn has(&self, r: &ObjectRef) -> bool {
545
        self.path_for(r).is_file()
546
            || self.find_packed(r).is_some()
547
            || self
548
                .pending
549
                .borrow()
550
                .as_ref()
551
                .is_some_and(|p| p.lookup.contains_key(r))
552
    }
553
554
    /// Start a write batch: until the matching `finish`, `put` collects
555
    /// objects and writes them out together, with one `fsync` for a large
556
    /// batch (a pack) instead of one per object. Batches nest; only the
557
    /// outermost does the work. Objects are readable while pending.
558
    pub fn begin_batch(&self) -> Result<Batch<'_>> {
559
        let depth = self.batch_depth.get();
560
        if depth == 0 {
561
            *self.pending.borrow_mut() = Some(Pending {
562
                buffered: vec![],
563
                buffered_bytes: 0,
564
                lookup: Default::default(),
565
                pack: None,
566
                report: PackReport::default(),
567
            });
568
        }
569
        self.batch_depth.set(depth + 1);
570
        Ok(Batch {
571
            store: self,
572
            done: false,
573
        })
574
    }
575
576
    fn finish_batch(&self) -> Result<()> {
577
        let depth = self.batch_depth.get();
578
        if depth == 0 {
579
            return Ok(());
580
        }
581
        self.batch_depth.set(depth - 1);
582
        if depth > 1 {
583
            return Ok(());
584
        }
585
        let Some(pending) = self.pending.borrow_mut().take() else {
586
            return Ok(());
587
        };
588
        self.write_pending(pending)
589
    }
590
591
    /// Make every object written so far durable, even inside an open
592
    /// batch, which stays open. Call before a reference that names them
593
    /// is moved (spec §13.6): the operation head, a bookmark pointer.
594
    pub fn flush(&self) -> Result<()> {
595
        let Some(pending) = self.pending.borrow_mut().take() else {
596
            return Ok(());
597
        };
598
        self.write_pending(pending)?;
599
        *self.pending.borrow_mut() = Some(Pending {
600
            buffered: vec![],
601
            buffered_bytes: 0,
602
            lookup: Default::default(),
603
            pack: None,
604
            report: PackReport::default(),
605
        });
606
        Ok(())
607
    }
608
609
    fn write_pending(&self, pending: Pending) -> Result<()> {
610
        match pending.pack {
611
            None => {
612
                for (ty, body) in &pending.buffered {
613
                    self.put_loose(*ty, body)?;
614
                }
615
                Ok(())
616
            }
617
            Some(mut pack) => {
618
                pack.writer.flush()?;
619
                pack.writer.get_ref().sync_all()?;
620
                drop(pack.writer);
621
                pack.index.sort_by_key(|(r, _)| *r);
622
                self.finish_pack_files(&pack.id, &pack.tmp_path, &pack.index)?;
623
                Ok(())
624
            }
625
        }
626
    }
627
628
    /// Whether a batch is open and holds `r`.
629
    fn pending_get(&self, r: &ObjectRef) -> Option<Result<(ObjectType, Vec<u8>)>> {
630
        let mut guard = self.pending.borrow_mut();
631
        let pending = guard.as_mut()?;
632
        let entry = *pending.lookup.get(r)?;
633
        Some(match entry {
634
            PendingEntry::Buffered(i) => {
635
                let (ty, body) = &pending.buffered[i];
636
                Ok((*ty, body.clone()))
637
            }
638
            PendingEntry::Packed(e) => {
639
                let Some(pack) = pending.pack.as_mut() else {
640
                    return Some(Err(StoreError::NotFound(*r)));
641
                };
642
                match pack.writer.flush() {
643
                    Ok(()) => PackIndex {
644
                        path: pack.tmp_path.clone(),
645
                        refs: vec![],
646
                        entries: vec![],
647
                    }
648
                    .read(r, e),
649
                    Err(err) => Err(err.into()),
650
                }
651
            }
652
        })
653
    }
654
655
    /// Append one object to the pending pack file.
656
    fn pending_pack_write(
657
        &self,
658
        pack: &mut PendingPack,
659
        r: &ObjectRef,
660
        ty: ObjectType,
661
        body: &[u8],
662
        report: &mut PackReport,
663
    ) -> Result<PackEntry> {
664
        let (flags, stored) = if self.is_nocompress(r) {
665
            (0, body.to_vec())
666
        } else {
667
            let compressed = zstd::bulk::compress(body, ZSTD_LEVEL)
668
                .map_err(|e| StoreError::Corrupt(*r, format!("zstd: {e}")))?;
669
            if compressed.len() < body.len() {
670
                (FLAG_ZSTD, compressed)
671
            } else {
672
                (0, body.to_vec())
673
            }
674
        };
675
        let header_len = 1 + 1 + 4 + 4 + 32;
676
        pack.writer.write_all(&[type_index(ty), flags])?;
677
        pack.writer
678
            .write_all(&(stored.len() as u32).to_le_bytes())?;
679
        pack.writer.write_all(&(body.len() as u32).to_le_bytes())?;
680
        pack.writer.write_all(&r.0)?;
681
        pack.writer.write_all(&stored)?;
682
        let entry = PackEntry {
683
            offset: pack.offset + header_len,
684
            stored_len: stored.len() as u32,
685
            raw_len: body.len() as u32,
686
            ty,
687
            flags,
688
        };
689
        pack.offset += header_len + stored.len() as u64;
690
        pack.index.push((*r, entry));
691
        report.objects += 1;
692
        report.raw_bytes += body.len() as u64;
693
        report.packed_bytes += stored.len() as u64;
694
        Ok(entry)
695
    }
696
697
    fn put_pending(&self, r: ObjectRef, ty: ObjectType, body: &[u8]) -> Result<()> {
698
        let mut guard = self.pending.borrow_mut();
699
        let pending = guard.as_mut().expect("batch open");
700
        if pending.lookup.contains_key(&r) {
701
            return Ok(());
702
        }
703
        if pending.pack.is_none()
704
            && pending.buffered.len() < SPILL_OBJECTS
705
            && pending.buffered_bytes + body.len() <= SPILL_BYTES
706
        {
707
            pending.buffered.push((ty, body.to_vec()));
708
            pending.buffered_bytes += body.len();
709
            pending
710
                .lookup
711
                .insert(r, PendingEntry::Buffered(pending.buffered.len() - 1));
712
            return Ok(());
713
        }
714
        if pending.pack.is_none() {
715
            // Spill: open the pack and move the buffered objects into it.
716
            let mut nonce = [0u8; 8];
717
            rand::rngs::OsRng.fill_bytes(&mut nonce);
718
            let id = hex::encode(nonce);
719
            let tmp_path = self.dir.join(format!("tmp-{id}.pack"));
720
            let mut writer = io::BufWriter::new(fs::File::create(&tmp_path)?);
721
            writer.write_all(PACK_MAGIC)?;
722
            let mut pack = PendingPack {
723
                id,
724
                tmp_path,
725
                writer,
726
                offset: PACK_MAGIC.len() as u64,
727
                index: vec![],
728
            };
729
            let buffered = std::mem::take(&mut pending.buffered);
730
            pending.buffered_bytes = 0;
731
            let mut report = std::mem::take(&mut pending.report);
732
            for (bty, bbody) in &buffered {
733
                let br = hash_object(*bty, bbody);
734
                let e = self.pending_pack_write(&mut pack, &br, *bty, bbody, &mut report)?;
735
                pending.lookup.insert(br, PendingEntry::Packed(e));
736
            }
737
            pending.report = report;
738
            pending.pack = Some(pack);
739
        }
740
        let mut report = std::mem::take(&mut pending.report);
741
        let pack = pending.pack.as_mut().unwrap();
742
        let e = self.pending_pack_write(pack, &r, ty, body, &mut report)?;
743
        pending.report = report;
744
        pending.lookup.insert(r, PendingEntry::Packed(e));
745
        Ok(())
746
    }
747
748
    /// Store `body` as an object of type `ty`; idempotent.
749
    pub fn put(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
750
        let r = hash_object(ty, body);
751
        let path = self.path_for(&r);
752
        if path.is_file() || self.find_packed(&r).is_some() {
753
            return Ok(r);
754
        }
755
        if self.pending.borrow().is_some() {
756
            self.put_pending(r, ty, body)?;
757
            return Ok(r);
758
        }
759
        self.put_loose(ty, body)?;
760
        Ok(r)
761
    }
762
763
    /// Write one loose object atomically with its own `fsync`.
764
    fn put_loose(&self, ty: ObjectType, body: &[u8]) -> Result<ObjectRef> {
765
        let r = hash_object(ty, body);
766
        let path = self.path_for(&r);
767
        if path.is_file() {
768
            return Ok(r);
769
        }
770
        let fanout = path.parent().unwrap();
771
        if !fanout.is_dir() {
772
            fs::create_dir_all(fanout)?;
773
            crate::durable::sync_dir(&self.dir.join("loose"))?;
774
        }
775
        let mut nonce = [0u8; 8];
776
        rand::rngs::OsRng.fill_bytes(&mut nonce);
777
        let tmp = self
778
            .dir
779
            .join("loose")
780
            .join(format!("tmp-{}", hex::encode(nonce)));
781
        crate::durable::step("write loose object")?;
782
        {
783
            let mut f = fs::File::create(&tmp)?;
784
            f.write_all(&header(ty, body.len()))?;
785
            f.write_all(body)?;
786
            f.sync_all()?;
787
        }
788
        crate::durable::rename(&tmp, &path)?;
789
        Ok(r)
790
    }
791
792
    /// Read and verify an object, loose, packed, or pending in a batch.
793
    pub fn get(&self, r: &ObjectRef) -> Result<(ObjectType, Vec<u8>)> {
794
        match read_loose_at(&self.path_for(r), r) {
795
            Some(found) => found,
796
            None => match self.find_packed(r) {
797
                Some((i, e)) => self.packs.borrow()[i].read(r, e),
798
                None => self.pending_get(r).unwrap_or(Err(StoreError::NotFound(*r))),
799
            },
800
        }
801
    }
802
803
    /// Many objects at once: loose ones are read and verified in parallel,
804
    /// the rest (packed, or pending in a batch) one by one. Results are in
805
    /// the order of `refs`.
806
    pub fn get_many(&self, refs: &[ObjectRef]) -> Vec<Result<Object>> {
807
        use rayon::prelude::*;
808
        let paths: Vec<(ObjectRef, PathBuf)> =
809
            refs.iter().map(|r| (*r, self.path_for(r))).collect();
810
        let mut found: Vec<Option<Result<Object>>> =
811
            paths.par_iter().map(|(r, p)| read_loose_at(p, r)).collect();
812
        // Packed ones: look them up in order, then read them in parallel.
813
        let packs = self.packs.borrow();
814
        let packed: Vec<(usize, usize, PackEntry)> = found
815
            .iter()
816
            .enumerate()
817
            .filter(|(_, f)| f.is_none())
818
            .filter_map(|(i, _)| {
819
                packs
820
                    .iter()
821
                    .enumerate()
822
                    .find_map(|(p, pack)| pack.find(&refs[i]).map(|e| (i, p, e)))
823
            })
824
            .collect();
825
        let indexes: &[PackIndex] = &packs;
826
        let read: Vec<(usize, Result<Object>)> = packed
827
            .par_iter()
828
            .map(|(i, p, e)| (*i, indexes[*p].read(&refs[*i], *e)))
829
            .collect();
830
        drop(packs);
831
        for (i, r) in read {
832
            found[i] = Some(r);
833
        }
834
        found
835
            .into_iter()
836
            .zip(refs)
837
            .map(|(f, r)| match f {
838
                Some(f) => f,
839
                None => self.get(r),
840
            })
841
            .collect()
842
    }
843
844
    pub fn get_typed(&self, r: &ObjectRef, want: ObjectType) -> Result<Vec<u8>> {
845
        let (ty, body) = self.get(r)?;
846
        if ty != want {
847
            return Err(StoreError::WrongType(*r, ty.name(), want.name()));
848
        }
849
        Ok(body)
850
    }
851
852
    pub fn put_change(&self, c: &Change) -> Result<ObjectRef> {
853
        self.put(ObjectType::Change, &c.encode())
854
    }
855
856
    pub fn get_change(&self, r: &ObjectRef) -> Result<Change> {
857
        Ok(Change::decode(&self.get_typed(r, ObjectType::Change)?)?)
858
    }
859
860
    pub fn put_chunk(&self, bytes: &[u8]) -> Result<ObjectRef> {
861
        self.put(ObjectType::Chunk, bytes)
862
    }
863
864
    /// Store a chunk that packs will keep uncompressed (`nocompress`, spec §13.3).
865
    pub fn put_chunk_nocompress(&self, bytes: &[u8]) -> Result<ObjectRef> {
866
        let r = hash_object(ObjectType::Chunk, bytes);
867
        let dir = self.dir.join("nocompress");
868
        fs::create_dir_all(&dir)?;
869
        let _ = fs::File::create(dir.join(r.to_hex()));
870
        self.put(ObjectType::Chunk, bytes)
871
    }
872
873
    fn is_nocompress(&self, r: &ObjectRef) -> bool {
874
        self.dir.join("nocompress").join(r.to_hex()).exists()
875
    }
876
877
    pub fn put_state(&self, st: &StateObject) -> Result<ObjectRef> {
878
        self.put(ObjectType::State, &st.encode())
879
    }
880
881
    pub fn get_state(&self, r: &ObjectRef) -> Result<StateObject> {
882
        Ok(StateObject::decode(&self.get_typed(r, ObjectType::State)?)?)
883
    }
884
885
    pub fn put_attest(&self, a: &Attest) -> Result<ObjectRef> {
886
        self.put(ObjectType::Attest, &a.encode())
887
    }
888
889
    pub fn get_attest(&self, r: &ObjectRef) -> Result<Attest> {
890
        Ok(Attest::decode(&self.get_typed(r, ObjectType::Attest)?)?)
891
    }
892
893
    pub fn put_receipt(&self, r: &Receipt) -> Result<ObjectRef> {
894
        self.put(ObjectType::Receipt, &r.encode())
895
    }
896
897
    pub fn get_receipt(&self, r: &ObjectRef) -> Result<Receipt> {
898
        Ok(Receipt::decode(&self.get_typed(r, ObjectType::Receipt)?)?)
899
    }
900
901
    fn index_path(&self, index: &str, key: &ObjectRef) -> PathBuf {
902
        let h = key.to_hex();
903
        self.dir.join(index).join(&h[..2]).join(&h[2..])
904
    }
905
906
    /// Add `value` to the sorted list kept for `key` in `index`.
907
    fn index_add(&self, index: &str, key: &ObjectRef, value: &ObjectRef) -> Result<()> {
908
        let mut values = self.index_get(index, key)?;
909
        if values.contains(value) {
910
            return Ok(());
911
        }
912
        values.push(*value);
913
        values.sort();
914
        let path = self.index_path(index, key);
915
        fs::create_dir_all(path.parent().unwrap())?;
916
        let text: String = values.iter().map(|s| format!("{}\n", s.to_hex())).collect();
917
        crate::durable::write_atomic(&path, text.as_bytes())?;
918
        Ok(())
919
    }
920
921
    fn index_get(&self, index: &str, key: &ObjectRef) -> Result<Vec<ObjectRef>> {
922
        match fs::read_to_string(self.index_path(index, key)) {
923
            Ok(t) => Ok(t.lines().filter_map(ObjectRef::from_hex).collect()),
924
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(vec![]),
925
            Err(e) => Err(e.into()),
926
        }
927
    }
928
929
    /// Record that seal attestation `attest` covers `change`.
930
    pub fn index_seal(&self, change: &ObjectRef, attest: &ObjectRef) -> Result<()> {
931
        self.index_add("seals", change, attest)
932
    }
933
934
    /// Seal attestations known to cover `change`.
935
    pub fn seals_for(&self, change: &ObjectRef) -> Result<Vec<ObjectRef>> {
936
        self.index_get("seals", change)
937
    }
938
939
    /// Record receipt attestation `attest` for `state` (spec §14.6).
940
    pub fn index_receipt(&self, state: &ObjectRef, attest: &ObjectRef) -> Result<()> {
941
        self.index_add("receipts", state, attest)
942
    }
943
944
    /// Receipt attestations recorded for `state`.
945
    pub fn receipts_for(&self, state: &ObjectRef) -> Result<Vec<ObjectRef>> {
946
        self.index_get("receipts", state)
947
    }
948
949
    /// Every state with a receipt, for garbage collection.
950
    pub fn receipt_states(&self) -> Result<Vec<ObjectRef>> {
951
        let mut out = Vec::new();
952
        let dir = self.dir.join("receipts");
953
        let top = match fs::read_dir(&dir) {
954
            Ok(d) => d,
955
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(out),
956
            Err(e) => return Err(e.into()),
957
        };
958
        for d in top {
959
            let d = d?;
960
            let prefix = d.file_name().to_string_lossy().into_owned();
961
            for f in fs::read_dir(d.path())? {
962
                let name = f?.file_name().to_string_lossy().into_owned();
963
                if let Some(r) = ObjectRef::from_hex(&format!("{prefix}{name}")) {
964
                    out.push(r);
965
                }
966
            }
967
        }
968
        out.sort();
969
        Ok(out)
970
    }
971
972
    pub fn put_identity(&self, id: &Identity) -> Result<ObjectRef> {
973
        self.put(ObjectType::Identity, &id.encode())
974
    }
975
976
    pub fn get_identity(&self, r: &ObjectRef) -> Result<Identity> {
977
        Ok(Identity::decode(&self.get_typed(r, ObjectType::Identity)?)?)
978
    }
979
980
    fn sig_path(&self, r: &ObjectRef) -> PathBuf {
981
        let h = r.to_hex();
982
        self.dir.join("sigs").join(&h[..2]).join(&h[2..])
983
    }
984
985
    /// Signatures stored alongside object `r` (spec §5.4). Empty if none.
986
    pub fn signatures(&self, r: &ObjectRef) -> Result<Vec<Signature>> {
987
        match fs::read(self.sig_path(r)) {
988
            Ok(b) => Ok(Signature::decode_list(&b)?),
989
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(vec![]),
990
            Err(e) => Err(e.into()),
991
        }
992
    }
993
994
    /// Attach a signature to object `r`, replacing any earlier one by the same key.
995
    pub fn put_signature(&self, r: &ObjectRef, sig: &Signature) -> Result<()> {
996
        let mut sigs = self.signatures(r)?;
997
        sigs.retain(|s| s.key != sig.key);
998
        sigs.push(sig.clone());
999
        sigs.sort_by_key(|s| s.key);
1000
        let path = self.sig_path(r);
1001
        fs::create_dir_all(path.parent().unwrap())?;
1002
        let mut nonce = [0u8; 8];
1003
        rand::rngs::OsRng.fill_bytes(&mut nonce);
1004
        let tmp = self.dir.join(format!("sigs-tmp-{}", hex::encode(nonce)));
1005
        {
1006
            let mut f = fs::File::create(&tmp)?;
1007
            f.write_all(&Signature::encode_list(&sigs))?;
1008
            f.sync_all()?;
1009
        }
1010
        crate::durable::rename(&tmp, &path)?;
1011
        Ok(())
1012
    }
1013
1014
    /// Every object reference in the store, loose and packed (slow; for gc and tests).
1015
    pub fn list(&self) -> io::Result<Vec<ObjectRef>> {
1016
        let mut out: Vec<ObjectRef> = self.loose_objects()?.into_iter().map(|(r, _)| r).collect();
1017
        for p in self.packs.borrow().iter() {
1018
            out.extend(p.refs.iter().copied());
1019
        }
1020
        out.sort();
1021
        out.dedup();
1022
        Ok(out)
1023
    }
1024
}
1025
1026
/// A loose object at `path`, verified against `r`; `None` when there is no
1027
/// such file (it may be packed).
1028
fn read_loose_at(path: &Path, r: &ObjectRef) -> Option<Result<Object>> {
1029
    let data = match fs::read(path) {
1030
        Ok(d) => d,
1031
        Err(e) if e.kind() == io::ErrorKind::NotFound => return None,
1032
        Err(e) => return Some(Err(e.into())),
1033
    };
1034
    Some((|| {
1035
        let nul = data
1036
            .iter()
1037
            .position(|&b| b == 0)
1038
            .ok_or_else(|| StoreError::Corrupt(*r, "no header".into()))?;
1039
        let head = std::str::from_utf8(&data[..nul])
1040
            .map_err(|_| StoreError::Corrupt(*r, "header not UTF-8".into()))?;
1041
        let mut parts = head.split(' ');
1042
        let (Some("oxy1"), Some(ty), Some(len), None) =
1043
            (parts.next(), parts.next(), parts.next(), parts.next())
1044
        else {
1045
            return Err(StoreError::Corrupt(*r, format!("bad header {head:?}")));
1046
        };
1047
        let ty = ObjectType::from_name(ty)
1048
            .ok_or_else(|| StoreError::Corrupt(*r, format!("unknown type {ty}")))?;
1049
        let len: usize = len
1050
            .parse()
1051
            .map_err(|_| StoreError::Corrupt(*r, "bad length".into()))?;
1052
        let body = &data[nul + 1..];
1053
        if body.len() != len {
1054
            return Err(StoreError::Corrupt(
1055
                *r,
1056
                format!("length {} != header {len}", body.len()),
1057
            ));
1058
        }
1059
        if hash_object(ty, body) != *r {
1060
            return Err(StoreError::Corrupt(*r, "hash mismatch".into()));
1061
        }
1062
        Ok((ty, body.to_vec()))
1063
    })())
1064
}
1065
1066
#[cfg(test)]
1067
mod tests {
1068
    use super::*;
1069
1070
    #[test]
1071
    fn put_get_verify() {
1072
        let dir = tempfile::tempdir().unwrap();
1073
        let s = Store::open(dir.path().join("objects")).unwrap();
1074
        let r = s.put(ObjectType::Chunk, b"hello").unwrap();
1075
        assert!(s.has(&r));
1076
        assert_eq!(s.get(&r).unwrap(), (ObjectType::Chunk, b"hello".to_vec()));
1077
        assert_eq!(s.put(ObjectType::Chunk, b"hello").unwrap(), r);
1078
        assert_eq!(s.list().unwrap(), vec![r]);
1079
        assert!(matches!(
1080
            s.get_typed(&r, ObjectType::Blob),
1081
            Err(StoreError::WrongType(..))
1082
        ));
1083
        assert!(matches!(
1084
            s.get(&ObjectRef([9; 32])),
1085
            Err(StoreError::NotFound(_))
1086
        ));
1087
1088
        // Tamper: flip a byte, read must fail.
1089
        let path = s.path_for(&r);
1090
        let mut data = fs::read(&path).unwrap();
1091
        let last = data.len() - 1;
1092
        data[last] ^= 1;
1093
        fs::write(&path, data).unwrap();
1094
        assert!(matches!(s.get(&r), Err(StoreError::Corrupt(..))));
1095
    }
1096
1097
    #[test]
1098
    fn packing_keeps_every_object_readable() {
1099
        let dir = tempfile::tempdir().unwrap();
1100
        let s = Store::open(dir.path()).unwrap();
1101
        let mut refs = Vec::new();
1102
        for i in 0..300u32 {
1103
            let body = format!("object {i} ").repeat(1 + (i % 7) as usize);
1104
            refs.push((s.put(ObjectType::Chunk, body.as_bytes()).unwrap(), body));
1105
        }
1106
        let c = crate::objects::tests::sample_change();
1107
        let cr = s.put_change(&c).unwrap();
1108
        assert_eq!(s.loose_count().unwrap(), 301);
1109
        let report = s.pack_loose().unwrap();
1110
        assert_eq!(report.objects, 301);
1111
        assert!(report.packed_bytes < report.raw_bytes, "{report:?}");
1112
        assert_eq!(s.loose_count().unwrap(), 0);
1113
        assert_eq!(s.pack_count(), 1);
1114
        for (r, body) in &refs {
1115
            assert!(s.has(r));
1116
            assert_eq!(
1117
                s.get(r).unwrap(),
1118
                (ObjectType::Chunk, body.as_bytes().to_vec())
1119
            );
1120
        }
1121
        assert_eq!(s.get_change(&cr).unwrap(), c);
1122
        assert_eq!(s.list().unwrap().len(), 301);
1123
        // Re-putting a packed object is a no-op; new objects go loose and a
1124
        // second pack coexists with the first.
1125
        assert_eq!(
1126
            s.put(ObjectType::Chunk, refs[0].1.as_bytes()).unwrap(),
1127
            refs[0].0
1128
        );
1129
        assert_eq!(s.loose_count().unwrap(), 0);
1130
        let extra = s.put(ObjectType::Chunk, b"after packing").unwrap();
1131
        s.pack_loose().unwrap();
1132
        assert_eq!(s.pack_count(), 2);
1133
        assert_eq!(s.get(&extra).unwrap().1, b"after packing");
1134
        // A reopened store sees the packs.
1135
        let s2 = Store::open(dir.path()).unwrap();
1136
        assert_eq!(s2.pack_count(), 2);
1137
        assert_eq!(s2.get(&refs[5].0).unwrap().1, refs[5].1.as_bytes());
1138
        assert!(matches!(
1139
            s2.get(&ObjectRef([9; 32])),
1140
            Err(StoreError::NotFound(_))
1141
        ));
1142
        // Corrupting an object's body inside the pack is detected on read.
1143
        let target = refs[5].0;
1144
        let (pack_path, offset) = {
1145
            let packs = s2.packs.borrow();
1146
            let (i, e) = s2.find_packed(&target).unwrap();
1147
            (packs[i].path.clone(), e.offset as usize)
1148
        };
1149
        let mut data = fs::read(&pack_path).unwrap();
1150
        data[offset] ^= 0xff;
1151
        fs::write(&pack_path, data).unwrap();
1152
        let s3 = Store::open(dir.path()).unwrap();
1153
        assert!(matches!(s3.get(&target), Err(StoreError::Corrupt(..))));
1154
        assert_eq!(
1155
            s3.get(&refs[6].0).unwrap().1,
1156
            refs[6].1.as_bytes(),
1157
            "other objects are unaffected"
1158
        );
1159
    }
1160
1161
    #[test]
1162
    fn nocompress_chunks_pack_raw() {
1163
        let dir = tempfile::tempdir().unwrap();
1164
        let s = Store::open(dir.path()).unwrap();
1165
        let body = vec![b'a'; 10_000];
1166
        let raw = s.put_chunk_nocompress(&body).unwrap();
1167
        let compressible = s.put(ObjectType::Chunk, &vec![b'b'; 10_000]).unwrap();
1168
        s.pack_loose().unwrap();
1169
        let packs = s.packs.borrow();
1170
        let (i, e) = s.find_packed(&raw).unwrap();
1171
        assert_eq!((e.flags, e.stored_len as usize), (0, 10_000));
1172
        let (_, c) = s.find_packed(&compressible).unwrap();
1173
        assert_eq!(c.flags, FLAG_ZSTD);
1174
        assert!(c.stored_len < 10_000);
1175
        assert_eq!(packs[i].read(&raw, e).unwrap().1, body);
1176
    }
1177
1178
    #[test]
1179
    fn many_packs_merge_into_one() {
1180
        let dir = tempfile::tempdir().unwrap();
1181
        let s = Store::open(dir.path()).unwrap();
1182
        let mut refs = Vec::new();
1183
        for i in 0..(MAX_PACKS + 2) {
1184
            refs.push(
1185
                s.put(ObjectType::Chunk, format!("pack {i}").as_bytes())
1186
                    .unwrap(),
1187
            );
1188
            s.pack_loose().unwrap();
1189
        }
1190
        assert_eq!(s.pack_count(), MAX_PACKS + 2);
1191
        assert_eq!(s.merge_packs().unwrap(), MAX_PACKS + 2);
1192
        assert_eq!(s.pack_count(), 1);
1193
        assert_eq!(s.merge_packs().unwrap(), 0);
1194
        for (i, r) in refs.iter().enumerate() {
1195
            assert_eq!(s.get(r).unwrap().1, format!("pack {i}").as_bytes());
1196
        }
1197
        assert_eq!(Store::open(dir.path()).unwrap().pack_count(), 1);
1198
    }
1199
1200
    #[test]
1201
    fn prune_removes_unreachable_loose_and_packed_objects() {
1202
        use std::collections::HashSet;
1203
        let dir = tempfile::tempdir().unwrap();
1204
        let s = Store::open(dir.path()).unwrap();
1205
        let keep_a = s.put(ObjectType::Chunk, b"keep a").unwrap();
1206
        let drop_a = s.put(ObjectType::Chunk, b"drop a").unwrap();
1207
        s.pack_loose().unwrap();
1208
        let keep_b = s.put(ObjectType::Chunk, b"keep b").unwrap();
1209
        let drop_b = s.put(ObjectType::Chunk, b"drop b").unwrap();
1210
        let sk = crate::identity::SecretKey {
1211
            ed_seed: [1; 32],
1212
            ml_seed: [2; 32],
1213
        };
1214
        let id = sk.identity("a", "b", 0);
1215
        let key = s.put_identity(&id).unwrap();
1216
        s.put_signature(
1217
            &drop_b,
1218
            &sk.sign(crate::identity::Domain::Change, &key, &drop_b),
1219
        )
1220
        .unwrap();
1221
        s.index_seal(&drop_a, &ObjectRef([3; 32])).unwrap();
1222
        let keep: HashSet<ObjectRef> = [keep_a, keep_b, key].into_iter().collect();
1223
        // Everything is too young under a grace period.
1224
        let r = s
1225
            .prune(&keep, std::time::Duration::from_secs(3600))
1226
            .unwrap();
1227
        assert_eq!((r.objects, r.deferred), (0, 2));
1228
        let r = s.prune(&keep, std::time::Duration::ZERO).unwrap();
1229
        assert_eq!(r.objects, 2, "{r:?}");
1230
        assert!(s.has(&keep_a) && s.has(&keep_b) && s.has(&key));
1231
        assert!(!s.has(&drop_a) && !s.has(&drop_b));
1232
        assert!(s.signatures(&drop_b).unwrap().is_empty());
1233
        assert!(s.seals_for(&drop_a).unwrap().is_empty());
1234
        assert_eq!(s.pack_count(), 1);
1235
        assert_eq!(s.get(&keep_a).unwrap().1, b"keep a");
1236
        assert_eq!(s.list().unwrap().len(), 3);
1237
    }
1238
1239
    #[test]
1240
    fn signatures_are_stored_beside_objects() {
1241
        use crate::identity::{Domain, SecretKey};
1242
        let dir = tempfile::tempdir().unwrap();
1243
        let s = Store::open(dir.path()).unwrap();
1244
        let r = s.put(ObjectType::Chunk, b"x").unwrap();
1245
        assert!(s.signatures(&r).unwrap().is_empty());
1246
        let sk = SecretKey {
1247
            ed_seed: [1; 32],
1248
            ml_seed: [2; 32],
1249
        };
1250
        let id = sk.identity("a", "b", 0);
1251
        let key = s.put_identity(&id).unwrap();
1252
        assert_eq!(s.get_identity(&key).unwrap(), id);
1253
        let sig = sk.sign(Domain::Change, &key, &r);
1254
        s.put_signature(&r, &sig).unwrap();
1255
        s.put_signature(&r, &sig).unwrap();
1256
        assert_eq!(s.signatures(&r).unwrap(), vec![sig]);
1257
        // The object itself is untouched by its signatures.
1258
        assert_eq!(s.get(&r).unwrap().1, b"x");
1259
    }
1260
1261
    #[test]
1262
    fn change_round_trip_through_store() {
1263
        let dir = tempfile::tempdir().unwrap();
1264
        let s = Store::open(dir.path()).unwrap();
1265
        let c = crate::objects::tests::sample_change();
1266
        let r = s.put_change(&c).unwrap();
1267
        assert_eq!(r, c.hash());
1268
        assert_eq!(s.get_change(&r).unwrap(), c);
1269
    }
1270
1271
    fn xorshift(state: &mut u64) -> u64 {
1272
        *state ^= *state << 13;
1273
        *state ^= *state >> 7;
1274
        *state ^= *state << 17;
1275
        *state
1276
    }
1277
1278
    /// Random corruption of pack and index files never panics: reads fail
1279
    /// with an error or the store refuses to open, and untouched objects
1280
    /// stay readable when the index survives.
1281
    #[test]
1282
    fn corrupted_packs_and_indexes_fail_cleanly() {
1283
        let dir = tempfile::tempdir().unwrap();
1284
        let objects = dir.path().join("objects");
1285
        let s = Store::open(&objects).unwrap();
1286
        let mut refs = Vec::new();
1287
        let mut seed = 0x9e3779b97f4a7c15u64;
1288
        for i in 0..40u32 {
1289
            let len = (xorshift(&mut seed) % 5000) as usize;
1290
            let mut body = vec![0u8; len];
1291
            for b in body.iter_mut() {
1292
                *b = if i % 2 == 0 {
1293
                    (xorshift(&mut seed) & 0xff) as u8
1294
                } else {
1295
                    b'a' + (i % 26) as u8
1296
                };
1297
            }
1298
            refs.push(s.put(ObjectType::Chunk, &body).unwrap());
1299
        }
1300
        s.pack_loose().unwrap();
1301
        let pack = fs::read_dir(&objects)
1302
            .unwrap()
1303
            .filter_map(|e| e.ok().map(|e| e.path()))
1304
            .find(|p| p.extension().is_some_and(|x| x == "pack"))
1305
            .unwrap();
1306
        let idx = pack.with_extension("idx");
1307
        let pristine_pack = fs::read(&pack).unwrap();
1308
        let pristine_idx = fs::read(&idx).unwrap();
1309
1310
        for round in 0..300u32 {
1311
            let (path, pristine) = if round % 2 == 0 {
1312
                (&pack, &pristine_pack)
1313
            } else {
1314
                (&idx, &pristine_idx)
1315
            };
1316
            let mut data = pristine.clone();
1317
            match xorshift(&mut seed) % 4 {
1318
                0 => {
1319
                    let at = (xorshift(&mut seed) as usize) % data.len();
1320
                    data[at] ^= 1 << (xorshift(&mut seed) % 8);
1321
                }
1322
                1 => {
1323
                    let at = (xorshift(&mut seed) as usize) % data.len();
1324
                    data[at] = (xorshift(&mut seed) & 0xff) as u8;
1325
                }
1326
                2 => {
1327
                    let keep = (xorshift(&mut seed) as usize) % data.len();
1328
                    data.truncate(keep);
1329
                }
1330
                _ => {
1331
                    let at = (xorshift(&mut seed) as usize) % data.len();
1332
                    let n = ((xorshift(&mut seed) % 8) + 1) as usize;
1333
                    for k in 0..n.min(data.len() - at) {
1334
                        data[at + k] = 0xff;
1335
                    }
1336
                }
1337
            }
1338
            fs::write(path, &data).unwrap();
1339
            // Opening may refuse a broken index; a store that opens must
1340
            // either return each object intact or report it corrupt/missing.
1341
            if let Ok(s2) = Store::open(&objects) {
1342
                for r in &refs {
1343
                    match s2.get(r) {
1344
                        Ok((ty, body)) => assert_eq!(
1345
                            hash_object(ty, &body),
1346
                            *r,
1347
                            "a returned object always matches its reference"
1348
                        ),
1349
                        Err(StoreError::Corrupt(..))
1350
                        | Err(StoreError::NotFound(_))
1351
                        | Err(StoreError::Io(_)) => {}
1352
                        Err(e) => panic!("unexpected error {e}"),
1353
                    }
1354
                }
1355
            }
1356
            fs::write(path, pristine).unwrap();
1357
        }
1358
        // Restored files read back in full.
1359
        let s3 = Store::open(&objects).unwrap();
1360
        for r in &refs {
1361
            let (ty, body) = s3.get(r).unwrap();
1362
            assert_eq!(hash_object(ty, &body), *r);
1363
        }
1364
    }
1365
1366
    /// A small batch becomes loose objects; a large one becomes a single
1367
    /// pack. Objects are readable while pending, and batches nest.
1368
    #[test]
1369
    fn batches_write_loose_or_one_pack() {
1370
        let dir = tempfile::tempdir().unwrap();
1371
        let s = Store::open(dir.path().join("objects")).unwrap();
1372
        let outer = s.begin_batch().unwrap();
1373
        let a = s.put(ObjectType::Chunk, b"small a").unwrap();
1374
        {
1375
            let inner = s.begin_batch().unwrap();
1376
            let b = s.put(ObjectType::Chunk, b"small b").unwrap();
1377
            assert!(s.has(&b));
1378
            assert_eq!(s.get(&b).unwrap().1, b"small b");
1379
            inner.finish().unwrap();
1380
            assert_eq!(
1381
                s.loose_count().unwrap(),
1382
                0,
1383
                "the inner batch defers to the outer one"
1384
            );
1385
        }
1386
        assert_eq!(s.get(&a).unwrap().1, b"small a");
1387
        assert_eq!(
1388
            s.put(ObjectType::Chunk, b"small a").unwrap(),
1389
            a,
1390
            "idempotent while pending"
1391
        );
1392
        outer.finish().unwrap();
1393
        assert_eq!(s.loose_count().unwrap(), 2);
1394
        assert_eq!(s.pack_count(), 0);
1395
        assert_eq!(s.get(&a).unwrap().1, b"small a");
1396
1397
        // Large: spills into one pack, readable during and after.
1398
        let big = s.begin_batch().unwrap();
1399
        let mut refs = Vec::new();
1400
        let mut seed = 7u64;
1401
        for i in 0..300u32 {
1402
            let mut body = vec![b'z'; 40_000];
1403
            if i % 3 == 0 {
1404
                for b in body.iter_mut() {
1405
                    *b = (xorshift(&mut seed) & 0xff) as u8;
1406
                }
1407
            }
1408
            body[..4].copy_from_slice(&i.to_le_bytes());
1409
            let r = if i % 5 == 0 {
1410
                s.put_chunk_nocompress(&body).unwrap()
1411
            } else {
1412
                s.put(ObjectType::Chunk, &body).unwrap()
1413
            };
1414
            assert_eq!(
1415
                s.get(&r).unwrap().1,
1416
                body,
1417
                "readable while pending (object {i})"
1418
            );
1419
            refs.push((r, body));
1420
        }
1421
        assert!(s.has(&refs[0].0));
1422
        big.finish().unwrap();
1423
        assert_eq!(s.pack_count(), 1);
1424
        assert_eq!(
1425
            s.loose_count().unwrap(),
1426
            2,
1427
            "nothing from the big batch is loose"
1428
        );
1429
        assert!(fs::read_dir(dir.path().join("objects"))
1430
            .unwrap()
1431
            .flatten()
1432
            .all(|e| !e.file_name().to_string_lossy().starts_with("tmp-")));
1433
        let s2 = Store::open(dir.path().join("objects")).unwrap();
1434
        for (r, body) in &refs {
1435
            assert_eq!(s2.get(r).unwrap().1, *body);
1436
        }
1437
        // A dropped batch still writes its objects.
1438
        {
1439
            let _b = s2.begin_batch().unwrap();
1440
            s2.put(ObjectType::Chunk, b"dropped").unwrap();
1441
        }
1442
        assert_eq!(
1443
            s2.get(&hash_object(ObjectType::Chunk, b"dropped"))
1444
                .unwrap()
1445
                .1,
1446
            b"dropped"
1447
        );
1448
    }
1449
1450
    #[test]
1451
    fn get_many_reads_loose_and_packed_objects_in_order() {
1452
        let dir = tempfile::tempdir().unwrap();
1453
        let s = Store::open(dir.path().join("objects")).unwrap();
1454
        let packed: Vec<ObjectRef> = (0..20)
1455
            .map(|i| {
1456
                s.put(ObjectType::Chunk, format!("packed {i}").as_bytes())
1457
                    .unwrap()
1458
            })
1459
            .collect();
1460
        s.pack_loose().unwrap();
1461
        let loose: Vec<ObjectRef> = (0..20)
1462
            .map(|i| {
1463
                s.put(ObjectType::Chunk, format!("loose {i}").as_bytes())
1464
                    .unwrap()
1465
            })
1466
            .collect();
1467
        let missing = hash_object(ObjectType::Chunk, b"never stored");
1468
        let mut refs: Vec<ObjectRef> = Vec::new();
1469
        for i in 0..20 {
1470
            refs.push(loose[i]);
1471
            refs.push(packed[i]);
1472
        }
1473
        refs.push(missing);
1474
        let got = s.get_many(&refs);
1475
        assert_eq!(got.len(), refs.len());
1476
        for (i, r) in refs.iter().enumerate().take(40) {
1477
            let want = if i % 2 == 0 {
1478
                format!("loose {}", i / 2)
1479
            } else {
1480
                format!("packed {}", i / 2)
1481
            };
1482
            let (ty, body) = got[i].as_ref().unwrap();
1483
            assert_eq!(
1484
                (*ty, body.as_slice()),
1485
                (ObjectType::Chunk, want.as_bytes()),
1486
                "{r:?}"
1487
            );
1488
        }
1489
        assert!(matches!(got[40], Err(StoreError::NotFound(_))));
1490
    }
1491
}