OXYGENHUB

Coverage / crates/oxy-core/src/strata.rs 87.4%

768 of 879 lines run, 147 of 234 functions · run #290 on main · run missed

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
//!
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.
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,
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>,
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.
74131305×
pub fn check_key(key: &str) -> Result<()> {
75131305×
    let bad = |why| Error::Key(key.to_string(), why);
76131305×
    if key.is_empty() {
771×
        return Err(bad("empty"));
78131304×
    }
79131304×
    if key.len() > MAX_KEY_LEN {
801×
        return Err(bad("longer than 1024 bytes"));
81131303×
    }
82356749×
    for seg in key.split('/') {
83356749×
        if seg.is_empty() {
844×
            return Err(bad(
854×
                "an empty segment (a leading, trailing or doubled slash)",
864×
            ));
87356745×
        }
88356745×
        if seg == "." || seg == ".." {
894×
            return Err(bad("a . or .. segment"));
90356741×
        }
91356741×
        if !seg
92356741×
            .bytes()
938395064×
            .all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
94
        {
953×
            return Err(bad("a character outside A-Z a-z 0-9 . _ -"));
96356738×
        }
97
    }
98131292×
    Ok(())
99131305×
}
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<()>;
122
    /// One object's listing entry without listing its neighbours; `None`
123
    /// when there is none.
124380×
    fn stat(&self, key: &str) -> Result<Option<Entry>> {
125380×
        Ok(self.list(key)?.into_iter().find(|e| e.key == key))
126380×
    }
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.
1301×
    fn put_many_new(&self, objects: &[(String, Vec<u8>)]) -> Result<()> {
1312×
        for (key, bytes) in objects {
1322×
            match self.put_new(key, bytes) {
1332×
                Ok(()) | Err(Error::Exists(_)) => {}
1340×
                Err(e) => return Err(e),
135
            }
136
        }
1371×
        Ok(())
1381×
    }
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.
14123×
    fn put_new_file(&self, key: &str, path: &Path) -> Result<()> {
14223×
        self.put_new(key, &fs::read(path)?)
14323×
    }
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.
158230×
fn content_version(bytes: &[u8]) -> Version {
159230×
    Version(blake3::hash(bytes).to_hex().to_string())
160230×
}
161
16289×
fn check_expected(key: &str, current: Option<&Version>, expected: Option<&Version>) -> Result<()> {
16389×
    if current == expected {
16457×
        Ok(())
165
    } else {
16632×
        Err(Error::Conflict(key.to_string()))
167
    }
16889×
}
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.
1882777×
    pub fn open(root: impl Into<PathBuf>) -> Result<LocalStore> {
1892777×
        let root = root.into();
1902777×
        fs::create_dir_all(&root)?;
1912777×
        Ok(LocalStore { root })
1922777×
    }
193
1941×
    pub fn root(&self) -> &Path {
1951×
        &self.root
1961×
    }
197
198128238×
    fn path(&self, key: &str) -> Result<PathBuf> {
199128238×
        check_key(key)?;
200128236×
        Ok(self.root.join(key))
201128238×
    }
202
20312269×
    fn temp_beside(&self, path: &Path) -> PathBuf {
20412269×
        let mut nonce = [0u8; 6];
20512269×
        rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut nonce);
20612269×
        let name = path
20712269×
            .file_name()
20812269×
            .map(|n| n.to_string_lossy().into_owned())
20912269×
            .unwrap_or_default();
21012269×
        path.with_file_name(format!(".{name}.tmp-{}", hex::encode(nonce)))
21112269×
    }
212
21326860×
    fn list_dir(&self, dir: &Path, rel: &str, prefix: &str, out: &mut Vec<Entry>) -> Result<()> {
21426860×
        let entries = match fs::read_dir(dir) {
21526860×
            Ok(e) => e,
2160×
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
2170×
            Err(e) => return Err(e.into()),
218
        };
21959755×
        for entry in entries {
22059755×
            let entry = entry?;
22159755×
            let name = entry.file_name().to_string_lossy().into_owned();
222
            // Temporary and lock files are not objects.
22359755×
            if name.starts_with('.') {
22434×
                continue;
22559721×
            }
22659721×
            let key = if rel.is_empty() {
22719054×
                name.clone()
228
            } else {
22940667×
                format!("{rel}/{name}")
230
            };
23159721×
            let ty = entry.file_type()?;
23259721×
            if ty.is_dir() {
233
                // Descend only where the prefix could still match.
23433469×
                let dir_prefix = format!("{key}/");
23533469×
                if prefix.starts_with(&dir_prefix) || dir_prefix.starts_with(prefix) {
23619168×
                    self.list_dir(&entry.path(), &key, prefix, out)?;
23714301×
                }
23826252×
            } else if ty.is_file() && key.starts_with(prefix) {
23919895×
                let meta = entry.metadata()?;
24019895×
                out.push(Entry {
24119895×
                    key,
24219895×
                    size: meta.len(),
24319895×
                    modified_ms: modified_ms(&meta),
24419895×
                });
2456357×
            }
246
        }
24726860×
        Ok(())
24826860×
    }
249
250
    /// Take `path.lock`, waiting for another writer, and removing a lock
251
    /// a dead process left behind.
25251×
    fn lock(&self, key: &str, path: &Path) -> Result<PathBuf> {
25351×
        let lock = path.with_file_name(format!(
254
            ".{}.lock",
25551×
            path.file_name()
25651×
                .map(|n| n.to_string_lossy().into_owned())
25751×
                .unwrap_or_default()
258
        ));
25951×
        let started = Instant::now();
260
        loop {
26152×
            match fs::OpenOptions::new()
26252×
                .write(true)
26352×
                .create_new(true)
26452×
                .open(&lock)
265
            {
26651×
                Ok(mut f) => {
26751×
                    let _ = writeln!(f, "{}", std::process::id());
26851×
                    return Ok(lock);
269
                }
2701×
                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
2711×
                    if let Ok(m) = fs::metadata(&lock) {
2721×
                        let age = m.modified().ok().and_then(|t| t.elapsed().ok());
2731×
                        if age.is_some_and(|a| a > LOCK_STALE) {
2741×
                            let _ = fs::remove_file(&lock);
2751×
                            continue;
2760×
                        }
2770×
                    }
2780×
                    if started.elapsed() > LOCK_WAIT {
2790×
                        return Err(Error::Busy(key.to_string()));
2800×
                    }
2810×
                    std::thread::sleep(Duration::from_millis(10));
282
                }
2830×
                Err(e) => return Err(e.into()),
284
            }
285
        }
28651×
    }
287
}
288
289
/// A file's modification time as wall milliseconds.
29020619×
fn modified_ms(meta: &fs::Metadata) -> Option<u64> {
29120619×
    meta.modified()
29220619×
        .ok()
29320619×
        .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
29420619×
        .map(|d| d.as_millis() as u64)
29520619×
}
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.
3007544×
    fn link_into_place(&self, key: &str, tmp: &Path, path: &Path) -> Result<()> {
3017544×
        let linked = fs::hard_link(tmp, path);
3027544×
        let _ = fs::remove_file(tmp);
3030×
        match linked {
304
            Ok(()) => {
3057544×
                if let Some(dir) = path.parent() {
3067544×
                    crate::durable::sync_dir(dir)?;
3070×
                }
3087544×
                Ok(())
309
            }
3100×
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
3110×
                Err(Error::Exists(key.to_string()))
312
            }
3130×
            Err(e) => Err(e.into()),
314
        }
3157544×
    }
316
317
    /// Create `path`'s directory, syncing the first directory that was
318
    /// made so its entry is durable too.
31912321×
    fn ensure_dir(&self, path: &Path) -> Result<()> {
32012321×
        let Some(dir) = path.parent() else {
3210×
            return Ok(());
322
        };
32312321×
        if dir.is_dir() {
3242138×
            return Ok(());
32510183×
        }
32610183×
        let mut made = dir.to_path_buf();
32710826×
        while let Some(parent) = made.parent() {
32810826×
            if parent.is_dir() || parent == self.root {
32910183×
                break;
330643×
            }
331643×
            made = parent.to_path_buf();
332
        }
33310183×
        fs::create_dir_all(dir)?;
33410183×
        if let Some(parent) = made.parent() {
33510183×
            crate::durable::sync_dir(parent)?;
3360×
        }
33710183×
        Ok(())
33812321×
    }
339
}
340
341
impl Store for LocalStore {
34286×
    fn describe(&self) -> String {
34386×
        self.root.display().to_string()
34486×
    }
345
346730×
    fn stat(&self, key: &str) -> Result<Option<Entry>> {
347730×
        match fs::metadata(self.path(key)?) {
348724×
            Ok(m) if m.is_file() => Ok(Some(Entry {
349724×
                key: key.to_string(),
350724×
                size: m.len(),
351724×
                modified_ms: modified_ms(&m),
352724×
            })),
3530×
            Ok(_) => Ok(None),
3546×
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
3550×
            Err(e) => Err(e.into()),
356
        }
357730×
    }
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.
3621351×
    fn put_many_new(&self, objects: &[(String, Vec<u8>)]) -> Result<()> {
363
        use rayon::prelude::*;
3641351×
        let mut todo: Vec<(PathBuf, PathBuf, &[u8])> = Vec::new();
3654725×
        for (key, bytes) in objects {
3664725×
            let path = self.path(key)?;
3677533×
            if path.exists() || todo.iter().any(|(p, ..)| *p == path) {
3680×
                continue;
3694725×
            }
3704725×
            self.ensure_dir(&path)?;
3714725×
            todo.push((path.clone(), self.temp_beside(&path), bytes));
372
        }
3731351×
        if todo.is_empty() {
3740×
            return Ok(());
3751351×
        }
3761351×
        crate::durable::step("write loose objects")?;
3771349×
        todo.par_iter()
3784712×
            .try_for_each(|(_, tmp, bytes)| -> io::Result<()> {
3794712×
                let mut f = fs::File::create(tmp)?;
3804712×
                f.write_all(bytes)?;
3814712×
                f.sync_all()
3824712×
            })?;
3831349×
        crate::durable::step("rename loose objects")?;
3841347×
        let mut dirs: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::new();
3854699×
        for (path, tmp, _) in &todo {
3864699×
            let linked = fs::hard_link(tmp, path);
3874699×
            let _ = fs::remove_file(tmp);
3880×
            match linked {
389
                Ok(()) => {
3904699×
                    if let Some(d) = path.parent() {
3914699×
                        dirs.insert(d.to_path_buf());
3924699×
                    }
393
                }
3940×
                Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
3950×
                Err(e) => return Err(e.into()),
396
            }
397
        }
3981347×
        dirs.par_iter()
3994668×
            .try_for_each(|d| crate::durable::sync_dir(d))?;
4001347×
        Ok(())
4011351×
    }
402
403
    /// The file is linked into place when it is on this file system,
404
    /// else copied; it is removed either way.
40552×
    fn put_new_file(&self, key: &str, file: &Path) -> Result<()> {
40652×
        let path = self.path(key)?;
40752×
        if path.exists() {
4080×
            let _ = fs::remove_file(file);
4090×
            return Err(Error::Exists(key.to_string()));
41052×
        }
41152×
        self.ensure_dir(&path)?;
41252×
        match fs::hard_link(file, &path) {
413
            Ok(()) => {
41452×
                let _ = fs::remove_file(file);
41552×
                if let Some(dir) = path.parent() {
41652×
                    crate::durable::sync_dir(dir)?;
4170×
                }
41852×
                Ok(())
419
            }
4200×
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
4210×
                let _ = fs::remove_file(file);
4220×
                Err(Error::Exists(key.to_string()))
423
            }
424
            Err(_) => {
425
                // Another file system: copy beside the name, then link.
4260×
                let tmp = self.temp_beside(&path);
4270×
                let copied = (|| {
4280×
                    let mut src = fs::File::open(file)?;
4290×
                    let mut dst = fs::File::create(&tmp)?;
4300×
                    io::copy(&mut src, &mut dst)?;
4310×
                    dst.sync_all()
432
                })();
4330×
                let _ = fs::remove_file(file);
4340×
                copied?;
4350×
                self.link_into_place(key, &tmp, &path)
436
            }
437
        }
43852×
    }
439
44028075×
    fn has(&self, key: &str) -> Result<bool> {
44128075×
        Ok(self.path(key)?.is_file())
44228075×
    }
443
44469099×
    fn get(&self, key: &str) -> Result<Vec<u8>> {
44569099×
        let path = self.path(key)?;
44669098×
        match fs::read(&path) {
44748834×
            Ok(b) => Ok(b),
44820264×
            Err(e) if e.kind() == io::ErrorKind::NotFound => Err(Error::NotFound(key.to_string())),
4490×
            Err(e) => Err(e.into()),
450
        }
45169099×
    }
452
45314851×
    fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> {
45414851×
        let path = self.path(key)?;
45514851×
        let mut f = match fs::File::open(&path) {
45614849×
            Ok(f) => f,
4572×
            Err(e) if e.kind() == io::ErrorKind::NotFound => {
4582×
                return Err(Error::NotFound(key.to_string()))
459
            }
4600×
            Err(e) => return Err(e.into()),
461
        };
46214849×
        let len = f.metadata()?.len();
46314849×
        if range.end > len || range.start > range.end {
4645×
            return Err(Error::Backend(format!(
4655×
                "{key}: bytes {}..{} asked of an object of {len} bytes",
4665×
                range.start, range.end
4675×
            )));
46814844×
        }
46914844×
        f.seek(SeekFrom::Start(range.start))?;
47014844×
        let mut out = vec![0u8; (range.end - range.start) as usize];
47114844×
        f.read_exact(&mut out)?;
47214844×
        Ok(out)
47314851×
    }
474
4757565×
    fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> {
4767565×
        let path = self.path(key)?;
4777564×
        if path.exists() {
47820×
            return Err(Error::Exists(key.to_string()));
4797544×
        }
4807544×
        self.ensure_dir(&path)?;
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.
4837544×
        let tmp = self.temp_beside(&path);
4847544×
        let written = (|| {
4857544×
            let mut f = fs::File::create(&tmp)?;
4867544×
            f.write_all(bytes)?;
4877544×
            f.sync_all()
488
        })();
4897544×
        if let Err(e) = written {
4900×
            let _ = fs::remove_file(&tmp);
4910×
            return Err(e.into());
4927544×
        }
4937544×
        self.link_into_place(key, &tmp, &path)
4947565×
    }
495
4967692×
    fn list(&self, prefix: &str) -> Result<Vec<Entry>> {
4977692×
        let mut out = Vec::new();
4987692×
        self.list_dir(&self.root, "", prefix, &mut out)?;
49997771×
        out.sort_by(|a, b| a.key.cmp(&b.key));
5007692×
        Ok(out)
5017692×
    }
502
5032927×
    fn delete(&self, key: &str) -> Result<()> {
5042927×
        let path = self.path(key)?;
5052927×
        match fs::remove_file(&path) {
5062282×
            Ok(()) => {}
507645×
            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
5080×
            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.
5122282×
        let mut dir = path.parent();
5133120×
        while let Some(d) = dir {
5143120×
            if d == self.root || fs::remove_dir(d).is_err() {
5152282×
                break;
516838×
            }
517838×
            dir = d.parent();
518
        }
5192282×
        Ok(())
5202927×
    }
521
522133×
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> {
523133×
        let path = self.path(key)?;
524133×
        match fs::read(&path) {
525105×
            Ok(b) => {
526105×
                let v = content_version(&b);
527105×
                Ok(Some((b, v)))
528
            }
52928×
            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
5300×
            Err(e) => Err(e.into()),
531
        }
532133×
    }
533
53451×
    fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> {
53551×
        let path = self.path(key)?;
53651×
        if let Some(dir) = path.parent() {
53751×
            fs::create_dir_all(dir)?;
5380×
        }
53951×
        let lock = self.lock(key, &path)?;
54051×
        let result = (|| {
54151×
            let current = self.read_ref(key)?.map(|(_, v)| v);
54251×
            check_expected(key, current.as_ref(), expected)?;
54335×
            crate::durable::write_atomic(&path, bytes)?;
54435×
            Ok(content_version(bytes))
545
        })();
54651×
        let _ = fs::remove_file(&lock);
54751×
        result
54851×
    }
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 {
557
    objects: Mutex<BTreeMap<String, (Vec<u8>, u64)>>,
558
}
559
560254×
fn now_ms() -> u64 {
561254×
    std::time::SystemTime::now()
562254×
        .duration_since(std::time::UNIX_EPOCH)
563254×
        .map(|d| d.as_millis() as u64)
564254×
        .unwrap_or(0)
565254×
}
566
567
impl MemoryStore {
56815×
    pub fn new() -> MemoryStore {
56915×
        MemoryStore::default()
57015×
    }
571
5723604×
    fn map(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, (Vec<u8>, u64)>> {
5733604×
        self.objects.lock().unwrap_or_else(|p| p.into_inner())
5743604×
    }
575
576
    /// Pretend `key` was written `age` ago (for grace-period tests).
5770×
    pub fn age(&self, key: &str, age: Duration) {
5780×
        if let Some(entry) = self.map().get_mut(key) {
5790×
            entry.1 = now_ms().saturating_sub(age.as_millis() as u64);
5800×
        }
5810×
    }
582
}
583
584
impl Store for MemoryStore {
5851×
    fn describe(&self) -> String {
5861×
        "memory".into()
5871×
    }
588
589693×
    fn has(&self, key: &str) -> Result<bool> {
590693×
        check_key(key)?;
591693×
        Ok(self.map().contains_key(key))
592693×
    }
593
5941923×
    fn get(&self, key: &str) -> Result<Vec<u8>> {
5951923×
        check_key(key)?;
5961923×
        self.map()
5971923×
            .get(key)
5981923×
            .map(|(b, _)| b.clone())
5991923×
            .ok_or_else(|| Error::NotFound(key.to_string()))
6001923×
    }
601
602362×
    fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> {
603362×
        let all = self.get(key)?;
604362×
        let len = all.len() as u64;
605362×
        if range.end > len || range.start > range.end {
6061×
            return Err(Error::Backend(format!(
6071×
                "{key}: bytes {}..{} asked of an object of {len} bytes",
6081×
                range.start, range.end
6091×
            )));
610361×
        }
611361×
        Ok(all[range.start as usize..range.end as usize].to_vec())
612362×
    }
613
614235×
    fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> {
615235×
        check_key(key)?;
616235×
        let mut map = self.map();
617235×
        if map.contains_key(key) {
6183×
            return Err(Error::Exists(key.to_string()));
619232×
        }
620232×
        map.insert(key.to_string(), (bytes.to_vec(), now_ms()));
621232×
        Ok(())
622235×
    }
623
624551×
    fn list(&self, prefix: &str) -> Result<Vec<Entry>> {
625551×
        Ok(self
626551×
            .map()
627551×
            .range(prefix.to_string()..)
6281673×
            .take_while(|(k, _)| k.starts_with(prefix))
629551×
            .map(|(k, (v, t))| Entry {
6301256×
                key: k.clone(),
6311256×
                size: v.len() as u64,
6321256×
                modified_ms: Some(*t),
6331256×
            })
634551×
            .collect())
635551×
    }
636
637119×
    fn delete(&self, key: &str) -> Result<()> {
638119×
        check_key(key)?;
639119×
        self.map().remove(key);
640119×
        Ok(())
641119×
    }
642
64345×
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> {
64445×
        check_key(key)?;
64545×
        Ok(self
64645×
            .map()
64745×
            .get(key)
64845×
            .map(|(b, _)| (b.clone(), content_version(b))))
64945×
    }
650
65138×
    fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> {
65238×
        check_key(key)?;
65338×
        let mut map = self.map();
65438×
        let current = map.get(key).map(|(b, _)| content_version(b));
65538×
        check_expected(key, current.as_ref(), expected)?;
65622×
        map.insert(key.to_string(), (bytes.to_vec(), now_ms()));
65722×
        Ok(content_version(bytes))
65838×
    }
659
}
660
661
// ---------------------------------------------------------------- caching
662
663
/// A store in front of another (oxygen#68): objects are read through a
664
/// local directory and written through it, to the remote first, so the
665
/// remote always holds everything and the cache can be dropped at any
666
/// time. Objects never change, so a cached one is always right (and every
667
/// read is hashed again above this layer). The cache is bounded: past its
668
/// limit the objects used least recently are removed. An object larger
669
/// than an eighth of the limit is not cached; it is read by range from
670
/// the remote. References are never cached: they are read from and
671
/// written to the remote every time, so compare-and-swap is the remote's.
672
pub struct CachingStore {
673
    cache: LocalStore,
674
    remote: std::sync::Arc<dyn Store>,
675
    limit: u64,
676
    /// Bytes in the cache, counted on first need and kept up to date.
677
    used: Mutex<Option<u64>>,
678
}
679
680
/// How stale a cached object's use time may get before a read renews it.
681
const TOUCH_AFTER: Duration = Duration::from_secs(60);
682
683
impl CachingStore {
684
    /// `remote` behind a cache in the directory `dir` of at most `limit`
685
    /// bytes.
6867×
    pub fn open(
6877×
        remote: std::sync::Arc<dyn Store>,
6887×
        dir: impl Into<PathBuf>,
6897×
        limit: u64,
6907×
    ) -> Result<CachingStore> {
691
        Ok(CachingStore {
6927×
            cache: LocalStore::open(dir)?,
6937×
            remote,
6947×
            limit,
6957×
            used: Mutex::new(None),
696
        })
6977×
    }
698
6990×
    pub fn remote(&self) -> &std::sync::Arc<dyn Store> {
7000×
        &self.remote
7010×
    }
702
703
    /// Bytes the cache holds now.
70423×
    pub fn cached_bytes(&self) -> Result<u64> {
70523×
        let mut used = self.used.lock().unwrap_or_else(|p| p.into_inner());
70623×
        if used.is_none() {
7071×
            *used = Some(self.cache.list("")?.iter().map(|e| e.size).sum());
70822×
        }
70923×
        Ok(used.unwrap_or(0))
71023×
    }
711
712
    /// Whether an object of `size` bytes is kept in the cache at all.
71366×
    fn cacheable(&self, size: u64) -> bool {
71466×
        size <= self.limit / 8
71566×
    }
716
717
    /// Keep a copy of an object the remote holds.
71864×
    fn keep(&self, key: &str, bytes: &[u8]) -> Result<()> {
71964×
        if !self.cacheable(bytes.len() as u64) {
7201×
            return Ok(());
72163×
        }
72263×
        match self.cache.put_new(key, bytes) {
72363×
            Ok(()) => self.added(bytes.len() as u64),
7240×
            Err(Error::Exists(_)) => Ok(()),
7250×
            Err(e) => Err(e),
726
        }
72764×
    }
728
729
    /// Count `n` new bytes, and remove the least recently used objects
730
    /// once the cache is past its limit, down to nine tenths of it.
73163×
    fn added(&self, n: u64) -> Result<()> {
73263×
        let total = {
73363×
            let mut used = self.used.lock().unwrap_or_else(|p| p.into_inner());
73463×
            let total = match *used {
73558×
                Some(u) => u + n,
736
                // Counted now, with the new object already in place.
7375×
                None => self.cache.list("")?.iter().map(|e| e.size).sum(),
738
            };
73963×
            *used = Some(total);
74063×
            total
741
        };
74263×
        if total <= self.limit {
74331×
            return Ok(());
74432×
        }
74532×
        let mut entries = self.cache.list("")?;
746736×
        entries.sort_by_key(|e| e.modified_ms.unwrap_or(0));
74732×
        let mut left: u64 = entries.iter().map(|e| e.size).sum();
74832×
        let target = self.limit / 10 * 9;
74964×
        for e in entries {
75064×
            if left <= target {
75132×
                break;
75232×
            }
75332×
            self.cache.delete(&e.key)?;
75432×
            left -= e.size;
755
        }
75632×
        *self.used.lock().unwrap_or_else(|p| p.into_inner()) = Some(left);
75732×
        Ok(())
75863×
    }
759
760
    /// Mark a cached object used now, at most once a minute.
76130×
    fn touch(&self, key: &str) {
76230×
        let Ok(path) = self.cache.path(key) else {
7630×
            return;
764
        };
76530×
        let stale = fs::metadata(&path)
76630×
            .and_then(|m| m.modified())
76730×
            .ok()
76830×
            .and_then(|t| t.elapsed().ok())
76930×
            .is_some_and(|age| age > TOUCH_AFTER);
77030×
        if stale {
7710×
            if let Ok(f) = fs::File::options().append(true).open(&path) {
7720×
                let _ = f.set_modified(std::time::SystemTime::now());
7730×
            }
77430×
        }
77530×
    }
776
}
777
778
impl Store for CachingStore {
7791×
    fn describe(&self) -> String {
7801×
        format!(
781
            "{} (cached in {})",
7821×
            self.remote.describe(),
7831×
            self.cache.root().display()
784
        )
7851×
    }
786
78717×
    fn has(&self, key: &str) -> Result<bool> {
78817×
        Ok(self.cache.has(key)? || self.remote.has(key)?)
78917×
    }
790
79157×
    fn get(&self, key: &str) -> Result<Vec<u8>> {
79257×
        match self.cache.get(key) {
79328×
            Ok(bytes) => {
79428×
                self.touch(key);
79528×
                return Ok(bytes);
796
            }
797
            // Not cached, or removed by another process's eviction meanwhile.
79829×
            Err(Error::NotFound(_)) => {}
7990×
            Err(e) => return Err(e),
800
        }
80129×
        let bytes = self.remote.get(key)?;
80228×
        self.keep(key, &bytes)?;
80328×
        Ok(bytes)
80457×
    }
805
8065×
    fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> {
8075×
        match self.cache.get_range(key, range.clone()) {
8082×
            Ok(bytes) => {
8092×
                self.touch(key);
8102×
                return Ok(bytes);
811
            }
8122×
            Err(Error::NotFound(_)) => {}
8131×
            Err(e) if self.cache.has(key)? => return Err(e),
8140×
            Err(_) => {}
815
        }
816
        // A small object is fetched whole and kept; a large one is read by
817
        // range, every time.
8182×
        match self.remote.stat(key)? {
8192×
            Some(e) if self.cacheable(e.size) => {
8201×
                let bytes = self.get(key)?;
8211×
                let len = bytes.len() as u64;
8221×
                if range.end > len || range.start > range.end {
8230×
                    return Err(Error::Backend(format!(
8240×
                        "{key}: bytes {}..{} asked of an object of {len} bytes",
8250×
                        range.start, range.end
8260×
                    )));
8271×
                }
8281×
                Ok(bytes[range.start as usize..range.end as usize].to_vec())
829
            }
8301×
            Some(_) => self.remote.get_range(key, range),
8310×
            None => Err(Error::NotFound(key.to_string())),
832
        }
8335×
    }
834
83529×
    fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> {
83629×
        self.remote.put_new(key, bytes)?;
83728×
        self.keep(key, bytes)
83829×
    }
839
8402×
    fn put_many_new(&self, objects: &[(String, Vec<u8>)]) -> Result<()> {
8412×
        self.remote.put_many_new(objects)?;
8428×
        for (key, bytes) in objects {
8438×
            self.keep(key, bytes)?;
844
        }
8452×
        Ok(())
8462×
    }
847
8480×
    fn put_new_file(&self, key: &str, path: &Path) -> Result<()> {
8490×
        let size = fs::metadata(path)?.len();
8500×
        let copy = if self.cacheable(size) {
8510×
            Some(fs::read(path)?)
852
        } else {
8530×
            None
854
        };
8550×
        self.remote.put_new_file(key, path)?;
8560×
        match copy {
8570×
            Some(bytes) => self.keep(key, &bytes),
8580×
            None => Ok(()),
859
        }
8600×
    }
861
86211×
    fn list(&self, prefix: &str) -> Result<Vec<Entry>> {
86311×
        self.remote.list(prefix)
86411×
    }
865
8662×
    fn stat(&self, key: &str) -> Result<Option<Entry>> {
8672×
        self.remote.stat(key)
8682×
    }
869
8704×
    fn delete(&self, key: &str) -> Result<()> {
8714×
        self.remote.delete(key)?;
8724×
        let size = self.cache.stat(key)?.map(|e| e.size);
8734×
        self.cache.delete(key)?;
8744×
        if let Some(n) = size {
8752×
            let mut used = self.used.lock().unwrap_or_else(|p| p.into_inner());
8762×
            if let Some(u) = used.as_mut() {
8772×
                *u = u.saturating_sub(n);
8782×
            }
8792×
        }
8804×
        Ok(())
8814×
    }
882
88317×
    fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> {
88417×
        self.remote.read_ref(key)
88517×
    }
886
8879×
    fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> {
8889×
        self.remote.write_ref(key, bytes, expected)
8899×
    }
890
}
891
892
// ---------------------------------------------------------------- conformance
893
894
/// Exercise a store under `prefix` (which must be free, and is left
895
/// empty) and report what passed: object round trips, ranges, create-only
896
/// refusal, listing, deletion, and compare-and-swap on a reference. Every
897
/// store runs this in its tests, and `oxy remote check` runs it against a
898
/// remote before trusting it with a repository.
8996×
pub fn conformance(store: &dyn Store, prefix: &str) -> Result<Vec<&'static str>> {
9006×
    let mut passed = Vec::new();
901144×
    let key = |name: &str| format!("{prefix}/{name}");
9026×
    let fail = |what: &str| Error::Backend(format!("{}: {what}", store.describe()));
903132×
    let expect = |ok: bool, what: &str| if ok { Ok(()) } else { Err(fail(what)) };
904
9056×
    expect(
9066×
        store.list(prefix)?.is_empty(),
9076×
        "the probe prefix is not empty",
9080×
    )?;
9096×
    expect(
9106×
        !store.has(&key("a"))?,
9116×
        "has() of a missing object says true",
9120×
    )?;
9136×
    expect(
9146×
        matches!(store.get(&key("a")), Err(Error::NotFound(_))),
9156×
        "get() of a missing object is not NotFound",
9160×
    )?;
9176×
    passed.push("missing objects are reported missing");
918
9191572864×
    let a: Vec<u8> = (0..256u32 * 1024).map(|i| (i % 251) as u8).collect();
9206×
    store.put_new(&key("a"), &a)?;
9216×
    expect(store.has(&key("a"))?, "has() after put_new says false")?;
9226×
    expect(
9236×
        store.get(&key("a"))? == a,
9246×
        "get() returns other bytes than put_new wrote",
9250×
    )?;
9266×
    passed.push("an object round-trips");
927
9286×
    expect(
9296×
        store.get_range(&key("a"), 1000..1500)? == a[1000..1500],
9306×
        "get_range() returns the wrong bytes",
9310×
    )?;
9326×
    expect(
9336×
        store.get_range(&key("a"), 0..0)?.is_empty(),
9346×
        "an empty range is not empty",
9350×
    )?;
9366×
    expect(
9376×
        store.get_range(&key("a"), 0..a.len() as u64 + 1).is_err(),
9386×
        "a range past the end is not refused",
9396×
    )?;
9406×
    passed.push("ranges read correctly and are bounded");
941
9426×
    expect(
9436×
        matches!(store.put_new(&key("a"), b"other"), Err(Error::Exists(_))),
9446×
        "put_new() over an existing object is not refused",
9450×
    )?;
9466×
    expect(
9476×
        store.get(&key("a"))? == a,
9486×
        "a refused put_new() changed the object",
9490×
    )?;
9506×
    passed.push("objects are written once");
951
9526×
    store.put_new(&key("dir/b"), b"bb")?;
9536×
    let listed: Vec<(String, u64)> = store
9546×
        .list(prefix)?
9556×
        .into_iter()
95612×
        .map(|e| (e.key, e.size))
9576×
        .collect();
9586×
    expect(
9596×
        listed == vec![(key("a"), a.len() as u64), (key("dir/b"), 2)],
9606×
        "list() does not return the two objects, sorted, with their sizes",
9616×
    )?;
9626×
    expect(
9636×
        store
9646×
            .stat(&key("dir/b"))?
9656×
            .is_some_and(|e| e.key == key("dir/b") && e.size == 2)
9666×
            && store.stat(&key("dir/c"))?.is_none(),
9676×
        "stat() does not describe one object, or describes a missing one",
9680×
    )?;
9696×
    expect(
9706×
        store
9716×
            .list(&key("dir/"))?
9726×
            .into_iter()
9736×
            .map(|e| (e.key, e.size))
9746×
            .collect::<Vec<_>>()
9756×
            == vec![(key("dir/b"), 2)],
9766×
        "list() of a sub-prefix is wrong",
9770×
    )?;
9786×
    expect(
9796×
        store.list(&key("nothing/"))?.is_empty(),
9806×
        "list() of an unused prefix is not empty",
9810×
    )?;
9826×
    passed.push("listing by prefix");
983
9846×
    store.delete(&key("dir/b"))?;
9856×
    expect(
9866×
        !store.has(&key("dir/b"))?,
9876×
        "an object is still there after delete()",
9880×
    )?;
9896×
    store.delete(&key("dir/b"))?;
9906×
    passed.push("deletion, and deleting again, succeed");
991
9926×
    let r = key("refs");
9936×
    expect(
9946×
        store.read_ref(&r)?.is_none(),
9956×
        "an unwritten reference reads as something",
9960×
    )?;
9976×
    let v1 = store.write_ref(&r, b"one", None)?;
9986×
    expect(
9996×
        matches!(store.write_ref(&r, b"again", None), Err(Error::Conflict(_))),
10006×
        "a second create-only write of the reference is not refused",
10010×
    )?;
10026×
    let (bytes, seen) = store
10036×
        .read_ref(&r)?
10046×
        .ok_or_else(|| fail("the reference vanished"))?;
10056×
    expect(
10066×
        bytes == b"one" && seen == v1,
10076×
        "the reference reads back wrong",
10080×
    )?;
10096×
    let v2 = store.write_ref(&r, b"two", Some(&v1))?;
10106×
    expect(v2 != v1, "the version did not change on write")?;
10116×
    expect(
10120×
        matches!(
10136×
            store.write_ref(&r, b"three", Some(&v1)),
1014
            Err(Error::Conflict(_))
1015
        ),
10166×
        "a write with a stale version is not refused",
10170×
    )?;
10186×
    let (bytes, seen) = store
10196×
        .read_ref(&r)?
10206×
        .ok_or_else(|| fail("the reference vanished"))?;
10216×
    expect(
10226×
        bytes == b"two" && seen == v2,
10236×
        "a refused write changed the reference",
10240×
    )?;
10256×
    passed.push("references update by compare-and-swap");
1026
10276×
    store.delete(&key("a"))?;
10286×
    store.delete(&r)?;
10296×
    expect(
10306×
        store.list(prefix)?.is_empty(),
10316×
        "the probe prefix is not empty after cleanup",
10320×
    )?;
10336×
    passed.push("cleanup");
10346×
    Ok(passed)
10356×
}
1036
1037
#[cfg(test)]
1038
mod tests {
1039
    use super::*;
1040
    use std::sync::Arc;
1041
1042
    #[test]
10431×
    fn keys_are_plain_relative_paths() {
10443×
        for ok in ["a", "packs/ab.pack", "a-b_c.d/e"] {
10453×
            check_key(ok).unwrap();
10463×
        }
104710×
        for bad in [
10481×
            "", "/a", "a/", "a//b", "..", "a/../b", "./a", "a b", "a\\b", "ä",
10491×
        ] {
105010×
            assert!(check_key(bad).is_err(), "{bad:?}");
1051
        }
10521×
        assert!(check_key(&"x".repeat(MAX_KEY_LEN + 1)).is_err());
10531×
    }
1054
1055
    #[test]
10561×
    fn a_caching_store_conforms_and_reads_and_writes_through() {
10571×
        let dir = tempfile::tempdir().unwrap();
10581×
        let remote: Arc<dyn Store> = Arc::new(MemoryStore::new());
10591×
        let store = CachingStore::open(remote.clone(), dir.path().join("cache"), 8 << 20).unwrap();
10601×
        assert_eq!(conformance(&store, "probe").unwrap().len(), 8);
10611×
        assert_eq!(
10621×
            store.cached_bytes().unwrap(),
1063
            0,
1064
            "the probe left nothing cached"
1065
        );
1066
1067
        // Written through: the remote has it, and so does the cache.
10681×
        store.put_new("loose/aa", b"written").unwrap();
10691×
        assert_eq!(remote.get("loose/aa").unwrap(), b"written");
10701×
        assert!(store.cache.has("loose/aa").unwrap());
1071
        // Read through: an object only the remote has is kept on first read.
10721×
        remote.put_new("loose/bb", b"remote only").unwrap();
10731×
        assert!(!store.cache.has("loose/bb").unwrap());
10741×
        assert_eq!(store.get_range("loose/bb", 0..6).unwrap(), b"remote");
10751×
        assert!(store.cache.has("loose/bb").unwrap());
1076
        // A cached object read after the remote lost it is still served.
10771×
        remote.delete("loose/bb").unwrap();
10781×
        assert_eq!(store.get("loose/bb").unwrap(), b"remote only");
1079
        // References are always the remote's.
10801×
        let v = remote.write_ref("refs", b"one", None).unwrap();
10811×
        assert_eq!(
10821×
            store.read_ref("refs").unwrap(),
10831×
            Some((b"one".to_vec(), v.clone()))
1084
        );
10851×
        remote.write_ref("refs", b"two", Some(&v)).unwrap();
10861×
        assert_eq!(store.read_ref("refs").unwrap().unwrap().0, b"two");
10871×
        assert!(matches!(
10881×
            store.write_ref("refs", b"three", Some(&v)),
1089
            Err(Error::Conflict(_))
1090
        ));
10911×
        assert!(!store.cache.has("refs").unwrap());
10921×
    }
1093
1094
    #[test]
10951×
    fn a_caching_store_stays_within_its_limit() {
10961×
        let dir = tempfile::tempdir().unwrap();
10971×
        let remote: Arc<dyn Store> = Arc::new(MemoryStore::new());
10981×
        let store = CachingStore::open(remote.clone(), dir.path().join("cache"), 8000).unwrap();
109920×
        for i in 0..20 {
110020×
            store
110120×
                .put_new(&format!("loose/{i:02}"), &[i as u8; 900])
110220×
                .unwrap();
110320×
            assert!(store.cached_bytes().unwrap() <= 8000);
1104
        }
11051×
        assert_eq!(
11061×
            remote.list("loose/").unwrap().len(),
1107
            20,
1108
            "the remote holds everything"
1109
        );
11101×
        let cached = store.cache.list("loose/").unwrap();
11111×
        assert!(cached.len() < 20 && cached.len() >= 7, "{}", cached.len());
11121×
        assert!(
11138×
            cached.iter().any(|e| e.key == "loose/19"),
1114
            "the newest is kept"
1115
        );
111620×
        for i in 0..20 {
111720×
            assert_eq!(
111820×
                store.get(&format!("loose/{i:02}")).unwrap(),
111920×
                vec![i as u8; 900]
1120
            );
1121
        }
1122
        // An object over an eighth of the limit is read by range, not kept.
11231×
        remote.put_new("pack-big", &[7u8; 2000]).unwrap();
11241×
        assert_eq!(store.get_range("pack-big", 10..20).unwrap(), vec![7u8; 10]);
11251×
        assert_eq!(store.get("pack-big").unwrap().len(), 2000);
11261×
        assert!(!store.cache.has("pack-big").unwrap());
1127
        // Counting from scratch agrees with what was kept track of.
11281×
        let fresh = CachingStore::open(remote, dir.path().join("cache"), 8000).unwrap();
11291×
        assert_eq!(fresh.cached_bytes().unwrap(), store.cached_bytes().unwrap());
11301×
    }
1131
1132
    #[test]
11331×
    fn the_memory_store_conforms() {
11341×
        let store = MemoryStore::new();
11351×
        let passed = conformance(&store, "probe").unwrap();
11361×
        assert_eq!(passed.len(), 8, "{passed:?}");
11371×
    }
1138
1139
    #[test]
11401×
    fn the_directory_store_conforms() {
11411×
        let dir = tempfile::tempdir().unwrap();
11421×
        let store = LocalStore::open(dir.path().join("bucket")).unwrap();
11431×
        let passed = conformance(&store, "repos/one").unwrap();
11441×
        assert_eq!(passed.len(), 8, "{passed:?}");
1145
        // Nothing but the (empty) directories and no stray temporaries.
11461×
        let stray: Vec<_> = walkdir(dir.path())
11471×
            .into_iter()
11481×
            .filter(|p| p.is_file())
11491×
            .collect();
11501×
        assert!(stray.is_empty(), "{stray:?}");
11511×
        assert_eq!(
11521×
            fs::read_dir(dir.path().join("bucket")).unwrap().count(),
1153
            0,
1154
            "no empty directories left"
1155
        );
11561×
    }
1157
11582×
    fn walkdir(dir: &Path) -> Vec<PathBuf> {
11592×
        let mut out = vec![];
11602×
        if let Ok(rd) = fs::read_dir(dir) {
11612×
            for e in rd.flatten() {
11621×
                let p = e.path();
11631×
                if p.is_dir() {
11641×
                    out.extend(walkdir(&p));
11651×
                } else {
11660×
                    out.push(p);
11670×
                }
1168
            }
11690×
        }
11702×
        out
11712×
    }
1172
1173
    #[test]
11741×
    fn a_directory_store_refuses_keys_that_leave_its_root() {
11751×
        let dir = tempfile::tempdir().unwrap();
11761×
        let store = LocalStore::open(dir.path().join("bucket")).unwrap();
11771×
        assert!(matches!(
11781×
            store.put_new("../escape", b"x"),
1179
            Err(Error::Key(..))
1180
        ));
11811×
        assert!(matches!(store.get("/etc/passwd"), Err(Error::Key(..))));
11821×
        assert!(!dir.path().join("escape").exists());
11831×
    }
1184
1185
    /// Many threads race to move a reference from the same version: exactly
1186
    /// one wins each round, on both stores.
1187
    #[test]
11881×
    fn compare_and_swap_admits_one_writer_per_round() {
11891×
        let dir = tempfile::tempdir().unwrap();
11901×
        let local: Arc<dyn Store> = Arc::new(LocalStore::open(dir.path()).unwrap());
11911×
        let memory: Arc<dyn Store> = Arc::new(MemoryStore::new());
11922×
        for store in [local, memory] {
11932×
            let v0 = store.write_ref("refs", b"0", None).unwrap();
11942×
            let wins: Vec<bool> = (0..8)
119516×
                .map(|i| {
119616×
                    let store = store.clone();
119716×
                    let v0 = v0.clone();
119816×
                    std::thread::spawn(move || {
119916×
                        store
120016×
                            .write_ref("refs", format!("{}", i + 1).as_bytes(), Some(&v0))
120116×
                            .is_ok()
120216×
                    })
120316×
                })
12042×
                .collect::<Vec<_>>()
12052×
                .into_iter()
120616×
                .map(|h| h.join().unwrap())
12072×
                .collect();
12082×
            assert_eq!(
12092×
                wins.iter().filter(|w| **w).count(),
1210
                1,
1211
                "{}: {wins:?}",
12120×
                store.describe()
1213
            );
12142×
            let (bytes, _) = store.read_ref("refs").unwrap().unwrap();
12152×
            assert_ne!(bytes, b"0");
1216
        }
12171×
    }
1218
1219
    #[test]
12201×
    fn a_stale_lock_is_broken_and_a_live_one_waited_for() {
12211×
        let dir = tempfile::tempdir().unwrap();
12221×
        let store = LocalStore::open(dir.path()).unwrap();
12231×
        let lock = dir.path().join(".refs.lock");
12241×
        fs::write(&lock, "dead").unwrap();
12251×
        let old = std::time::SystemTime::now() - LOCK_STALE - Duration::from_secs(5);
12261×
        fs::File::open(&lock).unwrap().set_modified(old).unwrap();
12271×
        store.write_ref("refs", b"after a crash", None).unwrap();
12281×
        assert!(!lock.exists());
12291×
    }
1230
1231
    #[test]
12321×
    fn temporaries_and_locks_are_not_listed() {
12331×
        let dir = tempfile::tempdir().unwrap();
12341×
        let store = LocalStore::open(dir.path()).unwrap();
12351×
        store.put_new("packs/a", b"a").unwrap();
12361×
        fs::write(dir.path().join("packs/.a.tmp-ffffff"), "junk").unwrap();
12371×
        fs::write(dir.path().join(".refs.lock"), "1").unwrap();
12381×
        let keys: Vec<String> = store.list("").unwrap().into_iter().map(|e| e.key).collect();
12391×
        assert_eq!(keys, ["packs/a"]);
12401×
    }
1241
}