OXYGENHUB
# 21b2aec279ff oxygen / crates / oxy-core / src / protocol.rs
26.1 KiB · Rust · last changed Lazy blobs: clone --max-blob / filter.max-blob, absent-blob registry fetched on first read, objects endpoint (spec 0.6e) 1 month ago source blame history raw
5b9c6ad4 Andy McCall ↶ 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
750251b2 Andy McCall ↶ 3
//! directory in-process, `HttpRemote` speaks to an `oxy-server` over HTTP(S),
4
//! and the server side reuses `Repo::receive_push` and friends unchanged.
5
//!
6
//! Wire messages are deterministic CBOR (`wire` module); over HTTP each is a
7
//! request or response body with content type `application/oxy-cbor`.
5b9c6ad4 Andy McCall ↶ 8
9
use crate::attest::Attest;
750251b2 Andy McCall ↶ 10
use crate::cbor::{self, Value};
5b9c6ad4 Andy McCall ↶ 11
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
12
use crate::identity::Signature;
750251b2 Andy McCall ↶ 13
use crate::objects::{self as o, Change, ObjectError};
83e0677b Andy McCall ↶ 14
use crate::policy::Policy;
5b9c6ad4 Andy McCall ↶ 15
use crate::repo::{Repo, RepoError, Result};
2b5ee371 Andy McCall ↶ 16
use std::path::{Path, PathBuf};
17
use std::sync::Arc;
5b9c6ad4 Andy McCall ↶ 18
750251b2 Andy McCall ↶ 19
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
20
pub const PROTOCOL_VERSION: u64 = 1;
21
5b9c6ad4 Andy McCall ↶ 22
/// An attestation together with the signatures over its ObjectRef.
23
#[derive(Clone, Debug, PartialEq, Eq)]
24
pub struct SignedAttest {
25
    pub attest: Attest,
26
    pub sigs: Vec<Signature>,
27
}
28
83e0677b Andy McCall ↶ 29
/// A policy together with the root signatures over its ObjectRef.
30
#[derive(Clone, Debug, PartialEq, Eq)]
31
pub struct SignedPolicy {
32
    pub policy: Policy,
33
    pub sigs: Vec<Signature>,
34
}
35
36
/// What `refs` returns: every bookmark plus the current policy, if any.
37
#[derive(Clone, Debug, Default)]
38
pub struct RefsResponse {
39
    pub refs: Vec<RemoteRef>,
40
    pub policy: Option<SignedPolicy>,
41
}
42
5b9c6ad4 Andy McCall ↶ 43
/// One reference as a remote reports it.
44
#[derive(Clone, Debug)]
45
pub struct RemoteRef {
46
    pub name: String,
47
    pub state: ObjectRef,
48
    pub heads: Vec<ChangeHash>,
49
    pub attestation: Option<SignedAttest>,
50
}
51
52
/// Everything needed to add a set of changes to a repository.
53
#[derive(Clone, Debug, Default)]
54
pub struct Bundle {
55
    /// In dependency order, each with its individual signatures.
56
    pub changes: Vec<(Change, Vec<Signature>)>,
57
    /// Seals covering the changes.
58
    pub attests: Vec<SignedAttest>,
59
    /// Chunks, blobs, identities, and state objects referenced by the changes.
60
    pub objects: Vec<(ObjectType, Vec<u8>)>,
61
}
62
63
impl Bundle {
64
    pub fn is_empty(&self) -> bool {
65
        self.changes.is_empty() && self.attests.is_empty() && self.objects.is_empty()
66
    }
67
}
68
69
/// A requested bookmark move (spec §10.2, §16.3).
70
#[derive(Clone, Debug)]
71
pub struct RefUpdate {
72
    pub name: String,
73
    pub old_state: ObjectRef,
74
    pub new_state: ObjectRef,
75
    pub force: bool,
76
    pub attestation: Option<SignedAttest>,
77
}
78
79
#[derive(Clone, Debug, PartialEq, Eq)]
80
pub struct RefResult {
81
    pub name: String,
82
    pub ok: bool,
83
    pub reason: String,
84
}
85
86
pub trait Remote {
87
    fn repo_id(&self) -> Result<[u8; 16]>;
83e0677b Andy McCall ↶ 88
    fn refs(&self) -> Result<RefsResponse>;
5b9c6ad4 Andy McCall ↶ 89
    /// Changes in the closure of `want` but not of `have`, in dependency order.
90
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>>;
b3c5a59a Andy McCall ↶ 91
    /// The changes with their signatures, seals, identities, and blobs; blobs
92
    /// larger than `max_blob` bytes are left out for lazy fetching (spec §13.5).
93
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle>;
94
    /// Individual objects by reference (spec §16.1 `objects`); a blob comes
95
    /// with its chunks.
96
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>>;
2863456a Andy McCall ↶ 97
    /// A checkpoint of exactly `heads` (spec §16.1 `checkpoint`): the
98
    /// checkpoint object, its payload blob, and the blob's chunks.
99
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle>;
100
    /// The changes within `depth` dependency steps of `heads` (spec §17).
101
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>>;
83e0677b Andy McCall ↶ 102
    /// Push changes, bookmark updates, and optionally a newer policy. The
103
    /// policy is applied first; a result named `policy` reports it.
104
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>) -> Result<Vec<RefResult>>;
5b9c6ad4 Andy McCall ↶ 105
}
106
750251b2 Andy McCall ↶ 107
// ---------------------------------------------------------------- local
108
5b9c6ad4 Andy McCall ↶ 109
/// Another repository on this machine, served in-process.
110
pub struct LocalRemote {
111
    pub repo: Repo,
112
}
113
114
impl LocalRemote {
115
    pub fn open(path: &Path) -> Result<LocalRemote> {
116
        Ok(LocalRemote { repo: Repo::open(path)? })
117
    }
118
}
119
120
impl Remote for LocalRemote {
121
    fn repo_id(&self) -> Result<[u8; 16]> {
122
        self.repo.id()
123
    }
83e0677b Andy McCall ↶ 124
    fn refs(&self) -> Result<RefsResponse> {
5b9c6ad4 Andy McCall ↶ 125
        self.repo.remote_refs()
126
    }
127
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
128
        self.repo.negotiate(have, want)
129
    }
b3c5a59a Andy McCall ↶ 130
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
131
        self.repo.bundle_for_filtered(changes, max_blob)
132
    }
133
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
134
        self.repo.objects_for(refs)
5b9c6ad4 Andy McCall ↶ 135
    }
2863456a Andy McCall ↶ 136
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
137
        self.repo.checkpoint_bundle_for(heads)
138
    }
139
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
140
        self.repo.shallow_changes(heads, depth)
141
    }
83e0677b Andy McCall ↶ 142
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>) -> Result<Vec<RefResult>> {
143
        self.repo.receive_push(bundle, updates, policy)
5b9c6ad4 Andy McCall ↶ 144
    }
145
}
146
750251b2 Andy McCall ↶ 147
// ---------------------------------------------------------------- http
148
2b5ee371 Andy McCall ↶ 149
/// How to reach a remote: credentials and trust.
150
#[derive(Clone, Debug, Default)]
151
pub struct RemoteOptions {
152
    /// Bearer token for pushes.
153
    pub token: Option<String>,
154
    /// PEM file with extra root certificates to trust for `https://` remotes
155
    /// (self-hosted servers); the system web roots are always trusted.
156
    pub ca: Option<PathBuf>,
157
}
158
750251b2 Andy McCall ↶ 159
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
160
pub struct HttpRemote {
161
    base: String,
162
    token: Option<String>,
163
    agent: ureq::Agent,
164
}
165
2b5ee371 Andy McCall ↶ 166
/// A TLS client configuration trusting the web roots plus `ca`, if given.
167
fn tls_client_config(ca: Option<&Path>) -> Result<Arc<rustls::ClientConfig>> {
168
    let mut roots = rustls::RootCertStore::empty();
169
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
170
    if let Some(path) = ca {
171
        let pem = std::fs::read(path).map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?;
172
        let certs = rustls_pemfile::certs(&mut pem.as_slice())
173
            .collect::<std::result::Result<Vec<_>, _>>()
174
            .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?;
175
        if certs.is_empty() {
176
            return Err(RepoError::Msg(format!("CA file {} holds no certificates", path.display())));
177
        }
178
        for c in certs {
179
            roots.add(c).map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?;
180
        }
181
    }
182
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
183
        .with_safe_default_protocol_versions()
184
        .map_err(|e| RepoError::Msg(e.to_string()))?
185
        .with_root_certificates(roots)
186
        .with_no_client_auth();
187
    Ok(Arc::new(cfg))
188
}
189
750251b2 Andy McCall ↶ 190
impl HttpRemote {
2b5ee371 Andy McCall ↶ 191
    pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> {
192
        let agent = ureq::AgentBuilder::new().tls_config(tls_client_config(options.ca.as_deref())?).build();
193
        Ok(HttpRemote { base: base.trim_end_matches('/').to_string(), token: options.token.clone(), agent })
750251b2 Andy McCall ↶ 194
    }
195
196
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
197
        let url = format!("{}/oxy/v1/{endpoint}", self.base);
198
        let mut req = if post { self.agent.post(&url) } else { self.agent.get(&url) };
199
        req = req.set("Accept", CONTENT_TYPE);
200
        if let Some(t) = &self.token {
201
            req = req.set("Authorization", &format!("Bearer {t}"));
202
        }
203
        let resp = match body {
204
            Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b),
205
            None => req.call(),
206
        };
207
        match resp {
208
            Ok(r) => {
209
                let mut out = Vec::new();
210
                r.into_reader().read_to_end(&mut out).map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
211
                Ok(out)
212
            }
213
            Err(ureq::Error::Status(code, r)) => {
214
                let text = r.into_string().unwrap_or_default();
215
                Err(RepoError::Msg(format!("{url}: HTTP {code}: {}", text.trim())))
216
            }
217
            Err(e) => Err(RepoError::Msg(format!("{url}: {e}"))),
218
        }
219
    }
220
}
221
222
impl Remote for HttpRemote {
223
    fn repo_id(&self) -> Result<[u8; 16]> {
224
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
225
    }
83e0677b Andy McCall ↶ 226
    fn refs(&self) -> Result<RefsResponse> {
750251b2 Andy McCall ↶ 227
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
228
    }
229
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
230
        wire::decode_hashes(&self.call(true, "negotiate", Some(&wire::encode_negotiate(have, want)))?)
231
    }
b3c5a59a Andy McCall ↶ 232
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
233
        wire::decode_bundle(&self.call(true, "fetch", Some(&wire::encode_fetch(changes, max_blob)))?)
750251b2 Andy McCall ↶ 234
    }
b3c5a59a Andy McCall ↶ 235
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
236
        Ok(wire::decode_bundle(&self.call(true, "objects", Some(&wire::encode_hashes(refs)))?)?.objects)
237
    }
2863456a Andy McCall ↶ 238
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
239
        wire::decode_bundle(&self.call(true, "checkpoint", Some(&wire::encode_hashes(heads)))?)
240
    }
241
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
242
        wire::decode_hashes(&self.call(true, "shallow", Some(&wire::encode_shallow(heads, depth)))?)
243
    }
83e0677b Andy McCall ↶ 244
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>, policy: Option<SignedPolicy>) -> Result<Vec<RefResult>> {
245
        wire::decode_push_response(&self.call(true, "push", Some(&wire::encode_push(&bundle, &updates, policy.as_ref())))?)
750251b2 Andy McCall ↶ 246
    }
247
}
248
249
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
2b5ee371 Andy McCall ↶ 250
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
750251b2 Andy McCall ↶ 251
    if url.starts_with("http://") || url.starts_with("https://") {
2b5ee371 Andy McCall ↶ 252
        return Ok(Box::new(HttpRemote::new(url, options)?));
750251b2 Andy McCall ↶ 253
    }
5b9c6ad4 Andy McCall ↶ 254
    let path = url.strip_prefix("file://").unwrap_or(url);
255
    if path.contains("://") {
750251b2 Andy McCall ↶ 256
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
5b9c6ad4 Andy McCall ↶ 257
    }
258
    Ok(Box::new(LocalRemote::open(Path::new(path))?))
259
}
750251b2 Andy McCall ↶ 260
261
// ---------------------------------------------------------------- wire
262
263
/// CBOR encodings of the protocol messages (spec §16.1).
264
pub mod wire {
265
    use super::*;
266
267
    fn schema(msg: &str) -> RepoError {
268
        RepoError::Object(ObjectError::Schema(msg.into()))
269
    }
270
    fn sigs_value(sigs: &[Signature]) -> Value {
271
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
272
    }
273
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
274
        Ok(o::array(v, "signatures")?.iter().map(Signature::from_value).collect::<std::result::Result<Vec<_>, _>>()?)
275
    }
276
    fn hashes_value(hs: &[ObjectRef]) -> Value {
277
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
278
    }
279
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
280
        Ok(o::array(v, "hashes")?.iter().map(|h| o::oref(h, "hash")).collect::<std::result::Result<Vec<_>, _>>()?)
281
    }
282
    fn attest_value(sa: &SignedAttest) -> Value {
283
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
284
    }
285
    fn attest_from(v: &Value) -> Result<SignedAttest> {
286
        let a = o::array(v, "signed attest")?;
287
        if a.len() != 2 {
288
            return Err(schema("signed attest: expected [attest, sigs]"));
289
        }
290
        Ok(SignedAttest { attest: Attest::decode(o::bytes(&a[0], "attest")?)?, sigs: sigs_from(&a[1])? })
291
    }
292
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
293
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
294
    }
295
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
296
        if v.is_null() { Ok(None) } else { attest_from(v).map(Some) }
297
    }
83e0677b Andy McCall ↶ 298
    fn opt_policy_value(sp: Option<&SignedPolicy>) -> Value {
299
        match sp {
300
            Some(sp) => Value::Array(vec![Value::Bytes(sp.policy.encode()), sigs_value(&sp.sigs)]),
301
            None => Value::Null,
302
        }
303
    }
304
    fn opt_policy_from(v: &Value) -> Result<Option<SignedPolicy>> {
305
        if v.is_null() {
306
            return Ok(None);
307
        }
308
        let a = o::array(v, "signed policy")?;
309
        if a.len() != 2 {
310
            return Err(schema("signed policy: expected [policy, sigs]"));
311
        }
312
        Ok(Some(SignedPolicy { policy: Policy::decode(o::bytes(&a[0], "policy")?)?, sigs: sigs_from(&a[1])? }))
313
    }
750251b2 Andy McCall ↶ 314
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
315
        Ok(o::map(v, what)?)
316
    }
317
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
318
        Ok(o::field(m, k, what)?)
319
    }
320
321
    pub fn bundle_value(b: &Bundle) -> Value {
322
        Value::map([
323
            (
324
                0,
325
                Value::Array(b.changes.iter().map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)])).collect()),
326
            ),
327
            (1, Value::Array(b.attests.iter().map(attest_value).collect())),
328
            (
329
                2,
330
                Value::Array(
331
                    b.objects.iter().map(|(t, body)| Value::Array(vec![Value::Text(t.name().into()), Value::Bytes(body.clone())])).collect(),
332
                ),
333
            ),
334
        ])
335
    }
336
337
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
338
        let m = map(v, "bundle")?;
339
        let mut b = Bundle::default();
340
        for c in o::array(field(m, 0, "changes")?, "changes")? {
341
            let pair = o::array(c, "change entry")?;
342
            if pair.len() != 2 {
343
                return Err(schema("change entry: expected [change, sigs]"));
344
            }
345
            b.changes.push((Change::decode(o::bytes(&pair[0], "change")?)?, sigs_from(&pair[1])?));
346
        }
347
        for a in o::array(field(m, 1, "attests")?, "attests")? {
348
            b.attests.push(attest_from(a)?);
349
        }
350
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
351
            let pair = o::array(ob, "object")?;
352
            if pair.len() != 2 {
353
                return Err(schema("object: expected [type, body]"));
354
            }
355
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?).ok_or_else(|| schema("unknown object type"))?;
356
            b.objects.push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
357
        }
358
        Ok(b)
359
    }
360
361
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
362
        cbor::encode(&bundle_value(b))
363
    }
364
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
365
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
366
    }
367
368
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
369
        cbor::encode(&Value::map([
370
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
371
            (1, Value::Bytes(id.to_vec())),
372
            (2, Value::Array(vec![Value::Text("seal".into())])),
373
        ]))
374
    }
375
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
376
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
377
        let m = map(&v, "capabilities")?;
378
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
379
        if !versions.iter().any(|x| x.as_uint() == Some(PROTOCOL_VERSION)) {
380
            return Err(RepoError::Msg("remote does not speak protocol version 1".into()));
381
        }
382
        o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))
383
    }
384
83e0677b Andy McCall ↶ 385
    pub fn encode_refs(id: [u8; 16], resp: &RefsResponse) -> Vec<u8> {
750251b2 Andy McCall ↶ 386
        cbor::encode(&Value::map([
387
            (
388
                0,
389
                Value::Array(
83e0677b Andy McCall ↶ 390
                    resp.refs
391
                        .iter()
750251b2 Andy McCall ↶ 392
                        .map(|r| {
393
                            Value::map([
394
                                (0, Value::Text(r.name.clone())),
395
                                (1, Value::Bytes(r.state.0.to_vec())),
396
                                (2, hashes_value(&r.heads)),
397
                                (3, opt_attest_value(&r.attestation)),
398
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
399
                            ])
400
                        })
401
                        .collect(),
402
                ),
403
            ),
404
            (1, Value::Bytes(id.to_vec())),
83e0677b Andy McCall ↶ 405
            (2, opt_policy_value(resp.policy.as_ref())),
750251b2 Andy McCall ↶ 406
        ]))
407
    }
83e0677b Andy McCall ↶ 408
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], RefsResponse)> {
750251b2 Andy McCall ↶ 409
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
410
        let m = map(&v, "refs")?;
411
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))?;
412
        let mut refs = Vec::new();
413
        for r in o::array(field(m, 0, "refs")?, "refs")? {
414
            let rm = map(r, "ref")?;
415
            let ls = field(rm, 4, "last serial")?;
416
            refs.push(RemoteRef {
417
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
418
                state: o::oref(field(rm, 1, "state")?, "state")?,
419
                heads: hashes_from(field(rm, 2, "heads")?)?,
420
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
421
                last_serial: if ls.is_null() { None } else { Some(o::uint(ls, "last serial")?) },
422
            });
423
        }
83e0677b Andy McCall ↶ 424
        let policy = match m.get(&2) {
425
            Some(p) => opt_policy_from(p)?,
426
            None => None,
427
        };
428
        Ok((id, RefsResponse { refs, policy }))
750251b2 Andy McCall ↶ 429
    }
430
431
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
432
        cbor::encode(&Value::map([(0, hashes_value(have)), (1, hashes_value(want))]))
433
    }
434
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
435
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
436
        let m = map(&v, "negotiate")?;
437
        Ok((hashes_from(field(m, 0, "have")?)?, hashes_from(field(m, 1, "want")?)?))
438
    }
439
b3c5a59a Andy McCall ↶ 440
    /// `fetch` request: `{0: [changes], 1: max blob size or null}`.
441
    pub fn encode_fetch(hs: &[ChangeHash], max_blob: Option<u64>) -> Vec<u8> {
442
        cbor::encode(&Value::map([(0, hashes_value(hs)), (1, max_blob.map(Value::Uint).unwrap_or(Value::Null))]))
443
    }
444
    pub fn decode_fetch(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Option<u64>)> {
445
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
446
        let m = map(&v, "fetch")?;
447
        let max = match m.get(&1) {
448
            Some(x) if !x.is_null() => Some(o::uint(x, "max blob")?),
449
            _ => None,
450
        };
451
        Ok((hashes_from(field(m, 0, "changes")?)?, max))
452
    }
453
2863456a Andy McCall ↶ 454
    /// `shallow` request: `{0: [heads], 1: depth}`.
455
    pub fn encode_shallow(hs: &[ChangeHash], depth: u32) -> Vec<u8> {
456
        cbor::encode(&Value::map([(0, hashes_value(hs)), (1, Value::Uint(depth as u64))]))
457
    }
458
    pub fn decode_shallow(bytes: &[u8]) -> Result<(Vec<ChangeHash>, u32)> {
459
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
460
        let m = map(&v, "shallow")?;
461
        Ok((hashes_from(field(m, 0, "heads")?)?, o::u32_(field(m, 1, "depth")?, "depth")?))
462
    }
463
750251b2 Andy McCall ↶ 464
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
465
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
466
    }
467
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
468
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
469
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
470
    }
471
83e0677b Andy McCall ↶ 472
    pub fn encode_push(bundle: &Bundle, updates: &[RefUpdate], policy: Option<&SignedPolicy>) -> Vec<u8> {
750251b2 Andy McCall ↶ 473
        cbor::encode(&Value::map([
474
            (0, bundle_value(bundle)),
83e0677b Andy McCall ↶ 475
            (2, opt_policy_value(policy)),
750251b2 Andy McCall ↶ 476
            (
477
                1,
478
                Value::Array(
479
                    updates
480
                        .iter()
481
                        .map(|u| {
482
                            Value::map([
483
                                (0, Value::Text(u.name.clone())),
484
                                (1, Value::Bytes(u.old_state.0.to_vec())),
485
                                (2, Value::Bytes(u.new_state.0.to_vec())),
486
                                (3, Value::Bool(u.force)),
487
                                (4, opt_attest_value(&u.attestation)),
488
                            ])
489
                        })
490
                        .collect(),
491
                ),
492
            ),
493
        ]))
494
    }
83e0677b Andy McCall ↶ 495
    pub fn decode_push(bytes: &[u8]) -> Result<(Bundle, Vec<RefUpdate>, Option<SignedPolicy>)> {
750251b2 Andy McCall ↶ 496
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
497
        let m = map(&v, "push")?;
498
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
499
        let mut updates = Vec::new();
500
        for u in o::array(field(m, 1, "updates")?, "updates")? {
501
            let um = map(u, "update")?;
502
            updates.push(RefUpdate {
503
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
504
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
505
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
506
                force: field(um, 3, "force")?.as_bool().ok_or_else(|| schema("force: expected bool"))?,
507
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
508
            });
509
        }
83e0677b Andy McCall ↶ 510
        let policy = match m.get(&2) {
511
            Some(p) => opt_policy_from(p)?,
512
            None => None,
513
        };
514
        Ok((bundle, updates, policy))
750251b2 Andy McCall ↶ 515
    }
516
517
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
518
        cbor::encode(&Value::map([(
519
            0,
520
            Value::Array(
521
                results
522
                    .iter()
523
                    .map(|r| Value::map([(0, Value::Text(r.name.clone())), (1, Value::Bool(r.ok)), (2, Value::Text(r.reason.clone()))]))
524
                    .collect(),
525
            ),
526
        )]))
527
    }
528
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
529
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
530
        let m = map(&v, "push response")?;
531
        let mut out = Vec::new();
532
        for r in o::array(field(m, 0, "results")?, "results")? {
533
            let rm = map(r, "result")?;
534
            out.push(RefResult {
535
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
536
                ok: field(rm, 1, "ok")?.as_bool().ok_or_else(|| schema("ok: expected bool"))?,
537
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
538
            });
539
        }
540
        Ok(out)
541
    }
542
}
543
544
#[cfg(test)]
545
mod tests {
546
    use super::*;
547
    use crate::attest::AttestBody;
548
    use crate::objects::Hlc;
549
550
    #[test]
551
    fn wire_round_trips() {
552
        let c = crate::objects::tests::sample_change();
553
        let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
554
        let id = sk.identity("a", "b", 0);
555
        let key = id.hash();
556
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
557
        let seal = Attest::seal([3; 16], vec![c.hash()]);
558
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
559
        let bundle = Bundle {
560
            changes: vec![(c.clone(), vec![sig.clone()])],
561
            attests: vec![SignedAttest { attest: seal.clone(), sigs: vec![seal_sig] }],
562
            objects: vec![(ObjectType::Identity, id.encode()), (ObjectType::Chunk, b"xyz".to_vec())],
563
        };
564
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
565
        assert_eq!(d.changes, bundle.changes);
566
        assert_eq!(d.attests, bundle.attests);
567
        assert_eq!(d.objects, bundle.objects);
568
569
        let att = Attest { repo: [3; 16], hlc: Hlc::default(), body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 4 } };
570
        let refs = vec![
571
            RemoteRef { name: "main".into(), state: ObjectRef([2; 32]), heads: vec![c.hash()], attestation: Some(SignedAttest { attest: att.clone(), sigs: vec![sig.clone()] }), last_serial: Some(4) },
572
            RemoteRef { name: "dev".into(), state: ObjectRef([5; 32]), heads: vec![], attestation: None, last_serial: None },
573
        ];
83e0677b Andy McCall ↶ 574
        let policy = Policy::single_root([3; 16], id.clone(), 99);
575
        let psig = sk.sign(crate::identity::Domain::Policy, &key, &policy.hash());
576
        let resp = RefsResponse { refs, policy: Some(SignedPolicy { policy: policy.clone(), sigs: vec![psig.clone()] }) };
577
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &resp)).unwrap();
750251b2 Andy McCall ↶ 578
        assert_eq!(id_back, [3; 16]);
83e0677b Andy McCall ↶ 579
        assert_eq!(back.refs.len(), 2);
580
        assert_eq!(back.refs[0].attestation.as_ref().unwrap().attest, att);
581
        assert_eq!(back.refs[1].last_serial, None);
582
        assert_eq!(back.policy.as_ref().unwrap().policy, policy);
750251b2 Andy McCall ↶ 583
584
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(&[ObjectRef([1; 32])], &[ObjectRef([2; 32])])).unwrap();
585
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
586
        assert_eq!(wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(), vec![c.hash()]);
b3c5a59a Andy McCall ↶ 587
        assert_eq!(wire::decode_fetch(&wire::encode_fetch(&[c.hash()], Some(4096))).unwrap(), (vec![c.hash()], Some(4096)));
588
        assert_eq!(wire::decode_fetch(&wire::encode_fetch(&[], None)).unwrap(), (vec![], None));
2863456a Andy McCall ↶ 589
        assert_eq!(wire::decode_shallow(&wire::encode_shallow(&[c.hash()], 3)).unwrap(), (vec![c.hash()], 3));
b3c5a59a Andy McCall ↶ 590
        // An old-style fetch request without the filter field still decodes.
591
        assert_eq!(wire::decode_fetch(&wire::encode_hashes(&[c.hash()])).unwrap(), (vec![c.hash()], None));
750251b2 Andy McCall ↶ 592
593
        let up = RefUpdate { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), force: true, attestation: Some(SignedAttest { attest: att, sigs: vec![sig] }) };
83e0677b Andy McCall ↶ 594
        let sp = SignedPolicy { policy, sigs: vec![psig] };
595
        let (b2, ups, pol) = wire::decode_push(&wire::encode_push(&bundle, std::slice::from_ref(&up), Some(&sp))).unwrap();
596
        assert_eq!(pol, Some(sp));
750251b2 Andy McCall ↶ 597
        assert_eq!(b2.changes.len(), 1);
598
        assert_eq!(ups[0].name, "main");
599
        assert!(ups[0].force);
600
        assert_eq!(ups[0].attestation, up.attestation);
601
        let res = vec![RefResult { name: "main".into(), ok: false, reason: "stale".into() }];
602
        assert_eq!(wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(), res);
603
        assert_eq!(wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(), [7; 16]);
604
    }
605
}