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