OXYGENHUB
# 30d5fac7560d oxygen / crates / oxy-core / src / repo / strata.rs
31.9 KiB · Rust · last changed Strata: a repository kept on a store, as a remote 1 month ago source blame history raw
9f9e72b9 Andy McCall ↶ 1
//! A repository kept on a [`Store`] (Oxygen Strata, docs/stores.md): what
2
//! `oxy push dir://…` or `oxy clone s3://…` talk to.
3
//!
4
//! The store holds three kinds of key:
5
//!
6
//! - `oxygen.toml`: the layout's format and the repository's id, written
7
//!   once by the first push;
8
//! - `bundles/<seq>-<hash>.bundle`: each push's changes, signatures,
9
//!   seals, objects and policies, as the wire protocol encodes a bundle,
10
//!   written once and named by content;
11
//! - `refs`: every bookmark with its heads and attestation, every tag, the
12
//!   policy and its history, encoded as the protocol's `refs` response,
13
//!   moved by compare-and-swap.
14
//!
15
//! A store cannot run code, so the client does what a hub does on a push,
16
//! against a **mirror**: a bare repository of its own, kept beside the
17
//! remote's other state under `.oxy/remotes/<name>/strata/`, brought level
18
//! with the store before every use (the record read, the bundles it has
19
//! not seen imported, the view set from the record), and then used as a
20
//! [`LocalRemote`] would be. A push runs the hub's own checks
21
//! (`receive_push`) on the mirror, uploads the bundle, and moves `refs`
22
//! from the version it read; if the record moved meanwhile, the mirror is
23
//! put back and the push reports it, exactly as a hub reports a stale
24
//! push. Nothing in the store is trusted: every object is re-hashed as it
25
//! is imported, and the fetch that follows verifies each bookmark's
26
//! attestation under the policy, as it does for any remote.
27
28
use super::{fail, Repo, RepoError, Result};
29
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
30
use crate::objects::StateObject;
31
use crate::protocol::{
32
    wire, Bundle, RefResult, RefUpdate, RefsResponse, Remote, RemoteOptions, SignedPolicy, TagRef,
33
    UNBOUND_ID,
34
};
35
use crate::strata::{Error as StoreError, Store, Version};
36
use std::cell::RefCell;
37
use std::collections::BTreeSet;
38
use std::fs;
39
use std::path::{Path, PathBuf};
40
41
const MARKER_KEY: &str = "oxygen.toml";
42
const REFS_KEY: &str = "refs";
43
const BUNDLES_PREFIX: &str = "bundles/";
44
const FORMAT: u32 = 1;
45
46
impl From<StoreError> for RepoError {
47
    fn from(e: StoreError) -> RepoError {
48
        RepoError::Msg(e.to_string())
49
    }
50
}
51
52
/// The `oxygen.toml` marker: two lines, written once.
53
fn marker(id: [u8; 16]) -> Vec<u8> {
54
    format!("format = {FORMAT}\nrepo = \"{}\"\n", hex::encode(id)).into_bytes()
55
}
56
57
fn parse_marker(bytes: &[u8], what: &str) -> Result<[u8; 16]> {
58
    let text = String::from_utf8_lossy(bytes);
59
    let mut format = None;
60
    let mut repo = None;
61
    for line in text.lines() {
62
        let Some((k, v)) = line.split_once('=') else {
63
            continue;
64
        };
65
        let v = v.trim().trim_matches('"');
66
        match k.trim() {
67
            "format" => format = v.parse::<u32>().ok(),
68
            "repo" => {
69
                repo = hex::decode(v)
70
                    .ok()
71
                    .and_then(|b| <[u8; 16]>::try_from(b).ok())
72
            }
73
            _ => {}
74
        }
75
    }
76
    match (format, repo) {
77
        (Some(f), _) if f > FORMAT => fail(format!(
78
            "{what} holds a repository in Strata format {f}, newer than this oxy understands (format {FORMAT}): upgrade oxy"
79
        )),
80
        (Some(_), Some(id)) => Ok(id),
81
        _ => fail(format!("{what}: its oxygen.toml is not a Strata marker")),
82
    }
83
}
84
85
/// A remote kept on a store.
86
pub struct BucketRemote {
87
    store: Box<dyn Store>,
88
    /// Where the mirror lives: the remote's directory under `.oxy/remotes`,
89
    /// or a temporary directory when there is no repository yet (a clone),
90
    /// removed on drop.
91
    dir: PathBuf,
92
    temporary: bool,
93
    claimed_id: Option<[u8; 16]>,
94
    mirror: RefCell<Option<Repo>>,
95
}
96
97
impl Drop for BucketRemote {
98
    fn drop(&mut self) {
99
        if self.temporary {
100
            let _ = fs::remove_dir_all(&self.dir);
101
        }
102
    }
103
}
104
105
/// What a sync found: the record's version (to move it from) and the
106
/// repository id the store holds, if any.
107
struct Synced {
108
    version: Option<Version>,
109
    id: Option<[u8; 16]>,
110
}
111
112
impl BucketRemote {
113
    pub fn open(store: Box<dyn Store>, options: &RemoteOptions) -> Result<BucketRemote> {
114
        let (dir, temporary) = match &options.cache_dir {
115
            Some(d) => (d.clone(), false),
116
            None => {
117
                let mut nonce = [0u8; 6];
118
                rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut nonce);
119
                (
120
                    std::env::temp_dir().join(format!(
121
                        "oxy-strata-{}-{}",
122
                        std::process::id(),
123
                        hex::encode(nonce)
124
                    )),
125
                    true,
126
                )
127
            }
128
        };
129
        Ok(BucketRemote {
130
            store,
131
            dir,
132
            temporary,
133
            claimed_id: options.repo_id,
134
            mirror: RefCell::new(None),
135
        })
136
    }
137
138
    fn what(&self) -> String {
139
        self.store.describe()
140
    }
141
142
    /// The repository id the store holds: `None` until a first push.
143
    fn stored_id(&self) -> Result<Option<[u8; 16]>> {
144
        match self.store.get(MARKER_KEY) {
145
            Ok(b) => Ok(Some(parse_marker(&b, &self.what())?)),
146
            Err(StoreError::NotFound(_)) => Ok(None),
147
            Err(e) => Err(e.into()),
148
        }
149
    }
150
151
    fn mirror_dir(&self) -> PathBuf {
152
        self.dir.join("mirror")
153
    }
154
155
    fn seen_path(&self) -> PathBuf {
156
        self.dir.join("seen")
157
    }
158
159
    /// The bundles already imported into the mirror.
160
    fn seen(&self) -> BTreeSet<String> {
161
        fs::read_to_string(self.seen_path())
162
            .map(|s| s.lines().map(str::to_string).collect())
163
            .unwrap_or_default()
164
    }
165
166
    fn mark_seen(&self, key: &str) -> Result<()> {
167
        let mut seen = self.seen();
168
        if seen.insert(key.to_string()) {
169
            let text: String = seen.iter().map(|k| format!("{k}\n")).collect();
170
            crate::durable::write_atomic(&self.seen_path(), text.as_bytes())?;
171
        }
172
        Ok(())
173
    }
174
175
    /// Open the mirror for repository `id`, creating it on first use.
176
    fn open_mirror(&self, id: [u8; 16]) -> Result<()> {
177
        if self.mirror.borrow().is_some() {
178
            return Ok(());
179
        }
180
        let dir = self.mirror_dir();
181
        let repo = if dir.join(super::OXY_DIR).is_dir() {
182
            let repo = Repo::open(&dir)?;
183
            if repo.id()? != id {
184
                // The store was emptied and reused for another repository:
185
                // start the mirror over.
186
                drop(repo);
187
                fs::remove_dir_all(&self.dir)?;
188
                fs::create_dir_all(&self.dir)?;
189
                Repo::init_bare_with_id(&dir, id)?
190
            } else {
191
                repo
192
            }
193
        } else {
194
            fs::create_dir_all(&self.dir)?;
195
            Repo::init_bare_with_id(&dir, id)?
196
        };
197
        *self.mirror.borrow_mut() = Some(repo);
198
        Ok(())
199
    }
200
201
    fn with_mirror<T>(&self, f: impl FnOnce(&Repo) -> Result<T>) -> Result<T> {
202
        let m = self.mirror.borrow();
203
        match m.as_ref() {
204
            Some(repo) => f(repo),
205
            None => fail(format!(
206
                "{} holds no repository yet: push one to it first",
207
                self.what()
208
            )),
209
        }
210
    }
211
212
    /// Bring the mirror level with the store.
213
    fn sync(&self) -> Result<Synced> {
214
        let Some(id) = self.stored_id()? else {
215
            return Ok(Synced {
216
                version: None,
217
                id: None,
218
            });
219
        };
220
        self.open_mirror(id)?;
221
        let record = self.store.read_ref(REFS_KEY)?;
222
        let (resp, version) = match record {
223
            Some((bytes, version)) => {
224
                let (rid, resp) = wire::decode_refs(&bytes)?;
225
                if rid != id {
226
                    return fail(format!(
227
                        "{}: its refs record names a different repository than its oxygen.toml; the store is damaged",
228
                        self.what()
229
                    ));
230
                }
231
                (resp, Some(version))
232
            }
233
            None => (RefsResponse::default(), None),
234
        };
235
        self.with_mirror(|mirror| {
236
            // The policy first: bundles are then imported under it.
237
            if let Some(sp) = &resp.policy {
238
                let have = mirror.current_policy()?.map(|(_, p)| p.policy.serial);
239
                if have.is_none_or(|s| s < sp.policy.serial) {
240
                    mirror
241
                        .adopt_policy_chain(&resp.history, sp, "strata sync")
242
                        .map_err(|e| RepoError::Msg(format!("policy from {}: {e}", self.what())))?;
243
                }
244
            }
245
            self.import_new_bundles(mirror)?;
246
            self.set_view(mirror, &resp)?;
247
            Ok(())
248
        })?;
249
        Ok(Synced {
250
            version,
251
            id: Some(id),
252
        })
253
    }
254
255
    /// Import every bundle in the store the mirror has not seen. Bundles
256
    /// are named in the order they were pushed, but two pushes from the
257
    /// same base may interleave, so a bundle whose dependencies are not
258
    /// there yet is tried again after the others.
259
    fn import_new_bundles(&self, mirror: &Repo) -> Result<()> {
260
        let seen = self.seen();
261
        let mut pending: Vec<String> = self
262
            .store
263
            .list(BUNDLES_PREFIX)?
264
            .into_iter()
265
            .map(|e| e.key)
266
            .filter(|k| !seen.contains(k))
267
            .collect();
268
        let strict = mirror.current_policy()?.is_some();
269
        while !pending.is_empty() {
270
            let mut deferred = Vec::new();
271
            let mut last_error = None;
272
            for key in pending.drain(..) {
273
                let bytes = self.store.get(&key)?;
274
                let bundle = wire::decode_bundle(&bytes)
275
                    .map_err(|e| RepoError::Msg(format!("{}: {key}: {e}", self.what())))?;
276
                match mirror.import_bundle(&bundle, strict) {
277
                    Ok(_) => self.mark_seen(&key)?,
278
                    Err(RepoError::Msg(m)) if m.contains("neither stored nor in the bundle") => {
279
                        last_error = Some(m);
280
                        deferred.push(key);
281
                    }
282
                    Err(e) => return Err(RepoError::Msg(format!("{}: {key}: {e}", self.what()))),
283
                }
284
            }
285
            if deferred.len() == pending.len() + deferred.len() && !deferred.is_empty() {
286
                // No progress: something those bundles need was never
287
                // pushed, or a push is still in progress.
288
                return fail(format!(
289
                    "{}: {} cannot be imported ({}); a push may be in progress, so try again, else the store is missing changes",
290
                    self.what(),
291
                    deferred.join(", "),
292
                    last_error.unwrap_or_default()
293
                ));
294
            }
295
            pending = deferred;
296
        }
297
        Ok(())
298
    }
299
300
    /// Make the mirror's view say what the record says.
301
    fn set_view(&self, mirror: &Repo, resp: &RefsResponse) -> Result<()> {
302
        let (op_ref, op) = mirror.head_op()?;
303
        let mut view = op.view.clone();
304
        view.bookmarks.clear();
305
        for r in &resp.refs {
306
            for h in &r.heads {
307
                if !mirror.store.has(h) {
308
                    return fail(format!(
309
                        "{}: bookmark {:?} names change {}, which no bundle there holds; a push may be in progress, so try again",
310
                        self.what(),
311
                        r.name,
312
                        h.short(12)
313
                    ));
314
                }
315
            }
316
            let state = mirror.store.put_state(&StateObject::new(r.heads.clone()))?;
317
            if state != r.state {
318
                return fail(format!(
319
                    "{}: bookmark {:?} names a state that does not match its heads; the store is damaged",
320
                    self.what(),
321
                    r.name
322
                ));
323
            }
324
            if let Some(sa) = &r.attestation {
325
                let current = mirror.latest_ref_attest(&r.name)?.map(|(_, a)| a);
326
                if current.as_ref() != Some(sa) {
327
                    let a = mirror.store.put_attest(&sa.attest)?;
328
                    for sig in &sa.sigs {
329
                        mirror.store.put_signature(&a, sig)?;
330
                    }
331
                    mirror.set_ref_attest(&r.name, &a)?;
332
                }
333
            }
334
            view.bookmarks.insert(r.name.clone(), state);
335
        }
336
        let policy = mirror.current_policy()?.map(|(_, sp)| sp.policy);
337
        for t in &resp.tags {
338
            mirror.store.put_state(&StateObject::new(t.heads.clone()))?;
339
            mirror
340
                .adopt_tag(&mut view, t, policy.as_ref())
341
                .map_err(|e| RepoError::Msg(format!("{}: {e}", self.what())))?;
342
        }
343
        if view != op.view {
344
            mirror.record_op(&[op_ref], "strata sync", view)?;
345
        }
346
        Ok(())
347
    }
348
349
    /// The record for the mirror's current references.
350
    fn record(&self, id: [u8; 16]) -> Result<Vec<u8>> {
351
        self.with_mirror(|mirror| {
352
            let mut resp = mirror.remote_refs()?;
353
            // A mirror has no checkout; the store's clone picks a bookmark
354
            // as any other remote's does.
355
            resp.default_bookmark = None;
356
            Ok(wire::encode_refs(id, &resp))
357
        })
358
    }
359
360
    /// Store a bundle under the next sequence number; returns its key.
361
    fn upload(&self, bundle: &Bundle) -> Result<String> {
362
        let bytes = wire::encode_bundle(bundle);
363
        let hash = blake3::hash(&bytes).to_hex();
364
        let seq = self.store.list(BUNDLES_PREFIX)?.len() + 1;
365
        let key = format!("{BUNDLES_PREFIX}{seq:08}-{}.bundle", &hash[..16]);
366
        match self.store.put_new(&key, &bytes) {
367
            Ok(()) | Err(StoreError::Exists(_)) => {}
368
            Err(e) => return Err(e.into()),
369
        }
370
        self.mark_seen(&key)?;
371
        Ok(key)
372
    }
373
}
374
375
impl Remote for BucketRemote {
376
    fn repo_id(&self) -> Result<[u8; 16]> {
377
        Ok(self.stored_id()?.unwrap_or(UNBOUND_ID))
378
    }
379
380
    fn refs(&self) -> Result<RefsResponse> {
381
        let synced = self.sync()?;
382
        if synced.id.is_none() {
383
            return Ok(RefsResponse::default());
384
        }
385
        self.with_mirror(|mirror| {
386
            let mut resp = mirror.remote_refs()?;
387
            resp.default_bookmark = None;
388
            Ok(resp)
389
        })
390
    }
391
392
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
393
        self.with_mirror(|m| m.negotiate(have, want))
394
    }
395
396
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
397
        self.with_mirror(|m| m.bundle_raw_for(changes, max_blob))
398
    }
399
400
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
401
        self.with_mirror(|m| m.objects_for(refs))
402
    }
403
404
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
405
        self.with_mirror(|m| m.checkpoint_bundle_for(heads))
406
    }
407
408
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
409
        self.with_mirror(|m| m.shallow_changes(heads, depth))
410
    }
411
412
    fn push(
413
        &self,
414
        bundle: Bundle,
415
        updates: Vec<RefUpdate>,
416
        policy: Option<SignedPolicy>,
417
        tags: Vec<TagRef>,
418
    ) -> Result<Vec<RefResult>> {
419
        let mut synced = self.sync()?;
420
        let id = match synced.id {
421
            Some(id) => id,
422
            None => {
423
                // The first push binds the store to this repository. Two
424
                // first pushes race on the marker; the loser reads the
425
                // winner's and goes on only if it is the same repository.
426
                let Some(id) = self.claimed_id else {
427
                    return fail(format!(
428
                        "{} is empty, and this client did not say which repository it is",
429
                        self.what()
430
                    ));
431
                };
432
                match self.store.put_new(MARKER_KEY, &marker(id)) {
433
                    Ok(()) => {}
434
                    Err(StoreError::Exists(_)) => {}
435
                    Err(e) => return Err(e.into()),
436
                }
437
                synced = self.sync()?;
438
                match synced.id {
439
                    Some(stored) if stored == id => id,
440
                    _ => {
441
                        return fail(format!(
442
                            "{} was just bound to another repository by a concurrent push",
443
                            self.what()
444
                        ))
445
                    }
446
                }
447
            }
448
        };
449
        if let Some(claimed) = self.claimed_id {
450
            if claimed != id {
451
                return fail(format!(
452
                    "{} holds a different repository (id {}) than this one ({}): a store holds one repository; use another prefix or bucket",
453
                    self.what(),
454
                    hex::encode(id),
455
                    hex::encode(claimed)
456
                ));
457
            }
458
        }
459
        let (before_ref, before) = self.with_mirror(|m| m.head_op())?;
460
        let results =
461
            self.with_mirror(|m| m.receive_push(bundle.clone(), updates, policy, tags))?;
462
        if !results.iter().any(|r| r.ok) {
463
            return Ok(results);
464
        }
465
        if !bundle.is_empty() {
466
            self.upload(&bundle)?;
467
        }
468
        let record = self.record(id)?;
469
        match self
470
            .store
471
            .write_ref(REFS_KEY, &record, synced.version.as_ref())
472
        {
473
            Ok(_) => Ok(results),
474
            Err(StoreError::Conflict(_)) => {
475
                // Someone else moved the record meanwhile. The bundle is in
476
                // the store and harmless; the mirror goes back to what the
477
                // store says, and the push is reported as a hub would.
478
                self.with_mirror(|m| {
479
                    let (now, _) = m.head_op()?;
480
                    if now != before_ref {
481
                        m.record_op(
482
                            &[now],
483
                            "strata push undone: the remote moved",
484
                            before.view.clone(),
485
                        )?;
486
                    }
487
                    Ok(())
488
                })?;
489
                let _ = self.sync();
490
                Ok(results
491
                    .into_iter()
492
                    .map(|r| RefResult {
493
                        name: r.name,
494
                        ok: false,
495
                        reason: "the remote moved while pushing (another push landed first); pull, then push again".into(),
496
                    })
497
                    .collect())
498
            }
499
            Err(e) => Err(e.into()),
500
        }
501
    }
502
}
503
504
/// Open a remote on `store` for the local repository described by `options`.
505
pub fn open_store_remote(
506
    store: Box<dyn Store>,
507
    options: &RemoteOptions,
508
) -> Result<Box<dyn Remote>> {
509
    Ok(Box::new(BucketRemote::open(store, options)?))
510
}
511
512
/// Whether `path` holds a Strata store rather than a repository.
513
pub fn is_store_dir(path: &Path) -> bool {
514
    path.join(MARKER_KEY).is_file() || path.join(REFS_KEY).is_file()
515
}
516
517
#[cfg(test)]
518
mod tests {
519
    use super::*;
520
    use crate::repo::IdentityScope;
521
    use crate::strata::{Entry, LocalStore, MemoryStore};
522
    use std::ops::Range;
523
    use std::sync::Mutex;
524
525
    fn hermetic() {
526
        static ONCE: std::sync::Once = std::sync::Once::new();
527
        static DIR: std::sync::OnceLock<tempfile::TempDir> = std::sync::OnceLock::new();
528
        ONCE.call_once(|| {
529
            let d = tempfile::tempdir().unwrap();
530
            std::env::set_var("OXY_HOME", d.path());
531
            DIR.set(d).ok();
532
        });
533
    }
534
535
    fn write(root: &Path, rel: &str, content: &[u8]) {
536
        let p = root.join(rel);
537
        fs::create_dir_all(p.parent().unwrap()).unwrap();
538
        fs::write(p, content).unwrap();
539
    }
540
541
    fn add_remote(repo: &mut Repo, name: &str, url: &str) {
542
        repo.config.set(&format!("remote.{name}.url"), url).unwrap();
543
        repo.save_config().unwrap();
544
    }
545
546
    fn keys(store: &dyn Store, prefix: &str) -> Vec<String> {
547
        store
548
            .list(prefix)
549
            .unwrap()
550
            .into_iter()
551
            .map(|e| e.key)
552
            .collect()
553
    }
554
555
    #[test]
556
    fn push_clone_and_pull_through_a_directory_store() {
557
        hermetic();
558
        let dir = tempfile::tempdir().unwrap();
559
        let root = dir.path();
560
        let url = format!("dir://{}", root.join("bucket").display());
561
        let store = LocalStore::open(root.join("bucket")).unwrap();
562
563
        // A signs, commits, tags and pushes: the store gets the marker, one
564
        // bundle, and a record.
565
        let mut a = Repo::init(&root.join("a"))
566
            .unwrap()
567
            .with_home(root.join("home-a"));
568
        a.create_identity("Ada", "[email protected]", IdentityScope::Repo)
569
            .unwrap();
570
        add_remote(&mut a, "bucket", &url);
571
        write(&root.join("a"), "f.txt", b"a\n");
572
        let h1 = a.commit("first", &[]).unwrap();
573
        a.tag_create("v1", &[h1], "one").unwrap();
574
        let results = a.push("bucket", false).unwrap();
575
        assert!(results.iter().all(|r| r.ok), "{results:?}");
576
        assert_eq!(
577
            keys(&store, ""),
578
            [
579
                "bundles/00000001-".to_string() + &keys(&store, "bundles/")[0][17..],
580
                "oxygen.toml".into(),
581
                "refs".into()
582
            ]
583
        );
584
        assert_eq!(
585
            parse_marker(&store.get("oxygen.toml").unwrap(), "t").unwrap(),
586
            a.id().unwrap()
587
        );
588
        let again = a.push("bucket", false).unwrap();
589
        assert!(
590
            again[0].ok && again[0].reason.contains("up to date"),
591
            "{again:?}"
592
        );
593
        assert_eq!(
594
            keys(&store, "bundles/").len(),
595
            1,
596
            "a no-op push uploads nothing"
597
        );
598
599
        // B clones from the store: same id, same file, the tag, and the
600
        // seal on the change.
601
        let b = Repo::clone(&url, &root.join("b")).unwrap();
602
        assert_eq!(b.id().unwrap(), a.id().unwrap());
603
        assert_eq!(fs::read(root.join("b/f.txt")).unwrap(), b"a\n");
604
        assert_eq!(b.bookmark_heads().unwrap(), vec![h1]);
605
        assert_eq!(
606
            b.tags()
607
                .unwrap()
608
                .iter()
609
                .map(|t| t.name.clone())
610
                .collect::<Vec<_>>(),
611
            ["v1"]
612
        );
613
        assert!(matches!(
614
            b.authenticate_change(&h1).unwrap(),
615
            crate::repo::SignatureStatus::Valid { sealed: true, .. }
616
        ));
617
        assert!(
618
            root.join("b/.oxy/remotes/origin/strata/mirror/.oxy")
619
                .is_dir(),
620
            "the mirror lives with the remote's state"
621
        );
622
        assert!(
623
            !std::env::temp_dir()
624
                .read_dir()
625
                .unwrap()
626
                .flatten()
627
                .any(|e| e.file_name().to_string_lossy().starts_with("oxy-strata-")),
628
            "a clone's temporary mirror is removed"
629
        );
630
631
        // B commits and pushes; A pulls it. A then commits and pushes.
632
        write(&root.join("b"), "f.txt", b"a\nb\n");
633
        let h2 = b.commit("second", &[]).unwrap();
634
        assert!(b.push("origin", false).unwrap()[0].ok);
635
        assert_eq!(keys(&store, "bundles/").len(), 2);
636
        let pr = a.pull("bucket").unwrap();
637
        assert!(pr.changed && pr.checked_out);
638
        assert_eq!(fs::read(root.join("a/f.txt")).unwrap(), b"a\nb\n");
639
        write(&root.join("a"), "g.txt", b"g\n");
640
        let h3 = a.commit("third", &[]).unwrap();
641
        assert!(a.push("bucket", false).unwrap()[0].ok);
642
        let pr = b.pull("origin").unwrap();
643
        assert!(pr.changed);
644
        // g.txt is new, so its change commutes with B's: two heads.
645
        let mut want = vec![h2, h3];
646
        want.sort();
647
        assert_eq!(b.bookmark_heads().unwrap(), want);
648
        assert_eq!(fs::read(root.join("b/g.txt")).unwrap(), b"g\n");
649
650
        // A third clone gets everything from the three bundles.
651
        let c = Repo::clone(&url, &root.join("c")).unwrap();
652
        assert_eq!(c.bookmark_heads().unwrap(), want);
653
        assert_eq!(fs::read(root.join("c/f.txt")).unwrap(), b"a\nb\n");
654
    }
655
656
    #[test]
657
    fn a_push_behind_the_remote_is_refused_before_anything_is_uploaded() {
658
        hermetic();
659
        let dir = tempfile::tempdir().unwrap();
660
        let root = dir.path();
661
        let url = format!("dir://{}", root.join("bucket").display());
662
        let store = LocalStore::open(root.join("bucket")).unwrap();
663
        let mut a = Repo::init(&root.join("a")).unwrap();
664
        add_remote(&mut a, "bucket", &url);
665
        write(&root.join("a"), "f.txt", b"a\n");
666
        a.commit("first", &[]).unwrap();
667
        assert!(a.push("bucket", false).unwrap()[0].ok);
668
        let b = Repo::clone(&url, &root.join("b")).unwrap();
669
        write(&root.join("b"), "f.txt", b"b\n");
670
        b.commit("from b", &[]).unwrap();
671
        assert!(b.push("origin", false).unwrap()[0].ok);
672
        write(&root.join("a"), "g.txt", b"g\n");
673
        a.commit("from a", &[]).unwrap();
674
        let e = a.push("bucket", false).unwrap_err().to_string();
675
        assert!(e.contains("pull first"), "{e}");
676
        assert_eq!(keys(&store, "bundles/").len(), 2);
677
        assert!(a.pull("bucket").unwrap().changed);
678
        assert!(a.push("bucket", false).unwrap()[0].ok);
679
        assert_eq!(keys(&store, "bundles/").len(), 3);
680
    }
681
682
    /// A store whose record moves under a writer the first time it is
683
    /// written to: what a concurrent push looks like between the read and
684
    /// the compare-and-swap.
685
    struct Racing {
686
        inner: MemoryStore,
687
        raced: Mutex<bool>,
688
    }
689
690
    impl Store for Racing {
691
        fn describe(&self) -> String {
692
            "racing".into()
693
        }
694
        fn has(&self, key: &str) -> crate::strata::Result<bool> {
695
            self.inner.has(key)
696
        }
697
        fn get(&self, key: &str) -> crate::strata::Result<Vec<u8>> {
698
            self.inner.get(key)
699
        }
700
        fn get_range(&self, key: &str, range: Range<u64>) -> crate::strata::Result<Vec<u8>> {
701
            self.inner.get_range(key, range)
702
        }
703
        fn put_new(&self, key: &str, bytes: &[u8]) -> crate::strata::Result<()> {
704
            self.inner.put_new(key, bytes)
705
        }
706
        fn list(&self, prefix: &str) -> crate::strata::Result<Vec<Entry>> {
707
            self.inner.list(prefix)
708
        }
709
        fn delete(&self, key: &str) -> crate::strata::Result<()> {
710
            self.inner.delete(key)
711
        }
712
        fn read_ref(&self, key: &str) -> crate::strata::Result<Option<(Vec<u8>, Version)>> {
713
            self.inner.read_ref(key)
714
        }
715
        fn write_ref(
716
            &self,
717
            key: &str,
718
            bytes: &[u8],
719
            expected: Option<&Version>,
720
        ) -> crate::strata::Result<Version> {
721
            let mut raced = self.raced.lock().unwrap();
722
            if !*raced && expected.is_some() {
723
                *raced = true;
724
                // Someone else lands a record: the same references with a
725
                // different detail, so the version moves.
726
                let (current, v) = self.inner.read_ref(key)?.unwrap();
727
                let (id, mut resp) =
728
                    wire::decode_refs(&current).map_err(|e| StoreError::Backend(e.to_string()))?;
729
                resp.default_bookmark = Some("raced".into());
730
                self.inner
731
                    .write_ref(key, &wire::encode_refs(id, &resp), Some(&v))?;
732
            }
733
            self.inner.write_ref(key, bytes, expected)
734
        }
735
    }
736
737
    #[test]
738
    fn a_record_moved_during_a_push_fails_the_push_and_keeps_the_mirror_honest() {
739
        hermetic();
740
        let dir = tempfile::tempdir().unwrap();
741
        let root = dir.path();
742
        let a = Repo::init(&root.join("a")).unwrap();
743
        write(&root.join("a"), "f.txt", b"a\n");
744
        let h1 = a.commit("first", &[]).unwrap();
745
        let store = Box::new(Racing {
746
            inner: MemoryStore::new(),
747
            raced: Mutex::new(false),
748
        });
749
        let options = RemoteOptions {
750
            repo_id: Some(a.id().unwrap()),
751
            cache_dir: Some(root.join("cache")),
752
            ..Default::default()
753
        };
754
        let remote = BucketRemote::open(store, &options).unwrap();
755
        // The first push creates the record (expected None: no race yet).
756
        let state = a.store.put_state(&StateObject::new(vec![h1])).unwrap();
757
        let empty = a.store.put_state(&StateObject::new(vec![])).unwrap();
758
        let mut bundle = a.bundle_for(&[h1]).unwrap();
759
        bundle
760
            .objects
761
            .push((ObjectType::State, StateObject::new(vec![h1]).encode()));
762
        let update = |old, new| RefUpdate {
763
            name: "main".into(),
764
            old_state: old,
765
            new_state: new,
766
            force: false,
767
            attestation: None,
768
        };
769
        let r = remote
770
            .push(bundle, vec![update(empty, state)], None, vec![])
771
            .unwrap();
772
        assert!(r[0].ok, "{r:?}");
773
        // The second push meets the race: refused, nothing moved.
774
        write(&root.join("a"), "f.txt", b"ab\n");
775
        let h2 = a.commit("second", &[]).unwrap();
776
        let state2 = a.store.put_state(&StateObject::new(vec![h2])).unwrap();
777
        let mut bundle = a.bundle_for(&[h2]).unwrap();
778
        bundle
779
            .objects
780
            .push((ObjectType::State, StateObject::new(vec![h2]).encode()));
781
        let r = remote
782
            .push(bundle, vec![update(state, state2)], None, vec![])
783
            .unwrap();
784
        assert!(
785
            !r[0].ok && r[0].reason.contains("moved while pushing"),
786
            "{r:?}"
787
        );
788
        let refs = remote.refs().unwrap();
789
        assert_eq!(
790
            refs.refs[0].heads,
791
            vec![h1],
792
            "the mirror says what the store says"
793
        );
794
    }
795
796
    #[test]
797
    fn a_store_holds_one_repository() {
798
        hermetic();
799
        let dir = tempfile::tempdir().unwrap();
800
        let root = dir.path();
801
        let url = format!("dir://{}", root.join("bucket").display());
802
        let mut a = Repo::init(&root.join("a")).unwrap();
803
        add_remote(&mut a, "bucket", &url);
804
        write(&root.join("a"), "f.txt", b"a\n");
805
        a.commit("first", &[]).unwrap();
806
        assert!(a.push("bucket", false).unwrap()[0].ok);
807
        let mut other = Repo::init(&root.join("other")).unwrap();
808
        add_remote(&mut other, "bucket", &url);
809
        write(&root.join("other"), "f.txt", b"o\n");
810
        other.commit("mine", &[]).unwrap();
811
        let e = other.push("bucket", false).unwrap_err().to_string();
812
        assert!(
813
            e.contains("different repository") || e.contains("another repository"),
814
            "{e}"
815
        );
816
        assert_eq!(
817
            keys(&LocalStore::open(root.join("bucket")).unwrap(), "bundles/").len(),
818
            1
819
        );
820
    }
821
822
    #[test]
823
    fn a_damaged_bundle_is_caught_and_an_empty_store_cannot_be_cloned() {
824
        hermetic();
825
        let dir = tempfile::tempdir().unwrap();
826
        let root = dir.path();
827
        let url = format!("dir://{}", root.join("bucket").display());
828
        let e = Repo::clone(&url, &root.join("nothing"))
829
            .err()
830
            .expect("refused")
831
            .to_string();
832
        assert!(e.contains("empty and unbound"), "{e}");
833
        let mut a = Repo::init(&root.join("a")).unwrap();
834
        add_remote(&mut a, "bucket", &url);
835
        write(&root.join("a"), "f.txt", b"a\n");
836
        a.commit("first", &[]).unwrap();
837
        assert!(a.push("bucket", false).unwrap()[0].ok);
838
        let store = LocalStore::open(root.join("bucket")).unwrap();
839
        let key = keys(&store, "bundles/").remove(0);
840
        let mut bytes = store.get(&key).unwrap();
841
        let mid = bytes.len() / 2;
842
        bytes[mid] ^= 0xff;
843
        store.delete(&key).unwrap();
844
        store.put_new(&key, &bytes).unwrap();
845
        let e = Repo::clone(&url, &root.join("b"))
846
            .err()
847
            .expect("refused")
848
            .to_string();
849
        assert!(
850
            e.contains(&key) || e.contains("bundle") || e.contains("hash"),
851
            "{e}"
852
        );
853
        assert!(!root.join("b/f.txt").exists());
854
    }
855
856
    #[test]
857
    fn markers_parse_and_refuse_newer_formats() {
858
        let id = [7u8; 16];
859
        assert_eq!(parse_marker(&marker(id), "s").unwrap(), id);
860
        let e = parse_marker(
861
            b"format = 9\nrepo = \"07070707070707070707070707070707\"\n",
862
            "s",
863
        )
864
        .unwrap_err()
865
        .to_string();
866
        assert!(e.contains("format 9"), "{e}");
867
        assert!(parse_marker(b"hello", "s").is_err());
868
    }
869
}