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