OXYGENHUB
◆ agents-in-readme oxygen / crates / oxy-core / src / strata.rs
32.3 KiB · Rust · last changed The object store runs on any Store: a directory today, a bucket next 1 month ago source blame history raw
a5f76c79 Andy McCall ↶ 1
//! Oxygen Strata (docs/stores.md): the storage a repository can be kept
2
//! on, as a trait, so that a bucket, a directory or a hub's disk all look
3
//! the same to the code that pushes to and fetches from them.
4
//!
5
//! A store has two halves. **Objects** are written once and never
6
//! change: packs and their indexes, named by content, which any store
7
//! can hold safely. **References** are the one mutable record (bookmarks,
8
//! tags, the policy), updated only by compare-and-swap on a version the
9
//! store hands out: an ETag on S3, a generation on GCS, a content hash on
10
//! a directory. Two writers can therefore share a store with no lock
11
//! server: the loser of a race reads again, merges, and tries again.
12
//!
64818938 Andy McCall ↶ 13
//! A repository's own objects (`.oxy/objects`, see [`crate::store`]) are
14
//! kept on a store too, a directory one, with the same keys a bucket
15
//! would hold; a remote is a store as well. This module holds the trait,
16
//! a directory store, an in-memory store for tests, and the conformance
17
//! check every store must pass. Cloud stores live in a crate of their
18
//! own, because of what they pull in.
a5f76c79 Andy McCall ↶ 19
20
use std::collections::BTreeMap;
21
use std::fs;
22
use std::io::{self, Read, Seek, SeekFrom, Write};
23
use std::ops::Range;
24
use std::path::{Path, PathBuf};
25
use std::sync::Mutex;
26
use std::time::{Duration, Instant};
27
28
/// The version of a reference record as the store reports it: opaque,
29
/// compared only for equality, and only ever valid for the store that
30
/// issued it. Writing the same bytes again may keep the version (S3's
31
/// ETag and a directory's content hash are both of the content): a
32
/// compare-and-swap guards against losing a *different* record, which is
33
/// all it needs to.
34
#[derive(Clone, Debug, PartialEq, Eq)]
35
pub struct Version(pub String);
36
37
/// One key in a listing.
38
#[derive(Clone, Debug, PartialEq, Eq)]
39
pub struct Entry {
40
    pub key: String,
41
    pub size: u64,
64818938 Andy McCall ↶ 42
    /// When the object was written, in wall milliseconds, where the store
43
    /// knows (garbage collection's grace period reads it).
44
    pub modified_ms: Option<u64>,
a5f76c79 Andy McCall ↶ 45
}
46
47
#[derive(Debug, thiserror::Error)]
48
pub enum Error {
49
    #[error("{0}: not in the store")]
50
    NotFound(String),
51
    #[error("{0}: already in the store")]
52
    Exists(String),
53
    #[error("{0}: changed since it was read")]
54
    Conflict(String),
55
    #[error("bad store key {0:?}: {1}")]
56
    Key(String, &'static str),
57
    #[error("{0}: busy (another writer holds it)")]
58
    Busy(String),
59
    #[error(transparent)]
60
    Io(#[from] io::Error),
61
    #[error("{0}")]
62
    Backend(String),
63
}
64
65
pub type Result<T> = std::result::Result<T, Error>;
66
67
/// Longest key a store accepts; S3 allows 1024 bytes.
68
pub const MAX_KEY_LEN: usize = 1024;
69
70
/// Refuse a key that is not a plain relative path: `/`-separated
71
/// segments of `[A-Za-z0-9._-]`, none empty, none `.` or `..`. The same
72
/// rule on every store, so a key written to one can be read from another,
73
/// and so a key can never escape a directory store's root.
74
pub fn check_key(key: &str) -> Result<()> {
75
    let bad = |why| Error::Key(key.to_string(), why);
76
    if key.is_empty() {
77
        return Err(bad("empty"));
78
    }
79
    if key.len() > MAX_KEY_LEN {
80
        return Err(bad("longer than 1024 bytes"));
81
    }
82
    for seg in key.split('/') {
83
        if seg.is_empty() {
84
            return Err(bad(
85
                "an empty segment (a leading, trailing or doubled slash)",
86
            ));
87
        }
88
        if seg == "." || seg == ".." {
89
            return Err(bad("a . or .. segment"));
90
        }
91
        if !seg
92
            .bytes()
93
            .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
94
        {
95
            return Err(bad("a character outside A-Z a-z 0-9 . _ -"));
96
        }
97
    }
98
    Ok(())
99
}
100
101
/// Where a repository's packs and references are kept. Every method is
102
/// synchronous and may be called from several threads at once.
103
pub trait Store: Send + Sync {
104
    /// A name for messages: the URL, or the directory.
105
    fn describe(&self) -> String;
106
107
    // ---- objects: written once, never changed
108
109
    fn has(&self, key: &str) -> Result<bool>;
110
    /// The whole object; `NotFound` when there is none.
111
    fn get(&self, key: &str) -> Result<Vec<u8>>;
112
    /// Bytes `range` of an object; an error when the range runs past its end.
113
    fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>>;
114
    /// Write an object that does not exist yet; `Exists` when it does, and
115
    /// then nothing is changed. Readers never see a partial object.
116
    fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()>;
117
    /// Every key under `prefix` (a key prefix, not necessarily a whole
118
    /// segment), sorted, with sizes.
119
    fn list(&self, prefix: &str) -> Result<Vec<Entry>>;
120
    /// Remove an object; fine when it is already gone.
121
    fn delete(&self, key: &str) -> Result<()>;
64818938 Andy McCall ↶ 122
    /// One object's listing entry without listing its neighbours; `None`
123
    /// when there is none.
124
    fn stat(&self, key: &str) -> Result<Option<Entry>> {
125
        Ok(self.list(key)?.into_iter().find(|e| e.key == key))
126
    }
127
    /// Write several objects that do not exist yet; one that does is left
128
    /// as it is, which is not an error (objects are named by content). A
129
    /// store that can batch its syncs does; this does them one at a time.
130
    fn put_many_new(&self, objects: &[(String, Vec<u8>)]) -> Result<()> {
131
        for (key, bytes) in objects {
132
            match self.put_new(key, bytes) {
133
                Ok(()) | Err(Error::Exists(_)) => {}
134
                Err(e) => return Err(e),
135
            }
136
        }
137
        Ok(())
138
    }
139
    /// `put_new` of a file's contents: a store on the same disk can move
140
    /// the file into place instead of copying it.
141
    fn put_new_file(&self, key: &str, path: &Path) -> Result<()> {
142
        self.put_new(key, &fs::read(path)?)
143
    }
a5f76c79 Andy McCall ↶ 144
145
    // ---- references: one small record, updated by compare-and-swap
146
147
    /// The record and its current version, or `None` when it was never
148
    /// written.
149
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>>;
150
    /// Replace the record only if its version is still `expected`
151
    /// (`None`: only if it does not exist yet). `Conflict` otherwise, and
152
    /// then nothing is changed. Returns the new version.
153
    fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version>;
154
}
155
156
/// A reference record's version on stores that have no native one: the
157
/// hash of its bytes, which two different records never share.
158
fn content_version(bytes: &[u8]) -> Version {
159
    Version(blake3::hash(bytes).to_hex().to_string())
160
}
161
162
fn check_expected(key: &str, current: Option<&Version>, expected: Option<&Version>) -> Result<()> {
163
    if current == expected {
164
        Ok(())
165
    } else {
166
        Err(Error::Conflict(key.to_string()))
167
    }
168
}
169
170
// ---------------------------------------------------------------- directory
171
172
/// A store in a directory: each key a file under the root. Create-only
173
/// writes go through a hard link, which the file system refuses when the
174
/// name is taken, and a reference is updated under a lock file beside it.
175
/// Several processes on one machine can share it; it is also what the
176
/// tests and `oxy remote check` use.
177
pub struct LocalStore {
178
    root: PathBuf,
179
}
180
181
/// How long a reference's lock may be held before it counts as abandoned
182
/// by a process that died, and how long a writer waits for it.
183
const LOCK_STALE: Duration = Duration::from_secs(60);
184
const LOCK_WAIT: Duration = Duration::from_secs(10);
185
186
impl LocalStore {
187
    /// Open the store at `root`, creating the directory if needed.
188
    pub fn open(root: impl Into<PathBuf>) -> Result<LocalStore> {
189
        let root = root.into();
190
        fs::create_dir_all(&root)?;
191
        Ok(LocalStore { root })
192
    }
193
194
    pub fn root(&self) -> &Path {
195
        &self.root
196
    }
197
198
    fn path(&self, key: &str) -> Result<PathBuf> {
199
        check_key(key)?;
200
        Ok(self.root.join(key))
201
    }
202
203
    fn temp_beside(&self, path: &Path) -> PathBuf {
204
        let mut nonce = [0u8; 6];
205
        rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut nonce);
206
        let name = path
207
            .file_name()
208
            .map(|n| n.to_string_lossy().into_owned())
209
            .unwrap_or_default();
210
        path.with_file_name(format!(".{name}.tmp-{}", hex::encode(nonce)))
211
    }
212
213
    fn list_dir(&self, dir: &Path, rel: &str, prefix: &str, out: &mut Vec<Entry>) -> Result<()> {
214
        let entries = match fs::read_dir(dir) {
215
            Ok(e) => e,
216
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
217
            Err(e) => return Err(e.into()),
218
        };
219
        for entry in entries {
220
            let entry = entry?;
221
            let name = entry.file_name().to_string_lossy().into_owned();
222
            // Temporary and lock files are not objects.
223
            if name.starts_with('.') {
224
                continue;
225
            }
226
            let key = if rel.is_empty() {
227
                name.clone()
228
            } else {
229
                format!("{rel}/{name}")
230
            };
231
            let ty = entry.file_type()?;
232
            if ty.is_dir() {
233
                // Descend only where the prefix could still match.
234
                let dir_prefix = format!("{key}/");
235
                if prefix.starts_with(&dir_prefix) || dir_prefix.starts_with(prefix) {
236
                    self.list_dir(&entry.path(), &key, prefix, out)?;
237
                }
238
            } else if ty.is_file() && key.starts_with(prefix) {
64818938 Andy McCall ↶ 239
                let meta = entry.metadata()?;
a5f76c79 Andy McCall ↶ 240
                out.push(Entry {
241
                    key,
64818938 Andy McCall ↶ 242
                    size: meta.len(),
243
                    modified_ms: modified_ms(&meta),
a5f76c79 Andy McCall ↶ 244
                });
245
            }
246
        }
247
        Ok(())
248
    }
249
250
    /// Take `path.lock`, waiting for another writer, and removing a lock
251
    /// a dead process left behind.
252
    fn lock(&self, key: &str, path: &Path) -> Result<PathBuf> {
253
        let lock = path.with_file_name(format!(
254
            ".{}.lock",
255
            path.file_name()
256
                .map(|n| n.to_string_lossy().into_owned())
257
                .unwrap_or_default()
258
        ));
259
        let started = Instant::now();
260
        loop {
261
            match fs::OpenOptions::new()
262
                .write(true)
263
                .create_new(true)
264
                .open(&lock)
265
            {
266
                Ok(mut f) => {
267
                    let _ = writeln!(f, "{}", std::process::id());
268
                    return Ok(lock);
269
                }
270
                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
271
                    if let Ok(m) = fs::metadata(&lock) {
272
                        let age = m.modified().ok().and_then(|t| t.elapsed().ok());
273
                        if age.is_some_and(|a| a > LOCK_STALE) {
274
                            let _ = fs::remove_file(&lock);
275
                            continue;
276
                        }
277
                    }
278
                    if started.elapsed() > LOCK_WAIT {
279
                        return Err(Error::Busy(key.to_string()));
280
                    }
281
                    std::thread::sleep(Duration::from_millis(10));
282
                }
283
                Err(e) => return Err(e.into()),
64818938 Andy McCall ↶ 284
            }
285
        }
286
    }
287
}
288
289
/// A file's modification time as wall milliseconds.
290
fn modified_ms(meta: &fs::Metadata) -> Option<u64> {
291
    meta.modified()
292
        .ok()
293
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
294
        .map(|d| d.as_millis() as u64)
295
}
296
297
impl LocalStore {
298
    /// Link `tmp` into `path`, create-only; the directory is synced and
299
    /// `tmp` removed either way. `Exists` when the name is taken.
300
    fn link_into_place(&self, key: &str, tmp: &Path, path: &Path) -> Result<()> {
301
        let linked = fs::hard_link(tmp, path);
302
        let _ = fs::remove_file(tmp);
303
        match linked {
304
            Ok(()) => {
305
                if let Some(dir) = path.parent() {
306
                    crate::durable::sync_dir(dir)?;
307
                }
308
                Ok(())
309
            }
310
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
311
                Err(Error::Exists(key.to_string()))
a5f76c79 Andy McCall ↶ 312
            }
64818938 Andy McCall ↶ 313
            Err(e) => Err(e.into()),
314
        }
315
    }
316
317
    /// Create `path`'s directory, syncing the first directory that was
318
    /// made so its entry is durable too.
319
    fn ensure_dir(&self, path: &Path) -> Result<()> {
320
        let Some(dir) = path.parent() else {
321
            return Ok(());
322
        };
323
        if dir.is_dir() {
324
            return Ok(());
325
        }
326
        let mut made = dir.to_path_buf();
327
        while let Some(parent) = made.parent() {
328
            if parent.is_dir() || parent == self.root {
329
                break;
330
            }
331
            made = parent.to_path_buf();
332
        }
333
        fs::create_dir_all(dir)?;
334
        if let Some(parent) = made.parent() {
335
            crate::durable::sync_dir(parent)?;
a5f76c79 Andy McCall ↶ 336
        }
64818938 Andy McCall ↶ 337
        Ok(())
a5f76c79 Andy McCall ↶ 338
    }
339
}
340
341
impl Store for LocalStore {
342
    fn describe(&self) -> String {
343
        self.root.display().to_string()
64818938 Andy McCall ↶ 344
    }
345
346
    fn stat(&self, key: &str) -> Result<Option<Entry>> {
347
        match fs::metadata(self.path(key)?) {
348
            Ok(m) if m.is_file() => Ok(Some(Entry {
349
                key: key.to_string(),
350
                size: m.len(),
351
                modified_ms: modified_ms(&m),
352
            })),
353
            Ok(_) => Ok(None),
354
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
355
            Err(e) => Err(e.into()),
356
        }
357
    }
358
359
    /// Every file written and synced (on every core), then each linked
360
    /// into place and each directory synced once: durable when this
361
    /// returns, as one at a time would be, with far fewer waits.
362
    fn put_many_new(&self, objects: &[(String, Vec<u8>)]) -> Result<()> {
363
        use rayon::prelude::*;
364
        let mut todo: Vec<(PathBuf, PathBuf, &[u8])> = Vec::new();
365
        for (key, bytes) in objects {
366
            let path = self.path(key)?;
367
            if path.exists() || todo.iter().any(|(p, ..)| *p == path) {
368
                continue;
369
            }
370
            self.ensure_dir(&path)?;
371
            todo.push((path.clone(), self.temp_beside(&path), bytes));
372
        }
373
        if todo.is_empty() {
374
            return Ok(());
375
        }
376
        crate::durable::step("write loose objects")?;
377
        todo.par_iter()
378
            .try_for_each(|(_, tmp, bytes)| -> io::Result<()> {
379
                let mut f = fs::File::create(tmp)?;
380
                f.write_all(bytes)?;
381
                f.sync_all()
382
            })?;
383
        crate::durable::step("rename loose objects")?;
384
        let mut dirs: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
385
        for (path, tmp, _) in &todo {
386
            let linked = fs::hard_link(tmp, path);
387
            let _ = fs::remove_file(tmp);
388
            match linked {
389
                Ok(()) => {
390
                    if let Some(d) = path.parent() {
391
                        dirs.insert(d.to_path_buf());
392
                    }
393
                }
394
                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
395
                Err(e) => return Err(e.into()),
396
            }
397
        }
398
        dirs.par_iter()
399
            .try_for_each(|d| crate::durable::sync_dir(d))?;
400
        Ok(())
a5f76c79 Andy McCall ↶ 401
    }
402
64818938 Andy McCall ↶ 403
    /// The file is linked into place when it is on this file system,
404
    /// else copied; it is removed either way.
405
    fn put_new_file(&self, key: &str, file: &Path) -> Result<()> {
406
        let path = self.path(key)?;
407
        if path.exists() {
408
            let _ = fs::remove_file(file);
409
            return Err(Error::Exists(key.to_string()));
410
        }
411
        self.ensure_dir(&path)?;
412
        match fs::hard_link(file, &path) {
413
            Ok(()) => {
414
                let _ = fs::remove_file(file);
415
                if let Some(dir) = path.parent() {
416
                    crate::durable::sync_dir(dir)?;
417
                }
418
                Ok(())
419
            }
420
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
421
                let _ = fs::remove_file(file);
422
                Err(Error::Exists(key.to_string()))
423
            }
424
            Err(_) => {
425
                // Another file system: copy beside the name, then link.
426
                let tmp = self.temp_beside(&path);
427
                let copied = (|| {
428
                    let mut src = fs::File::open(file)?;
429
                    let mut dst = fs::File::create(&tmp)?;
430
                    io::copy(&mut src, &mut dst)?;
431
                    dst.sync_all()
432
                })();
433
                let _ = fs::remove_file(file);
434
                copied?;
435
                self.link_into_place(key, &tmp, &path)
436
            }
437
        }
438
    }
439
a5f76c79 Andy McCall ↶ 440
    fn has(&self, key: &str) -> Result<bool> {
441
        Ok(self.path(key)?.is_file())
442
    }
443
444
    fn get(&self, key: &str) -> Result<Vec<u8>> {
445
        let path = self.path(key)?;
446
        match fs::read(&path) {
447
            Ok(b) => Ok(b),
448
            Err(e) if e.kind() == io::ErrorKind::NotFound => Err(Error::NotFound(key.to_string())),
449
            Err(e) => Err(e.into()),
450
        }
451
    }
452
453
    fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> {
454
        let path = self.path(key)?;
455
        let mut f = match fs::File::open(&path) {
456
            Ok(f) => f,
457
            Err(e) if e.kind() == io::ErrorKind::NotFound => {
458
                return Err(Error::NotFound(key.to_string()))
459
            }
460
            Err(e) => return Err(e.into()),
461
        };
462
        let len = f.metadata()?.len();
463
        if range.end > len || range.start > range.end {
464
            return Err(Error::Backend(format!(
465
                "{key}: bytes {}..{} asked of an object of {len} bytes",
466
                range.start, range.end
467
            )));
468
        }
469
        f.seek(SeekFrom::Start(range.start))?;
470
        let mut out = vec![0u8; (range.end - range.start) as usize];
471
        f.read_exact(&mut out)?;
472
        Ok(out)
473
    }
474
475
    fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> {
476
        let path = self.path(key)?;
477
        if path.exists() {
478
            return Err(Error::Exists(key.to_string()));
479
        }
64818938 Andy McCall ↶ 480
        self.ensure_dir(&path)?;
a5f76c79 Andy McCall ↶ 481
        // Written beside its name, then linked into it: the link fails if
482
        // the name was taken meanwhile, and a reader never sees a part.
483
        let tmp = self.temp_beside(&path);
484
        let written = (|| {
485
            let mut f = fs::File::create(&tmp)?;
486
            f.write_all(bytes)?;
487
            f.sync_all()
488
        })();
64818938 Andy McCall ↶ 489
        if let Err(e) = written {
490
            let _ = fs::remove_file(&tmp);
491
            return Err(e.into());
a5f76c79 Andy McCall ↶ 492
        }
64818938 Andy McCall ↶ 493
        self.link_into_place(key, &tmp, &path)
a5f76c79 Andy McCall ↶ 494
    }
495
496
    fn list(&self, prefix: &str) -> Result<Vec<Entry>> {
497
        let mut out = Vec::new();
498
        self.list_dir(&self.root, "", prefix, &mut out)?;
499
        out.sort_by(|a, b| a.key.cmp(&b.key));
500
        Ok(out)
501
    }
502
503
    fn delete(&self, key: &str) -> Result<()> {
504
        let path = self.path(key)?;
505
        match fs::remove_file(&path) {
9aa39904 Andy McCall ↶ 506
            Ok(()) => {}
507
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
508
            Err(e) => return Err(e.into()),
509
        }
510
        // A directory left empty goes too, up to the root, so the store
511
        // holds nothing but its objects.
512
        let mut dir = path.parent();
513
        while let Some(d) = dir {
514
            if d == self.root || fs::remove_dir(d).is_err() {
515
                break;
516
            }
517
            dir = d.parent();
a5f76c79 Andy McCall ↶ 518
        }
9aa39904 Andy McCall ↶ 519
        Ok(())
a5f76c79 Andy McCall ↶ 520
    }
521
522
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> {
523
        let path = self.path(key)?;
524
        match fs::read(&path) {
525
            Ok(b) => {
526
                let v = content_version(&b);
527
                Ok(Some((b, v)))
528
            }
529
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
530
            Err(e) => Err(e.into()),
531
        }
532
    }
533
534
    fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> {
535
        let path = self.path(key)?;
536
        if let Some(dir) = path.parent() {
537
            fs::create_dir_all(dir)?;
538
        }
539
        let lock = self.lock(key, &path)?;
540
        let result = (|| {
541
            let current = self.read_ref(key)?.map(|(_, v)| v);
542
            check_expected(key, current.as_ref(), expected)?;
543
            crate::durable::write_atomic(&path, bytes)?;
544
            Ok(content_version(bytes))
545
        })();
546
        let _ = fs::remove_file(&lock);
547
        result
548
    }
549
}
550
551
// ---------------------------------------------------------------- memory
552
553
/// A store in memory, for tests: the conditional writes behave as the
554
/// cloud stores' do.
555
#[derive(Default)]
556
pub struct MemoryStore {
64818938 Andy McCall ↶ 557
    objects: Mutex<BTreeMap<String, (Vec<u8>, u64)>>,
a5f76c79 Andy McCall ↶ 558
}
559
64818938 Andy McCall ↶ 560
fn now_ms() -> u64 {
561
    std::time::SystemTime::now()
562
        .duration_since(std::time::UNIX_EPOCH)
563
        .map(|d| d.as_millis() as u64)
564
        .unwrap_or(0)
565
}
566
a5f76c79 Andy McCall ↶ 567
impl MemoryStore {
568
    pub fn new() -> MemoryStore {
569
        MemoryStore::default()
570
    }
571
64818938 Andy McCall ↶ 572
    fn map(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, (Vec<u8>, u64)>> {
a5f76c79 Andy McCall ↶ 573
        self.objects.lock().unwrap_or_else(|p| p.into_inner())
64818938 Andy McCall ↶ 574
    }
575
576
    /// Pretend `key` was written `age` ago (for grace-period tests).
577
    pub fn age(&self, key: &str, age: Duration) {
578
        if let Some(entry) = self.map().get_mut(key) {
579
            entry.1 = now_ms().saturating_sub(age.as_millis() as u64);
580
        }
a5f76c79 Andy McCall ↶ 581
    }
582
}
583
584
impl Store for MemoryStore {
585
    fn describe(&self) -> String {
586
        "memory".into()
587
    }
588
589
    fn has(&self, key: &str) -> Result<bool> {
590
        check_key(key)?;
591
        Ok(self.map().contains_key(key))
592
    }
593
594
    fn get(&self, key: &str) -> Result<Vec<u8>> {
595
        check_key(key)?;
596
        self.map()
597
            .get(key)
64818938 Andy McCall ↶ 598
            .map(|(b, _)| b.clone())
a5f76c79 Andy McCall ↶ 599
            .ok_or_else(|| Error::NotFound(key.to_string()))
600
    }
601
602
    fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> {
603
        let all = self.get(key)?;
604
        let len = all.len() as u64;
605
        if range.end > len || range.start > range.end {
606
            return Err(Error::Backend(format!(
607
                "{key}: bytes {}..{} asked of an object of {len} bytes",
608
                range.start, range.end
609
            )));
610
        }
611
        Ok(all[range.start as usize..range.end as usize].to_vec())
612
    }
613
614
    fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> {
615
        check_key(key)?;
616
        let mut map = self.map();
617
        if map.contains_key(key) {
618
            return Err(Error::Exists(key.to_string()));
619
        }
64818938 Andy McCall ↶ 620
        map.insert(key.to_string(), (bytes.to_vec(), now_ms()));
a5f76c79 Andy McCall ↶ 621
        Ok(())
622
    }
623
624
    fn list(&self, prefix: &str) -> Result<Vec<Entry>> {
625
        Ok(self
626
            .map()
627
            .range(prefix.to_string()..)
628
            .take_while(|(k, _)| k.starts_with(prefix))
64818938 Andy McCall ↶ 629
            .map(|(k, (v, t))| Entry {
a5f76c79 Andy McCall ↶ 630
                key: k.clone(),
631
                size: v.len() as u64,
64818938 Andy McCall ↶ 632
                modified_ms: Some(*t),
a5f76c79 Andy McCall ↶ 633
            })
634
            .collect())
635
    }
636
637
    fn delete(&self, key: &str) -> Result<()> {
638
        check_key(key)?;
639
        self.map().remove(key);
640
        Ok(())
641
    }
642
643
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> {
644
        check_key(key)?;
64818938 Andy McCall ↶ 645
        Ok(self
646
            .map()
647
            .get(key)
648
            .map(|(b, _)| (b.clone(), content_version(b))))
a5f76c79 Andy McCall ↶ 649
    }
650
651
    fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> {
652
        check_key(key)?;
653
        let mut map = self.map();
64818938 Andy McCall ↶ 654
        let current = map.get(key).map(|(b, _)| content_version(b));
a5f76c79 Andy McCall ↶ 655
        check_expected(key, current.as_ref(), expected)?;
64818938 Andy McCall ↶ 656
        map.insert(key.to_string(), (bytes.to_vec(), now_ms()));
a5f76c79 Andy McCall ↶ 657
        Ok(content_version(bytes))
658
    }
659
}
660
661
// ---------------------------------------------------------------- conformance
662
663
/// Exercise a store under `prefix` (which must be free, and is left
664
/// empty) and report what passed: object round trips, ranges, create-only
665
/// refusal, listing, deletion, and compare-and-swap on a reference. Every
666
/// store runs this in its tests, and `oxy remote check` runs it against a
667
/// remote before trusting it with a repository.
668
pub fn conformance(store: &dyn Store, prefix: &str) -> Result<Vec<&'static str>> {
669
    let mut passed = Vec::new();
670
    let key = |name: &str| format!("{prefix}/{name}");
671
    let fail = |what: &str| Error::Backend(format!("{}: {what}", store.describe()));
672
    let expect = |ok: bool, what: &str| if ok { Ok(()) } else { Err(fail(what)) };
673
674
    expect(
675
        store.list(prefix)?.is_empty(),
676
        "the probe prefix is not empty",
677
    )?;
678
    expect(
679
        !store.has(&key("a"))?,
680
        "has() of a missing object says true",
681
    )?;
682
    expect(
683
        matches!(store.get(&key("a")), Err(Error::NotFound(_))),
684
        "get() of a missing object is not NotFound",
685
    )?;
686
    passed.push("missing objects are reported missing");
687
688
    let a: Vec<u8> = (0..256u32 * 1024).map(|i| (i % 251) as u8).collect();
689
    store.put_new(&key("a"), &a)?;
690
    expect(store.has(&key("a"))?, "has() after put_new says false")?;
691
    expect(
692
        store.get(&key("a"))? == a,
693
        "get() returns other bytes than put_new wrote",
694
    )?;
695
    passed.push("an object round-trips");
696
697
    expect(
698
        store.get_range(&key("a"), 1000..1500)? == a[1000..1500],
699
        "get_range() returns the wrong bytes",
700
    )?;
701
    expect(
702
        store.get_range(&key("a"), 0..0)?.is_empty(),
703
        "an empty range is not empty",
704
    )?;
705
    expect(
706
        store.get_range(&key("a"), 0..a.len() as u64 + 1).is_err(),
707
        "a range past the end is not refused",
708
    )?;
709
    passed.push("ranges read correctly and are bounded");
710
711
    expect(
712
        matches!(store.put_new(&key("a"), b"other"), Err(Error::Exists(_))),
713
        "put_new() over an existing object is not refused",
714
    )?;
715
    expect(
716
        store.get(&key("a"))? == a,
717
        "a refused put_new() changed the object",
718
    )?;
719
    passed.push("objects are written once");
720
721
    store.put_new(&key("dir/b"), b"bb")?;
64818938 Andy McCall ↶ 722
    let listed: Vec<(String, u64)> = store
723
        .list(prefix)?
724
        .into_iter()
725
        .map(|e| (e.key, e.size))
726
        .collect();
a5f76c79 Andy McCall ↶ 727
    expect(
64818938 Andy McCall ↶ 728
        listed == vec![(key("a"), a.len() as u64), (key("dir/b"), 2)],
a5f76c79 Andy McCall ↶ 729
        "list() does not return the two objects, sorted, with their sizes",
730
    )?;
731
    expect(
64818938 Andy McCall ↶ 732
        store
733
            .stat(&key("dir/b"))?
734
            .is_some_and(|e| e.key == key("dir/b") && e.size == 2)
735
            && store.stat(&key("dir/c"))?.is_none(),
736
        "stat() does not describe one object, or describes a missing one",
737
    )?;
738
    expect(
739
        store
740
            .list(&key("dir/"))?
741
            .into_iter()
742
            .map(|e| (e.key, e.size))
743
            .collect::<Vec<_>>()
744
            == vec![(key("dir/b"), 2)],
a5f76c79 Andy McCall ↶ 745
        "list() of a sub-prefix is wrong",
746
    )?;
747
    expect(
748
        store.list(&key("nothing/"))?.is_empty(),
749
        "list() of an unused prefix is not empty",
750
    )?;
751
    passed.push("listing by prefix");
752
753
    store.delete(&key("dir/b"))?;
754
    expect(
755
        !store.has(&key("dir/b"))?,
756
        "an object is still there after delete()",
757
    )?;
758
    store.delete(&key("dir/b"))?;
759
    passed.push("deletion, and deleting again, succeed");
760
761
    let r = key("refs");
762
    expect(
763
        store.read_ref(&r)?.is_none(),
764
        "an unwritten reference reads as something",
765
    )?;
766
    let v1 = store.write_ref(&r, b"one", None)?;
767
    expect(
768
        matches!(store.write_ref(&r, b"again", None), Err(Error::Conflict(_))),
769
        "a second create-only write of the reference is not refused",
770
    )?;
771
    let (bytes, seen) = store
772
        .read_ref(&r)?
773
        .ok_or_else(|| fail("the reference vanished"))?;
774
    expect(
775
        bytes == b"one" && seen == v1,
776
        "the reference reads back wrong",
777
    )?;
778
    let v2 = store.write_ref(&r, b"two", Some(&v1))?;
779
    expect(v2 != v1, "the version did not change on write")?;
780
    expect(
781
        matches!(
782
            store.write_ref(&r, b"three", Some(&v1)),
783
            Err(Error::Conflict(_))
784
        ),
785
        "a write with a stale version is not refused",
786
    )?;
787
    let (bytes, seen) = store
788
        .read_ref(&r)?
789
        .ok_or_else(|| fail("the reference vanished"))?;
790
    expect(
791
        bytes == b"two" && seen == v2,
792
        "a refused write changed the reference",
793
    )?;
794
    passed.push("references update by compare-and-swap");
795
796
    store.delete(&key("a"))?;
797
    store.delete(&r)?;
798
    expect(
799
        store.list(prefix)?.is_empty(),
800
        "the probe prefix is not empty after cleanup",
801
    )?;
802
    passed.push("cleanup");
803
    Ok(passed)
804
}
805
806
#[cfg(test)]
807
mod tests {
808
    use super::*;
809
    use std::sync::Arc;
810
811
    #[test]
812
    fn keys_are_plain_relative_paths() {
813
        for ok in ["a", "packs/ab.pack", "a-b_c.d/e"] {
814
            check_key(ok).unwrap();
815
        }
816
        for bad in [
817
            "", "/a", "a/", "a//b", "..", "a/../b", "./a", "a b", "a\\b", "ä",
818
        ] {
819
            assert!(check_key(bad).is_err(), "{bad:?}");
820
        }
821
        assert!(check_key(&"x".repeat(MAX_KEY_LEN + 1)).is_err());
822
    }
823
824
    #[test]
825
    fn the_memory_store_conforms() {
826
        let store = MemoryStore::new();
827
        let passed = conformance(&store, "probe").unwrap();
828
        assert_eq!(passed.len(), 8, "{passed:?}");
829
    }
830
831
    #[test]
832
    fn the_directory_store_conforms() {
833
        let dir = tempfile::tempdir().unwrap();
834
        let store = LocalStore::open(dir.path().join("bucket")).unwrap();
835
        let passed = conformance(&store, "repos/one").unwrap();
836
        assert_eq!(passed.len(), 8, "{passed:?}");
837
        // Nothing but the (empty) directories and no stray temporaries.
838
        let stray: Vec<_> = walkdir(dir.path())
839
            .into_iter()
840
            .filter(|p| p.is_file())
841
            .collect();
842
        assert!(stray.is_empty(), "{stray:?}");
9aa39904 Andy McCall ↶ 843
        assert_eq!(
844
            fs::read_dir(dir.path().join("bucket")).unwrap().count(),
845
            0,
846
            "no empty directories left"
847
        );
a5f76c79 Andy McCall ↶ 848
    }
849
850
    fn walkdir(dir: &Path) -> Vec<PathBuf> {
851
        let mut out = vec![];
852
        if let Ok(rd) = fs::read_dir(dir) {
853
            for e in rd.flatten() {
854
                let p = e.path();
855
                if p.is_dir() {
856
                    out.extend(walkdir(&p));
857
                } else {
858
                    out.push(p);
859
                }
860
            }
861
        }
862
        out
863
    }
864
865
    #[test]
866
    fn a_directory_store_refuses_keys_that_leave_its_root() {
867
        let dir = tempfile::tempdir().unwrap();
868
        let store = LocalStore::open(dir.path().join("bucket")).unwrap();
869
        assert!(matches!(
870
            store.put_new("../escape", b"x"),
871
            Err(Error::Key(..))
872
        ));
873
        assert!(matches!(store.get("/etc/passwd"), Err(Error::Key(..))));
874
        assert!(!dir.path().join("escape").exists());
875
    }
876
877
    /// Many threads race to move a reference from the same version: exactly
878
    /// one wins each round, on both stores.
879
    #[test]
880
    fn compare_and_swap_admits_one_writer_per_round() {
881
        let dir = tempfile::tempdir().unwrap();
882
        let local: Arc<dyn Store> = Arc::new(LocalStore::open(dir.path()).unwrap());
883
        let memory: Arc<dyn Store> = Arc::new(MemoryStore::new());
884
        for store in [local, memory] {
885
            let v0 = store.write_ref("refs", b"0", None).unwrap();
886
            let wins: Vec<bool> = (0..8)
887
                .map(|i| {
888
                    let store = store.clone();
889
                    let v0 = v0.clone();
890
                    std::thread::spawn(move || {
891
                        store
892
                            .write_ref("refs", format!("{}", i + 1).as_bytes(), Some(&v0))
893
                            .is_ok()
894
                    })
895
                })
896
                .collect::<Vec<_>>()
897
                .into_iter()
898
                .map(|h| h.join().unwrap())
899
                .collect();
900
            assert_eq!(
901
                wins.iter().filter(|w| **w).count(),
902
                1,
903
                "{}: {wins:?}",
904
                store.describe()
905
            );
906
            let (bytes, _) = store.read_ref("refs").unwrap().unwrap();
907
            assert_ne!(bytes, b"0");
908
        }
909
    }
910
911
    #[test]
912
    fn a_stale_lock_is_broken_and_a_live_one_waited_for() {
913
        let dir = tempfile::tempdir().unwrap();
914
        let store = LocalStore::open(dir.path()).unwrap();
915
        let lock = dir.path().join(".refs.lock");
916
        fs::write(&lock, "dead").unwrap();
917
        let old = std::time::SystemTime::now() - LOCK_STALE - Duration::from_secs(5);
918
        fs::File::open(&lock).unwrap().set_modified(old).unwrap();
919
        store.write_ref("refs", b"after a crash", None).unwrap();
920
        assert!(!lock.exists());
921
    }
922
923
    #[test]
924
    fn temporaries_and_locks_are_not_listed() {
925
        let dir = tempfile::tempdir().unwrap();
926
        let store = LocalStore::open(dir.path()).unwrap();
927
        store.put_new("packs/a", b"a").unwrap();
928
        fs::write(dir.path().join("packs/.a.tmp-ffffff"), "junk").unwrap();
929
        fs::write(dir.path().join(".refs.lock"), "1").unwrap();
930
        let keys: Vec<String> = store.list("").unwrap().into_iter().map(|e| e.key).collect();
931
        assert_eq!(keys, ["packs/a"]);
932
    }
933
}