OXYGENHUB
# 4161262eeb29 oxygen / crates / oxy-core / src / protocol.rs
3.8 KiB · Rust · last changed Remotes report stale attestations as absent but keep the last serial; remotes test expectations 1 month ago source blame history raw
1
//! Transport-agnostic protocol (spec §16): what a client asks of a remote and
2
//! what a server does with a push. `LocalRemote` serves another repository
3
//! directory in-process; an HTTPS transport implements the same trait and the
4
//! server side reuses `Repo::receive_push` and friends unchanged.
5
6
use crate::attest::Attest;
7
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
8
use crate::identity::Signature;
9
use crate::objects::Change;
10
use crate::repo::{Repo, RepoError, Result};
11
use std::path::Path;
12
13
/// An attestation together with the signatures over its ObjectRef.
14
#[derive(Clone, Debug, PartialEq, Eq)]
15
pub struct SignedAttest {
16
    pub attest: Attest,
17
    pub sigs: Vec<Signature>,
18
}
19
20
/// One reference as a remote reports it.
21
#[derive(Clone, Debug)]
22
pub struct RemoteRef {
23
    pub name: String,
24
    pub state: ObjectRef,
25
    pub heads: Vec<ChangeHash>,
26
    /// The latest attestation, only when it describes `state`. An unattested
27
    /// push (accepted while no trust policy exists) leaves it `None`.
28
    pub attestation: Option<SignedAttest>,
29
    /// Serial of the latest attestation ever recorded, stale or not, so the
30
    /// next attestation continues the sequence.
31
    pub last_serial: Option<u64>,
32
}
33
34
/// Everything needed to add a set of changes to a repository.
35
#[derive(Clone, Debug, Default)]
36
pub struct Bundle {
37
    /// In dependency order, each with its individual signatures.
38
    pub changes: Vec<(Change, Vec<Signature>)>,
39
    /// Seals covering the changes.
40
    pub attests: Vec<SignedAttest>,
41
    /// Chunks, blobs, identities, and state objects referenced by the changes.
42
    pub objects: Vec<(ObjectType, Vec<u8>)>,
43
}
44
45
impl Bundle {
46
    pub fn is_empty(&self) -> bool {
47
        self.changes.is_empty() && self.attests.is_empty() && self.objects.is_empty()
48
    }
49
}
50
51
/// A requested bookmark move (spec §10.2, §16.3).
52
#[derive(Clone, Debug)]
53
pub struct RefUpdate {
54
    pub name: String,
55
    pub old_state: ObjectRef,
56
    pub new_state: ObjectRef,
57
    pub force: bool,
58
    pub attestation: Option<SignedAttest>,
59
}
60
61
#[derive(Clone, Debug, PartialEq, Eq)]
62
pub struct RefResult {
63
    pub name: String,
64
    pub ok: bool,
65
    pub reason: String,
66
}
67
68
pub trait Remote {
69
    fn repo_id(&self) -> Result<[u8; 16]>;
70
    fn refs(&self) -> Result<Vec<RemoteRef>>;
71
    /// Changes in the closure of `want` but not of `have`, in dependency order.
72
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>>;
73
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle>;
74
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>>;
75
}
76
77
/// Another repository on this machine, served in-process.
78
pub struct LocalRemote {
79
    pub repo: Repo,
80
}
81
82
impl LocalRemote {
83
    pub fn open(path: &Path) -> Result<LocalRemote> {
84
        Ok(LocalRemote { repo: Repo::open(path)? })
85
    }
86
}
87
88
impl Remote for LocalRemote {
89
    fn repo_id(&self) -> Result<[u8; 16]> {
90
        self.repo.id()
91
    }
92
    fn refs(&self) -> Result<Vec<RemoteRef>> {
93
        self.repo.remote_refs()
94
    }
95
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
96
        self.repo.negotiate(have, want)
97
    }
98
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle> {
99
        self.repo.bundle_for(changes)
100
    }
101
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>> {
102
        self.repo.receive_push(bundle, updates)
103
    }
104
}
105
106
/// Open a remote by URL. Only local paths (optionally `file://`) exist so far.
107
pub fn open_remote(url: &str) -> Result<Box<dyn Remote>> {
108
    let path = url.strip_prefix("file://").unwrap_or(url);
109
    if path.contains("://") {
110
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}: only local paths are implemented")));
111
    }
112
    Ok(Box::new(LocalRemote::open(Path::new(path))?))
113
}