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