OXYGENHUB
# 17f94b13ae54 oxygen / crates / oxy-core / src / protocol.rs
39.8 KiB · Rust · last changed Store large inserts as text blobs (change format 2) 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, `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`.
8
9
use crate::attest::Attest;
10
use crate::cbor::{self, Value};
11
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
12
use crate::identity::Signature;
13
use crate::objects::{self as o, Change, ObjectError};
14
use crate::policy::Policy;
15
use crate::repo::{Repo, RepoError, Result};
16
use std::path::{Path, PathBuf};
17
use std::sync::Arc;
18
19
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
20
pub const PROTOCOL_VERSION: u64 = 1;
21
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
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
impl SignedPolicy {
37
    /// The proposal-file encoding (`wire::policy_value`).
38
    pub fn encode(&self) -> Vec<u8> {
39
        crate::cbor::encode(&wire::policy_value(self))
40
    }
41
    pub fn decode(b: &[u8]) -> Result<SignedPolicy> {
42
        wire::policy_from(
43
            &crate::cbor::decode(b).map_err(|e| RepoError::Object(ObjectError::from(e)))?,
44
        )
45
    }
46
}
47
48
/// What `refs` returns: every bookmark, every tag, and the current policy.
49
#[derive(Clone, Debug, Default)]
50
pub struct RefsResponse {
51
    pub refs: Vec<RemoteRef>,
52
    pub policy: Option<SignedPolicy>,
53
    pub tags: Vec<TagRef>,
54
    /// Earlier policies, ascending by serial, so a client behind by more
55
    /// than one update can verify each step (spec §14.5).
56
    pub history: Vec<SignedPolicy>,
57
}
58
59
/// A tag as a remote reports it (spec §10.1).
60
#[derive(Clone, Debug, PartialEq, Eq)]
61
pub struct TagRef {
62
    pub name: String,
63
    pub state: ObjectRef,
64
    pub heads: Vec<ChangeHash>,
65
    pub attestation: Option<SignedAttest>,
66
}
67
68
/// One reference as a remote reports it.
69
#[derive(Clone, Debug)]
70
pub struct RemoteRef {
71
    pub name: String,
72
    pub state: ObjectRef,
73
    pub heads: Vec<ChangeHash>,
74
    pub attestation: Option<SignedAttest>,
75
}
76
77
/// Everything needed to add a set of changes to a repository.
78
#[derive(Clone, Debug, Default)]
79
pub struct Bundle {
80
    /// In dependency order, each with its individual signatures.
81
    pub changes: Vec<(Change, Vec<Signature>)>,
82
    /// Seals covering the changes.
83
    pub attests: Vec<SignedAttest>,
84
    /// Chunks, blobs, identities, and state objects referenced by the changes.
85
    pub objects: Vec<(ObjectType, Vec<u8>)>,
86
    /// Policies between the receiver's and the one sent with the push,
87
    /// ascending by serial (spec §14.5).
88
    pub policies: Vec<SignedPolicy>,
89
}
90
91
impl Bundle {
92
    pub fn is_empty(&self) -> bool {
93
        self.changes.is_empty() && self.attests.is_empty() && self.objects.is_empty()
94
    }
95
}
96
97
/// A requested bookmark move (spec §10.2, §16.3).
98
#[derive(Clone, Debug)]
99
pub struct RefUpdate {
100
    pub name: String,
101
    pub old_state: ObjectRef,
102
    pub new_state: ObjectRef,
103
    pub force: bool,
104
    pub attestation: Option<SignedAttest>,
105
}
106
107
#[derive(Clone, Debug, PartialEq, Eq)]
108
pub struct RefResult {
109
    pub name: String,
110
    pub ok: bool,
111
    pub reason: String,
112
}
113
114
pub trait Remote {
115
    fn repo_id(&self) -> Result<[u8; 16]>;
116
    fn refs(&self) -> Result<RefsResponse>;
117
    /// Changes in the closure of `want` but not of `have`, in dependency order.
118
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>>;
119
    /// The changes with their signatures, seals, identities, and blobs; blobs
120
    /// larger than `max_blob` bytes are left out for lazy fetching (spec §13.5).
121
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle>;
122
    /// Individual objects by reference (spec §16.1 `objects`); a blob comes
123
    /// with its chunks.
124
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>>;
125
    /// A checkpoint of exactly `heads` (spec §16.1 `checkpoint`): the
126
    /// checkpoint object, its payload blob, and the blob's chunks.
127
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle>;
128
    /// The changes within `depth` dependency steps of `heads` (spec §17).
129
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>>;
130
    /// Push changes, bookmark updates, new tags, and optionally a newer
131
    /// policy. The policy is applied first; a result named `policy` reports
132
    /// it, and each tag reports as `tags/<name>`.
133
    fn push(
134
        &self,
135
        bundle: Bundle,
136
        updates: Vec<RefUpdate>,
137
        policy: Option<SignedPolicy>,
138
        tags: Vec<TagRef>,
139
    ) -> Result<Vec<RefResult>>;
140
}
141
142
// ---------------------------------------------------------------- local
143
144
/// Another repository on this machine, served in-process.
145
pub struct LocalRemote {
146
    pub repo: Repo,
147
    /// The pushing repository's id, which binds an unbound target on the
148
    /// first push, as `Oxy-Repo-Id` does over HTTP.
149
    pub claimed_id: Option<[u8; 16]>,
150
}
151
152
impl LocalRemote {
153
    pub fn open(path: &Path) -> Result<LocalRemote> {
154
        Ok(LocalRemote {
155
            repo: Repo::open(path)?,
156
            claimed_id: None,
157
        })
158
    }
159
}
160
161
impl Remote for LocalRemote {
162
    fn repo_id(&self) -> Result<[u8; 16]> {
163
        self.repo.id()
164
    }
165
    fn refs(&self) -> Result<RefsResponse> {
166
        self.repo.remote_refs()
167
    }
168
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
169
        self.repo.negotiate(have, want)
170
    }
171
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
172
        self.repo.bundle_for_filtered(changes, max_blob)
173
    }
174
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
175
        self.repo.objects_for(refs)
176
    }
177
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
178
        self.repo.checkpoint_bundle_for(heads)
179
    }
180
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
181
        self.repo.shallow_changes(heads, depth)
182
    }
183
    fn push(
184
        &self,
185
        bundle: Bundle,
186
        updates: Vec<RefUpdate>,
187
        policy: Option<SignedPolicy>,
188
        tags: Vec<TagRef>,
189
    ) -> Result<Vec<RefResult>> {
190
        if self.repo.is_unbound() {
191
            match self.claimed_id {
192
                Some(id) => self.repo.bind_id(id)?,
193
                None => {
194
                    return Err(RepoError::Msg(
195
                        "the remote is unbound and this client did not say which repository it is"
196
                            .into(),
197
                    ))
198
                }
199
            }
200
        }
201
        self.repo.receive_push(bundle, updates, policy, tags)
202
    }
203
}
204
205
// ---------------------------------------------------------------- http
206
207
/// How to reach a remote: credentials and trust.
208
#[derive(Clone, Debug, Default)]
209
pub struct RemoteOptions {
210
    /// Bearer token for pushes.
211
    pub token: Option<String>,
212
    /// PEM file with extra root certificates to trust for `https://` remotes
213
    /// (self-hosted servers); the system web roots are always trusted.
214
    pub ca: Option<PathBuf>,
215
    /// The local repository's id, sent as `Oxy-Repo-Id` so an unbound
216
    /// hosted repository can adopt it on the first push (spec §16.1).
217
    pub repo_id: Option<[u8; 16]>,
218
}
219
220
/// The request header carrying the client's repository id.
221
pub const REPO_ID_HEADER: &str = "Oxy-Repo-Id";
222
223
/// The id of a hosted repository that has not adopted one yet.
224
pub const UNBOUND_ID: [u8; 16] = [0; 16];
225
226
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
227
pub struct HttpRemote {
228
    base: String,
229
    token: Option<String>,
230
    repo_id: Option<[u8; 16]>,
231
    agent: ureq::Agent,
232
}
233
234
/// A TLS client configuration trusting the web roots plus `ca`, if given.
235
fn tls_client_config(ca: Option<&Path>) -> Result<Arc<rustls::ClientConfig>> {
236
    let mut roots = rustls::RootCertStore::empty();
237
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
238
    if let Some(path) = ca {
239
        let pem = std::fs::read(path)
240
            .map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?;
241
        let certs = rustls_pemfile::certs(&mut pem.as_slice())
242
            .collect::<std::result::Result<Vec<_>, _>>()
243
            .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?;
244
        if certs.is_empty() {
245
            return Err(RepoError::Msg(format!(
246
                "CA file {} holds no certificates",
247
                path.display()
248
            )));
249
        }
250
        for c in certs {
251
            roots
252
                .add(c)
253
                .map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?;
254
        }
255
    }
256
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(
257
        rustls::crypto::ring::default_provider(),
258
    ))
259
    .with_safe_default_protocol_versions()
260
    .map_err(|e| RepoError::Msg(e.to_string()))?
261
    .with_root_certificates(roots)
262
    .with_no_client_auth();
263
    Ok(Arc::new(cfg))
264
}
265
266
impl HttpRemote {
267
    pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> {
268
        let agent = ureq::AgentBuilder::new()
269
            .tls_config(tls_client_config(options.ca.as_deref())?)
270
            .build();
271
        Ok(HttpRemote {
272
            base: base.trim_end_matches('/').to_string(),
273
            token: options.token.clone(),
274
            repo_id: options.repo_id,
275
            agent,
276
        })
277
    }
278
279
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
280
        let url = format!("{}/oxy/v1/{endpoint}", self.base);
281
        let mut req = if post {
282
            self.agent.post(&url)
283
        } else {
284
            self.agent.get(&url)
285
        };
286
        req = req.set("Accept", CONTENT_TYPE);
287
        if let Some(t) = &self.token {
288
            req = req.set("Authorization", &format!("Bearer {t}"));
289
        }
290
        if let Some(id) = &self.repo_id {
291
            req = req.set(REPO_ID_HEADER, &hex::encode(id));
292
        }
293
        let resp = match body {
294
            Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b),
295
            None => req.call(),
296
        };
297
        match resp {
298
            Ok(r) => {
299
                let mut out = Vec::new();
300
                r.into_reader()
301
                    .read_to_end(&mut out)
302
                    .map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
303
                Ok(out)
304
            }
305
            Err(ureq::Error::Status(code, r)) => {
306
                let text = r.into_string().unwrap_or_default();
307
                let msg = format!("{url}: HTTP {code}: {}", text.trim());
308
                Err(RepoError::Msg(with_hint(msg)))
309
            }
310
            // The transport error already names the URL.
311
            Err(e) => Err(RepoError::Msg(with_hint(e.to_string()))),
312
        }
313
    }
314
}
315
316
/// What to do about a failed request, recognised from its message.
317
pub fn hint_for(message: &str) -> Option<&'static str> {
318
    let m = message.to_ascii_lowercase();
319
    let hint = if m.contains("http 401") {
320
        "the server did not accept the token: set one with `oxy config remote.<remote>.token TOKEN` (a personal access token; pushing needs repo:write)"
321
    } else if m.contains("http 403") {
322
        "the token or account lacks permission: pushing needs a token with repo:write and write access to the repository"
323
    } else if m.contains("http 404") {
324
        "nothing at this URL: check the owner and repository name, or create the repository on the hub first"
325
    } else if m.contains("unknownissuer")
326
        || m.contains("unknown issuer")
327
        || m.contains("certificate")
328
        || m.contains("invalidcertificate")
329
    {
330
        "the server's certificate is not trusted: for a hub with a self-signed certificate, download it from /.well-known/oxyhub-ca.pem on the hub and run `oxy config remote.<remote>.ca FILE`"
331
    } else if m.contains("connection refused") {
332
        "nothing is listening there: check the URL and port, and that the server is running"
333
    } else if m.contains("dns")
334
        || m.contains("failed to lookup")
335
        || m.contains("name or service not known")
336
    {
337
        "the host name does not resolve: check the URL"
338
    } else if m.contains("timed out") || m.contains("timeout") {
339
        "the server did not answer in time: check the network, or try again"
340
    } else {
341
        return None;
342
    };
343
    Some(hint)
344
}
345
346
/// `message`, followed by what to do about it when that is known.
347
pub fn with_hint(message: String) -> String {
348
    match hint_for(&message) {
349
        Some(h) => format!("{message}\n  hint: {h}"),
350
        None => message,
351
    }
352
}
353
354
/// An authenticated GET of any URL on a host, with the same TLS trust and
355
/// token as remotes use (for hub APIs: proposals). Returns the body.
356
pub fn http_get(url: &str, options: &RemoteOptions) -> Result<Vec<u8>> {
357
    let agent = ureq::AgentBuilder::new()
358
        .tls_config(tls_client_config(options.ca.as_deref())?)
359
        .build();
360
    let mut req = agent.get(url).set("Accept", "application/json");
361
    if let Some(t) = &options.token {
362
        req = req.set("Authorization", &format!("Bearer {t}"));
363
    }
364
    match req.call() {
365
        Ok(r) => {
366
            let mut out = Vec::new();
367
            r.into_reader()
368
                .read_to_end(&mut out)
369
                .map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
370
            Ok(out)
371
        }
372
        Err(ureq::Error::Status(code, r)) => {
373
            let text = r.into_string().unwrap_or_default();
374
            Err(RepoError::Msg(with_hint(format!(
375
                "{url}: HTTP {code}: {}",
376
                text.trim()
377
            ))))
378
        }
379
        // The transport error already names the URL.
380
        Err(e) => Err(RepoError::Msg(with_hint(e.to_string()))),
381
    }
382
}
383
384
impl Remote for HttpRemote {
385
    fn repo_id(&self) -> Result<[u8; 16]> {
386
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
387
    }
388
    fn refs(&self) -> Result<RefsResponse> {
389
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
390
    }
391
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
392
        wire::decode_hashes(&self.call(
393
            true,
394
            "negotiate",
395
            Some(&wire::encode_negotiate(have, want)),
396
        )?)
397
    }
398
    fn fetch(&self, changes: &[ChangeHash], max_blob: Option<u64>) -> Result<Bundle> {
399
        wire::decode_bundle(&self.call(
400
            true,
401
            "fetch",
402
            Some(&wire::encode_fetch(changes, max_blob)),
403
        )?)
404
    }
405
    fn objects(&self, refs: &[ObjectRef]) -> Result<Vec<(ObjectType, Vec<u8>)>> {
406
        Ok(
407
            wire::decode_bundle(&self.call(true, "objects", Some(&wire::encode_hashes(refs)))?)?
408
                .objects,
409
        )
410
    }
411
    fn checkpoint(&self, heads: &[ChangeHash]) -> Result<Bundle> {
412
        wire::decode_bundle(&self.call(true, "checkpoint", Some(&wire::encode_hashes(heads)))?)
413
    }
414
    fn shallow(&self, heads: &[ChangeHash], depth: u32) -> Result<Vec<ChangeHash>> {
415
        wire::decode_hashes(&self.call(
416
            true,
417
            "shallow",
418
            Some(&wire::encode_shallow(heads, depth)),
419
        )?)
420
    }
421
    fn push(
422
        &self,
423
        bundle: Bundle,
424
        updates: Vec<RefUpdate>,
425
        policy: Option<SignedPolicy>,
426
        tags: Vec<TagRef>,
427
    ) -> Result<Vec<RefResult>> {
428
        wire::decode_push_response(&self.call(
429
            true,
430
            "push",
431
            Some(&wire::encode_push(
432
                &bundle,
433
                &updates,
434
                policy.as_ref(),
435
                &tags,
436
            )),
437
        )?)
438
    }
439
}
440
441
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
442
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
443
    if url.starts_with("http://") || url.starts_with("https://") {
444
        return Ok(Box::new(HttpRemote::new(url, options)?));
445
    }
446
    let path = url.strip_prefix("file://").unwrap_or(url);
447
    if path.contains("://") {
448
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
449
    }
450
    let mut local = LocalRemote::open(Path::new(path))?;
451
    local.claimed_id = options.repo_id;
452
    Ok(Box::new(local))
453
}
454
455
// ---------------------------------------------------------------- wire
456
457
/// CBOR encodings of the protocol messages (spec §16.1).
458
pub mod wire {
459
    use super::*;
460
461
    fn schema(msg: &str) -> RepoError {
462
        RepoError::Object(ObjectError::Schema(msg.into()))
463
    }
464
    fn sigs_value(sigs: &[Signature]) -> Value {
465
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
466
    }
467
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
468
        Ok(o::array(v, "signatures")?
469
            .iter()
470
            .map(Signature::from_value)
471
            .collect::<std::result::Result<Vec<_>, _>>()?)
472
    }
473
    fn hashes_value(hs: &[ObjectRef]) -> Value {
474
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
475
    }
476
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
477
        Ok(o::array(v, "hashes")?
478
            .iter()
479
            .map(|h| o::oref(h, "hash"))
480
            .collect::<std::result::Result<Vec<_>, _>>()?)
481
    }
482
    fn attest_value(sa: &SignedAttest) -> Value {
483
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
484
    }
485
    fn attest_from(v: &Value) -> Result<SignedAttest> {
486
        let a = o::array(v, "signed attest")?;
487
        if a.len() != 2 {
488
            return Err(schema("signed attest: expected [attest, sigs]"));
489
        }
490
        Ok(SignedAttest {
491
            attest: Attest::decode(o::bytes(&a[0], "attest")?)?,
492
            sigs: sigs_from(&a[1])?,
493
        })
494
    }
495
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
496
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
497
    }
498
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
499
        if v.is_null() {
500
            Ok(None)
501
        } else {
502
            attest_from(v).map(Some)
503
        }
504
    }
505
    /// `[policy bytes, [signature]]`: also the format of a policy proposal
506
    /// file exchanged between root holders (spec §14.5).
507
    pub fn policy_value(sp: &SignedPolicy) -> Value {
508
        Value::Array(vec![Value::Bytes(sp.policy.encode()), sigs_value(&sp.sigs)])
509
    }
510
    pub fn policy_from(v: &Value) -> Result<SignedPolicy> {
511
        let a = o::array(v, "signed policy")?;
512
        if a.len() != 2 {
513
            return Err(schema("signed policy: expected [policy, sigs]"));
514
        }
515
        Ok(SignedPolicy {
516
            policy: Policy::decode(o::bytes(&a[0], "policy")?)?,
517
            sigs: sigs_from(&a[1])?,
518
        })
519
    }
520
    fn opt_policy_value(sp: Option<&SignedPolicy>) -> Value {
521
        sp.map(policy_value).unwrap_or(Value::Null)
522
    }
523
    fn opt_policy_from(v: &Value) -> Result<Option<SignedPolicy>> {
524
        if v.is_null() {
525
            Ok(None)
526
        } else {
527
            policy_from(v).map(Some)
528
        }
529
    }
530
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
531
        Ok(o::map(v, what)?)
532
    }
533
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
534
        Ok(o::field(m, k, what)?)
535
    }
536
537
    /// A bundle's value. Each change's text blobs and their chunks (spec
538
    /// §6.1) are added to its objects when they are not already there, so a
539
    /// bundle can always be read on its own.
540
    pub fn bundle_value(b: &Bundle) -> Value {
541
        let mut changes = Vec::with_capacity(b.changes.len());
542
        let mut texts: Vec<(ObjectType, Vec<u8>)> = Vec::new();
543
        let mut listed: Option<std::collections::HashSet<ObjectRef>> = None;
544
        for (c, s) in &b.changes {
545
            let (bytes, stored) = c.encode_parts();
546
            for t in stored {
547
                let listed = listed.get_or_insert_with(|| {
548
                    b.objects
549
                        .iter()
550
                        .map(|(ty, body)| crate::hash::hash_object(*ty, body))
551
                        .collect()
552
                });
553
                if !listed.insert(t.blob.hash()) {
554
                    continue;
555
                }
556
                for chunk in crate::chunk::chunks(&t.content) {
557
                    if listed.insert(crate::hash::hash_object(ObjectType::Chunk, chunk)) {
558
                        texts.push((ObjectType::Chunk, chunk.to_vec()));
559
                    }
560
                }
561
                texts.push((ObjectType::Blob, t.blob.encode()));
562
            }
563
            changes.push(Value::Array(vec![Value::Bytes(bytes), sigs_value(s)]));
564
        }
565
        Value::map([
566
            (0, Value::Array(changes)),
567
            (
568
                1,
569
                Value::Array(b.attests.iter().map(attest_value).collect()),
570
            ),
571
            (
572
                2,
573
                Value::Array(
574
                    b.objects
575
                        .iter()
576
                        .chain(&texts)
577
                        .map(|(t, body)| {
578
                            Value::Array(vec![
579
                                Value::Text(t.name().into()),
580
                                Value::Bytes(body.clone()),
581
                            ])
582
                        })
583
                        .collect(),
584
                ),
585
            ),
586
            (
587
                3,
588
                Value::Array(b.policies.iter().map(policy_value).collect()),
589
            ),
590
        ])
591
    }
592
593
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
594
        let m = map(v, "bundle")?;
595
        let mut b = Bundle::default();
596
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
597
            let pair = o::array(ob, "object")?;
598
            if pair.len() != 2 {
599
                return Err(schema("object: expected [type, body]"));
600
            }
601
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?)
602
                .ok_or_else(|| schema("unknown object type"))?;
603
            b.objects
604
                .push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
605
        }
606
        // Stored inserts are read from the bundle's own objects.
607
        let mut by_ref: Option<std::collections::HashMap<ObjectRef, usize>> = None;
608
        for c in o::array(field(m, 0, "changes")?, "changes")? {
609
            let pair = o::array(c, "change entry")?;
610
            if pair.len() != 2 {
611
                return Err(schema("change entry: expected [change, sigs]"));
612
            }
613
            let change = Change::decode_with(o::bytes(&pair[0], "change")?, &mut |text, count| {
614
                let by_ref = by_ref.get_or_insert_with(|| {
615
                    b.objects
616
                        .iter()
617
                        .enumerate()
618
                        .map(|(i, (ty, body))| (crate::hash::hash_object(*ty, body), i))
619
                        .collect()
620
                });
621
                let get =
622
                    |r: &ObjectRef, want: ObjectType| match by_ref.get(r).map(|i| &b.objects[*i]) {
623
                        Some((ty, body)) if *ty == want => Ok(body),
624
                        _ => Err(ObjectError::Schema(format!(
625
                            "the bundle lacks {} {}",
626
                            want.name(),
627
                            r.short(12)
628
                        ))),
629
                    };
630
                let blob = crate::objects::Blob::decode(get(text, ObjectType::Blob)?)?;
631
                let mut content = Vec::with_capacity(blob.length as usize);
632
                for chunk in &blob.chunks {
633
                    content.extend_from_slice(get(chunk, ObjectType::Chunk)?);
634
                }
635
                crate::objects::text_lines(&content, count)
636
            })?;
637
            b.changes.push((change, sigs_from(&pair[1])?));
638
        }
639
        for a in o::array(field(m, 1, "attests")?, "attests")? {
640
            b.attests.push(attest_from(a)?);
641
        }
642
        if let Some(ps) = m.get(&3) {
643
            for p in o::array(ps, "policies")? {
644
                b.policies.push(policy_from(p)?);
645
            }
646
        }
647
        Ok(b)
648
    }
649
650
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
651
        cbor::encode(&bundle_value(b))
652
    }
653
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
654
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
655
    }
656
657
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
658
        cbor::encode(&Value::map([
659
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
660
            (1, Value::Bytes(id.to_vec())),
661
            (2, Value::Array(vec![Value::Text("seal".into())])),
662
        ]))
663
    }
664
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
665
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
666
        let m = map(&v, "capabilities")?;
667
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
668
        if !versions
669
            .iter()
670
            .any(|x| x.as_uint() == Some(PROTOCOL_VERSION))
671
        {
672
            return Err(RepoError::Msg(
673
                "remote does not speak protocol version 1".into(),
674
            ));
675
        }
676
        o::bytes(field(m, 1, "repo id")?, "repo id")?
677
            .try_into()
678
            .map_err(|_| schema("repo id: expected 16 bytes"))
679
    }
680
681
    fn tags_value(tags: &[TagRef]) -> Value {
682
        Value::Array(
683
            tags.iter()
684
                .map(|t| {
685
                    Value::map([
686
                        (0, Value::Text(t.name.clone())),
687
                        (1, Value::Bytes(t.state.0.to_vec())),
688
                        (2, hashes_value(&t.heads)),
689
                        (3, opt_attest_value(&t.attestation)),
690
                    ])
691
                })
692
                .collect(),
693
        )
694
    }
695
    fn tags_from(v: &Value) -> Result<Vec<TagRef>> {
696
        let mut out = Vec::new();
697
        for t in o::array(v, "tags")? {
698
            let tm = map(t, "tag")?;
699
            out.push(TagRef {
700
                name: o::text(field(tm, 0, "name")?, "name")?.to_owned(),
701
                state: o::oref(field(tm, 1, "state")?, "state")?,
702
                heads: hashes_from(field(tm, 2, "heads")?)?,
703
                attestation: opt_attest_from(field(tm, 3, "attestation")?)?,
704
            });
705
        }
706
        Ok(out)
707
    }
708
709
    pub fn encode_refs(id: [u8; 16], resp: &RefsResponse) -> Vec<u8> {
710
        cbor::encode(&Value::map([
711
            (
712
                0,
713
                Value::Array(
714
                    resp.refs
715
                        .iter()
716
                        .map(|r| {
717
                            Value::map([
718
                                (0, Value::Text(r.name.clone())),
719
                                (1, Value::Bytes(r.state.0.to_vec())),
720
                                (2, hashes_value(&r.heads)),
721
                                (3, opt_attest_value(&r.attestation)),
722
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
723
                            ])
724
                        })
725
                        .collect(),
726
                ),
727
            ),
728
            (1, Value::Bytes(id.to_vec())),
729
            (2, opt_policy_value(resp.policy.as_ref())),
730
            (3, tags_value(&resp.tags)),
731
            (
732
                4,
733
                Value::Array(resp.history.iter().map(policy_value).collect()),
734
            ),
735
        ]))
736
    }
737
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], RefsResponse)> {
738
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
739
        let m = map(&v, "refs")?;
740
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?
741
            .try_into()
742
            .map_err(|_| schema("repo id: expected 16 bytes"))?;
743
        let mut refs = Vec::new();
744
        for r in o::array(field(m, 0, "refs")?, "refs")? {
745
            let rm = map(r, "ref")?;
746
            let ls = field(rm, 4, "last serial")?;
747
            refs.push(RemoteRef {
748
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
749
                state: o::oref(field(rm, 1, "state")?, "state")?,
750
                heads: hashes_from(field(rm, 2, "heads")?)?,
751
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
752
                last_serial: if ls.is_null() {
753
                    None
754
                } else {
755
                    Some(o::uint(ls, "last serial")?)
756
                },
757
            });
758
        }
759
        let policy = match m.get(&2) {
760
            Some(p) => opt_policy_from(p)?,
761
            None => None,
762
        };
763
        let tags = match m.get(&3) {
764
            Some(t) => tags_from(t)?,
765
            None => Vec::new(),
766
        };
767
        let mut history = Vec::new();
768
        if let Some(h) = m.get(&4) {
769
            for p in o::array(h, "history")? {
770
                history.push(policy_from(p)?);
771
            }
772
        }
773
        Ok((
774
            id,
775
            RefsResponse {
776
                refs,
777
                policy,
778
                tags,
779
                history,
780
            },
781
        ))
782
    }
783
784
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
785
        cbor::encode(&Value::map([
786
            (0, hashes_value(have)),
787
            (1, hashes_value(want)),
788
        ]))
789
    }
790
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
791
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
792
        let m = map(&v, "negotiate")?;
793
        Ok((
794
            hashes_from(field(m, 0, "have")?)?,
795
            hashes_from(field(m, 1, "want")?)?,
796
        ))
797
    }
798
799
    /// `fetch` request: `{0: [changes], 1: max blob size or null}`.
800
    pub fn encode_fetch(hs: &[ChangeHash], max_blob: Option<u64>) -> Vec<u8> {
801
        cbor::encode(&Value::map([
802
            (0, hashes_value(hs)),
803
            (1, max_blob.map(Value::Uint).unwrap_or(Value::Null)),
804
        ]))
805
    }
806
    pub fn decode_fetch(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Option<u64>)> {
807
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
808
        let m = map(&v, "fetch")?;
809
        let max = match m.get(&1) {
810
            Some(x) if !x.is_null() => Some(o::uint(x, "max blob")?),
811
            _ => None,
812
        };
813
        Ok((hashes_from(field(m, 0, "changes")?)?, max))
814
    }
815
816
    /// `shallow` request: `{0: [heads], 1: depth}`.
817
    pub fn encode_shallow(hs: &[ChangeHash], depth: u32) -> Vec<u8> {
818
        cbor::encode(&Value::map([
819
            (0, hashes_value(hs)),
820
            (1, Value::Uint(depth as u64)),
821
        ]))
822
    }
823
    pub fn decode_shallow(bytes: &[u8]) -> Result<(Vec<ChangeHash>, u32)> {
824
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
825
        let m = map(&v, "shallow")?;
826
        Ok((
827
            hashes_from(field(m, 0, "heads")?)?,
828
            o::u32_(field(m, 1, "depth")?, "depth")?,
829
        ))
830
    }
831
832
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
833
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
834
    }
835
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
836
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
837
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
838
    }
839
840
    pub fn encode_push(
841
        bundle: &Bundle,
842
        updates: &[RefUpdate],
843
        policy: Option<&SignedPolicy>,
844
        tags: &[TagRef],
845
    ) -> Vec<u8> {
846
        cbor::encode(&Value::map([
847
            (0, bundle_value(bundle)),
848
            (2, opt_policy_value(policy)),
849
            (3, tags_value(tags)),
850
            (
851
                1,
852
                Value::Array(
853
                    updates
854
                        .iter()
855
                        .map(|u| {
856
                            Value::map([
857
                                (0, Value::Text(u.name.clone())),
858
                                (1, Value::Bytes(u.old_state.0.to_vec())),
859
                                (2, Value::Bytes(u.new_state.0.to_vec())),
860
                                (3, Value::Bool(u.force)),
861
                                (4, opt_attest_value(&u.attestation)),
862
                            ])
863
                        })
864
                        .collect(),
865
                ),
866
            ),
867
        ]))
868
    }
869
    /// A decoded push: bundle, bookmark updates, policy, tags.
870
    pub type PushParts = (Bundle, Vec<RefUpdate>, Option<SignedPolicy>, Vec<TagRef>);
871
872
    pub fn decode_push(bytes: &[u8]) -> Result<PushParts> {
873
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
874
        let m = map(&v, "push")?;
875
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
876
        let mut updates = Vec::new();
877
        for u in o::array(field(m, 1, "updates")?, "updates")? {
878
            let um = map(u, "update")?;
879
            updates.push(RefUpdate {
880
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
881
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
882
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
883
                force: field(um, 3, "force")?
884
                    .as_bool()
885
                    .ok_or_else(|| schema("force: expected bool"))?,
886
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
887
            });
888
        }
889
        let policy = match m.get(&2) {
890
            Some(p) => opt_policy_from(p)?,
891
            None => None,
892
        };
893
        let tags = match m.get(&3) {
894
            Some(t) => tags_from(t)?,
895
            None => Vec::new(),
896
        };
897
        Ok((bundle, updates, policy, tags))
898
    }
899
900
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
901
        cbor::encode(&Value::map([(
902
            0,
903
            Value::Array(
904
                results
905
                    .iter()
906
                    .map(|r| {
907
                        Value::map([
908
                            (0, Value::Text(r.name.clone())),
909
                            (1, Value::Bool(r.ok)),
910
                            (2, Value::Text(r.reason.clone())),
911
                        ])
912
                    })
913
                    .collect(),
914
            ),
915
        )]))
916
    }
917
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
918
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
919
        let m = map(&v, "push response")?;
920
        let mut out = Vec::new();
921
        for r in o::array(field(m, 0, "results")?, "results")? {
922
            let rm = map(r, "result")?;
923
            out.push(RefResult {
924
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
925
                ok: field(rm, 1, "ok")?
926
                    .as_bool()
927
                    .ok_or_else(|| schema("ok: expected bool"))?,
928
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
929
            });
930
        }
931
        Ok(out)
932
    }
933
}
934
935
#[cfg(test)]
936
mod tests {
937
    use super::*;
938
    use crate::attest::AttestBody;
939
    use crate::objects::Hlc;
940
941
    #[test]
942
    fn wire_round_trips() {
943
        let c = crate::objects::tests::sample_change();
944
        let sk = crate::identity::SecretKey {
945
            ed_seed: [1; 32],
946
            ml_seed: [2; 32],
947
        };
948
        let id = sk.identity("a", "b", 0);
949
        let key = id.hash();
950
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
951
        let seal = Attest::seal([3; 16], vec![c.hash()]);
952
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
953
        let policy = Policy::single_root([3; 16], id.clone(), 99);
954
        let psig = sk.sign(crate::identity::Domain::Policy, &key, &policy.hash());
955
        let bundle = Bundle {
956
            changes: vec![(c.clone(), vec![sig.clone()])],
957
            attests: vec![SignedAttest {
958
                attest: seal.clone(),
959
                sigs: vec![seal_sig],
960
            }],
961
            objects: vec![
962
                (ObjectType::Identity, id.encode()),
963
                (ObjectType::Chunk, b"xyz".to_vec()),
964
            ],
965
            policies: vec![SignedPolicy {
966
                policy: policy.clone(),
967
                sigs: vec![psig.clone()],
968
            }],
969
        };
970
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
971
        assert_eq!(d.changes, bundle.changes);
972
        assert_eq!(d.attests, bundle.attests);
973
        assert_eq!(d.objects, bundle.objects);
974
        assert_eq!(d.policies, bundle.policies);
975
976
        let att = Attest {
977
            repo: [3; 16],
978
            hlc: Hlc::default(),
979
            body: AttestBody::Ref {
980
                name: "main".into(),
981
                old_state: ObjectRef([1; 32]),
982
                new_state: ObjectRef([2; 32]),
983
                serial: 4,
984
            },
985
        };
986
        let refs = vec![
987
            RemoteRef {
988
                name: "main".into(),
989
                state: ObjectRef([2; 32]),
990
                heads: vec![c.hash()],
991
                attestation: Some(SignedAttest {
992
                    attest: att.clone(),
993
                    sigs: vec![sig.clone()],
994
                }),
995
                last_serial: Some(4),
996
            },
997
            RemoteRef {
998
                name: "dev".into(),
999
                state: ObjectRef([5; 32]),
1000
                heads: vec![],
1001
                attestation: None,
1002
                last_serial: None,
1003
            },
1004
        ];
1005
        let tag = TagRef {
1006
            name: "v1".into(),
1007
            state: ObjectRef([2; 32]),
1008
            heads: vec![c.hash()],
1009
            attestation: None,
1010
        };
1011
        let resp = RefsResponse {
1012
            refs,
1013
            policy: Some(SignedPolicy {
1014
                policy: policy.clone(),
1015
                sigs: vec![psig.clone()],
1016
            }),
1017
            tags: vec![tag.clone()],
1018
            history: vec![SignedPolicy {
1019
                policy: policy.clone(),
1020
                sigs: vec![],
1021
            }],
1022
        };
1023
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &resp)).unwrap();
1024
        assert_eq!(id_back, [3; 16]);
1025
        assert_eq!(back.refs.len(), 2);
1026
        assert_eq!(back.refs[0].attestation.as_ref().unwrap().attest, att);
1027
        assert_eq!(back.refs[1].last_serial, None);
1028
        assert_eq!(back.policy.as_ref().unwrap().policy, policy);
1029
        assert_eq!(back.history, resp.history);
1030
        assert_eq!(back.tags, vec![tag.clone()]);
1031
1032
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(
1033
            &[ObjectRef([1; 32])],
1034
            &[ObjectRef([2; 32])],
1035
        ))
1036
        .unwrap();
1037
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
1038
        assert_eq!(
1039
            wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(),
1040
            vec![c.hash()]
1041
        );
1042
        assert_eq!(
1043
            wire::decode_fetch(&wire::encode_fetch(&[c.hash()], Some(4096))).unwrap(),
1044
            (vec![c.hash()], Some(4096))
1045
        );
1046
        assert_eq!(
1047
            wire::decode_fetch(&wire::encode_fetch(&[], None)).unwrap(),
1048
            (vec![], None)
1049
        );
1050
        assert_eq!(
1051
            wire::decode_shallow(&wire::encode_shallow(&[c.hash()], 3)).unwrap(),
1052
            (vec![c.hash()], 3)
1053
        );
1054
        // An old-style fetch request without the filter field still decodes.
1055
        assert_eq!(
1056
            wire::decode_fetch(&wire::encode_hashes(&[c.hash()])).unwrap(),
1057
            (vec![c.hash()], None)
1058
        );
1059
1060
        let up = RefUpdate {
1061
            name: "main".into(),
1062
            old_state: ObjectRef([1; 32]),
1063
            new_state: ObjectRef([2; 32]),
1064
            force: true,
1065
            attestation: Some(SignedAttest {
1066
                attest: att,
1067
                sigs: vec![sig],
1068
            }),
1069
        };
1070
        let sp = SignedPolicy {
1071
            policy,
1072
            sigs: vec![psig],
1073
        };
1074
        let (b2, ups, pol, tags) = wire::decode_push(&wire::encode_push(
1075
            &bundle,
1076
            std::slice::from_ref(&up),
1077
            Some(&sp),
1078
            std::slice::from_ref(&tag),
1079
        ))
1080
        .unwrap();
1081
        assert_eq!(pol, Some(sp));
1082
        assert_eq!(tags, vec![tag]);
1083
        assert_eq!(b2.changes.len(), 1);
1084
        assert_eq!(ups[0].name, "main");
1085
        assert!(ups[0].force);
1086
        assert_eq!(ups[0].attestation, up.attestation);
1087
        let res = vec![RefResult {
1088
            name: "main".into(),
1089
            ok: false,
1090
            reason: "stale".into(),
1091
        }];
1092
        assert_eq!(
1093
            wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(),
1094
            res
1095
        );
1096
        assert_eq!(
1097
            wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(),
1098
            [7; 16]
1099
        );
1100
    }
1101
}