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