OXYGENHUB
# 728da0f92161 oxygen / crates / oxy-core / src / objects.rs
26.4 KiB · Rust · last changed Subrepositories: subrepo node kind with Oxygen and Git pins, .oxymodules, nested repositories skipped by the walker and moved by checkout, oxy submodule add|update|status|remove, Git gitlink import and export (spec 0.8 section 7.8) 1 month ago source blame history raw
1
//! Typed objects and their canonical encodings (spec §6, §7, §13.2).
2
//!
3
//! Encoding conventions not fixed by the spec text are defined here:
4
//! - Enum-like items (`TreeOp`, `Edit`) are maps whose key 0 is the variant tag.
5
//! - An `OpRef`/atom reference is `[bytes32, uint]`; `AtomRef::Start` is `0`
6
//!   and `AtomRef::End` is `1`.
7
//! - A `Mode` is an unsigned integer: bit 0 = executable, bit 1 = opaque.
8
//! - `NodeKind`: 0 = file, 1 = dir, 2 = symlink.
9
10
use crate::cbor::{self, Value};
11
use crate::hash::{hash_object, ChangeHash, KeyId, ObjectRef, ObjectType};
12
use crate::ids::{ChangeId, NodeId};
13
use std::collections::BTreeMap;
14
15
pub const FORMAT_VERSION: u64 = 1;
16
17
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
18
pub enum ObjectError {
19
    #[error("cbor: {0}")]
20
    Cbor(#[from] cbor::CborError),
21
    #[error("schema: {0}")]
22
    Schema(String),
23
}
24
25
pub(crate) fn schema<T>(msg: impl Into<String>) -> Result<T, ObjectError> {
26
    Err(ObjectError::Schema(msg.into()))
27
}
28
29
pub(crate) type Map = BTreeMap<u64, Value>;
30
31
pub(crate) fn field<'a>(m: &'a Map, k: u64, what: &str) -> Result<&'a Value, ObjectError> {
32
    m.get(&k).ok_or_else(|| ObjectError::Schema(format!("missing field {k} ({what})")))
33
}
34
pub(crate) fn uint(v: &Value, what: &str) -> Result<u64, ObjectError> {
35
    v.as_uint().ok_or_else(|| ObjectError::Schema(format!("{what}: expected unsigned integer")))
36
}
37
pub(crate) fn u32_(v: &Value, what: &str) -> Result<u32, ObjectError> {
38
    u32::try_from(uint(v, what)?).map_err(|_| ObjectError::Schema(format!("{what}: exceeds u32")))
39
}
40
pub(crate) fn text<'a>(v: &'a Value, what: &str) -> Result<&'a str, ObjectError> {
41
    v.as_text().ok_or_else(|| ObjectError::Schema(format!("{what}: expected text")))
42
}
43
pub(crate) fn array<'a>(v: &'a Value, what: &str) -> Result<&'a [Value], ObjectError> {
44
    v.as_array().ok_or_else(|| ObjectError::Schema(format!("{what}: expected array")))
45
}
46
pub(crate) fn map<'a>(v: &'a Value, what: &str) -> Result<&'a Map, ObjectError> {
47
    v.as_map().ok_or_else(|| ObjectError::Schema(format!("{what}: expected map")))
48
}
49
pub(crate) fn bytes<'a>(v: &'a Value, what: &str) -> Result<&'a [u8], ObjectError> {
50
    v.as_bytes().ok_or_else(|| ObjectError::Schema(format!("{what}: expected bytes")))
51
}
52
pub(crate) fn oref(v: &Value, what: &str) -> Result<ObjectRef, ObjectError> {
53
    ObjectRef::from_slice(bytes(v, what)?).ok_or_else(|| ObjectError::Schema(format!("{what}: expected 32 bytes")))
54
}
55
pub(crate) fn node_id(v: &Value, what: &str) -> Result<NodeId, ObjectError> {
56
    NodeId::from_slice(bytes(v, what)?).ok_or_else(|| ObjectError::Schema(format!("{what}: expected 16 bytes")))
57
}
58
pub(crate) fn check_version(m: &Map) -> Result<(), ObjectError> {
59
    let v = uint(field(m, 0, "version")?, "version")?;
60
    if v != FORMAT_VERSION {
61
        return schema(format!("unsupported format version {v}"));
62
    }
63
    Ok(())
64
}
65
66
// ---------------------------------------------------------------- Hlc
67
68
/// Hybrid logical clock (spec §6.4).
69
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
70
pub struct Hlc {
71
    pub wall_ms: u64,
72
    pub logical: u32,
73
}
74
75
impl Hlc {
76
    pub fn now() -> Hlc {
77
        let wall_ms = std::time::SystemTime::now()
78
            .duration_since(std::time::UNIX_EPOCH)
79
            .map(|d| d.as_millis() as u64)
80
            .unwrap_or(0);
81
        Hlc { wall_ms, logical: 0 }
82
    }
83
    pub(crate) fn to_value(self) -> Value {
84
        Value::map([(0, Value::Uint(self.wall_ms)), (1, Value::Uint(self.logical as u64))])
85
    }
86
    pub(crate) fn from_value(v: &Value) -> Result<Hlc, ObjectError> {
87
        let m = map(v, "hlc")?;
88
        Ok(Hlc {
89
            wall_ms: uint(field(m, 0, "hlc.wall_ms")?, "hlc.wall_ms")?,
90
            logical: u32_(field(m, 1, "hlc.logical")?, "hlc.logical")?,
91
        })
92
    }
93
}
94
95
// ---------------------------------------------------------------- tree
96
97
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98
pub enum NodeKind {
99
    File,
100
    Dir,
101
    Symlink,
102
    /// A nested repository pinned to a state (spec §7.8); content is a pin.
103
    Subrepo,
104
}
105
106
impl NodeKind {
107
    pub(crate) fn tag(self) -> u64 {
108
        match self {
109
            NodeKind::File => 0,
110
            NodeKind::Dir => 1,
111
            NodeKind::Symlink => 2,
112
            NodeKind::Subrepo => 3,
113
        }
114
    }
115
    pub(crate) fn from_tag(t: u64) -> Result<NodeKind, ObjectError> {
116
        Ok(match t {
117
            0 => NodeKind::File,
118
            1 => NodeKind::Dir,
119
            2 => NodeKind::Symlink,
120
            3 => NodeKind::Subrepo,
121
            _ => return schema(format!("unknown node kind {t}")),
122
        })
123
    }
124
}
125
126
/// Executable bit plus content mode (spec §7.1, §7.5).
127
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
128
pub struct Mode {
129
    pub exec: bool,
130
    pub opaque: bool,
131
}
132
133
impl Mode {
134
    pub const TEXT: Mode = Mode { exec: false, opaque: false };
135
    pub const OPAQUE: Mode = Mode { exec: false, opaque: true };
136
    pub(crate) fn bits(self) -> u64 {
137
        (self.exec as u64) | ((self.opaque as u64) << 1)
138
    }
139
    pub(crate) fn from_bits(b: u64) -> Result<Mode, ObjectError> {
140
        if b > 3 {
141
            return schema(format!("unknown mode bits {b}"));
142
        }
143
        Ok(Mode { exec: b & 1 != 0, opaque: b & 2 != 0 })
144
    }
145
}
146
147
/// Reference to a tree operation of some change: the placement or mode atom it
148
/// introduced. `change == ObjectRef::ZERO` means this change.
149
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
150
pub struct OpRef {
151
    pub change: ChangeHash,
152
    pub index: u32,
153
}
154
155
impl OpRef {
156
    fn to_value(self) -> Value {
157
        Value::Array(vec![Value::Bytes(self.change.0.to_vec()), Value::Uint(self.index as u64)])
158
    }
159
    fn from_value(v: &Value, what: &str) -> Result<OpRef, ObjectError> {
160
        let a = array(v, what)?;
161
        if a.len() != 2 {
162
            return schema(format!("{what}: expected [hash, index]"));
163
        }
164
        Ok(OpRef { change: oref(&a[0], what)?, index: u32_(&a[1], what)? })
165
    }
166
    /// Replace a self reference with the concrete change hash.
167
    pub fn resolve(self, me: ChangeHash) -> OpRef {
168
        if self.change.is_zero() { OpRef { change: me, ..self } } else { self }
169
    }
170
}
171
172
#[derive(Debug, Clone, PartialEq, Eq)]
173
pub enum TreeOp {
174
    AddNode { node: NodeId, kind: NodeKind, mode: Mode, parent: NodeId, name: String },
175
    RemoveNode { node: NodeId, placement: OpRef },
176
    Move { node: NodeId, old: OpRef, parent: NodeId, name: String },
177
    SetMode { node: NodeId, old: OpRef, mode: Mode },
178
}
179
180
/// Names must be valid, non-empty, not `.`/`..`, and free of `/` and NUL (spec §7.1).
181
pub fn validate_name(name: &str) -> Result<(), ObjectError> {
182
    if name.is_empty() || name == "." || name == ".." || name.contains('/') || name.contains('\0') {
183
        return schema(format!("invalid placement name {name:?}"));
184
    }
185
    Ok(())
186
}
187
188
impl TreeOp {
189
    pub fn node(&self) -> NodeId {
190
        match self {
191
            TreeOp::AddNode { node, .. }
192
            | TreeOp::RemoveNode { node, .. }
193
            | TreeOp::Move { node, .. }
194
            | TreeOp::SetMode { node, .. } => *node,
195
        }
196
    }
197
198
    fn to_value(&self) -> Value {
199
        let nid = |n: &NodeId| Value::Bytes(n.0.to_vec());
200
        match self {
201
            TreeOp::AddNode { node, kind, mode, parent, name } => Value::map([
202
                (0, Value::Uint(0)),
203
                (1, nid(node)),
204
                (2, Value::Uint(kind.tag())),
205
                (3, Value::Uint(mode.bits())),
206
                (4, nid(parent)),
207
                (5, Value::Text(name.clone())),
208
            ]),
209
            TreeOp::RemoveNode { node, placement } => {
210
                Value::map([(0, Value::Uint(1)), (1, nid(node)), (2, placement.to_value())])
211
            }
212
            TreeOp::Move { node, old, parent, name } => Value::map([
213
                (0, Value::Uint(2)),
214
                (1, nid(node)),
215
                (2, old.to_value()),
216
                (4, nid(parent)),
217
                (5, Value::Text(name.clone())),
218
            ]),
219
            TreeOp::SetMode { node, old, mode } => Value::map([
220
                (0, Value::Uint(3)),
221
                (1, nid(node)),
222
                (2, old.to_value()),
223
                (3, Value::Uint(mode.bits())),
224
            ]),
225
        }
226
    }
227
228
    fn from_value(v: &Value) -> Result<TreeOp, ObjectError> {
229
        let m = map(v, "tree_op")?;
230
        let tag = uint(field(m, 0, "tree_op tag")?, "tree_op tag")?;
231
        let node = node_id(field(m, 1, "tree_op.node")?, "tree_op.node")?;
232
        let op = match tag {
233
            0 => {
234
                let name = text(field(m, 5, "name")?, "name")?.to_owned();
235
                validate_name(&name)?;
236
                TreeOp::AddNode {
237
                    node,
238
                    kind: NodeKind::from_tag(uint(field(m, 2, "kind")?, "kind")?)?,
239
                    mode: Mode::from_bits(uint(field(m, 3, "mode")?, "mode")?)?,
240
                    parent: node_id(field(m, 4, "parent")?, "parent")?,
241
                    name,
242
                }
243
            }
244
            1 => TreeOp::RemoveNode { node, placement: OpRef::from_value(field(m, 2, "placement")?, "placement")? },
245
            2 => {
246
                let name = text(field(m, 5, "name")?, "name")?.to_owned();
247
                validate_name(&name)?;
248
                TreeOp::Move {
249
                    node,
250
                    old: OpRef::from_value(field(m, 2, "old")?, "old")?,
251
                    parent: node_id(field(m, 4, "parent")?, "parent")?,
252
                    name,
253
                }
254
            }
255
            3 => TreeOp::SetMode {
256
                node,
257
                old: OpRef::from_value(field(m, 2, "old")?, "old")?,
258
                mode: Mode::from_bits(uint(field(m, 3, "mode")?, "mode")?)?,
259
            },
260
            _ => return schema(format!("unknown tree op tag {tag}")),
261
        };
262
        if node.is_root() {
263
            return schema("tree op on the root node");
264
        }
265
        Ok(op)
266
    }
267
}
268
269
// ---------------------------------------------------------------- edits
270
271
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
272
pub enum AtomRef {
273
    Start,
274
    End,
275
    Atom { change: ChangeHash, index: u32 },
276
}
277
278
impl AtomRef {
279
    fn to_value(self) -> Value {
280
        match self {
281
            AtomRef::Start => Value::Uint(0),
282
            AtomRef::End => Value::Uint(1),
283
            AtomRef::Atom { change, index } => {
284
                Value::Array(vec![Value::Bytes(change.0.to_vec()), Value::Uint(index as u64)])
285
            }
286
        }
287
    }
288
    fn from_value(v: &Value, what: &str) -> Result<AtomRef, ObjectError> {
289
        match v {
290
            Value::Uint(0) => Ok(AtomRef::Start),
291
            Value::Uint(1) => Ok(AtomRef::End),
292
            Value::Array(a) if a.len() == 2 => {
293
                Ok(AtomRef::Atom { change: oref(&a[0], what)?, index: u32_(&a[1], what)? })
294
            }
295
            _ => schema(format!("{what}: expected atom reference")),
296
        }
297
    }
298
    pub fn resolve(self, me: ChangeHash) -> AtomRef {
299
        match self {
300
            AtomRef::Atom { change, index } if change.is_zero() => AtomRef::Atom { change: me, index },
301
            other => other,
302
        }
303
    }
304
}
305
306
/// Half-open range of atoms introduced by one change.
307
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
308
pub struct AtomRange {
309
    pub change: ChangeHash,
310
    pub from: u32,
311
    pub to: u32,
312
}
313
314
impl AtomRange {
315
    fn to_value(self) -> Value {
316
        Value::Array(vec![
317
            Value::Bytes(self.change.0.to_vec()),
318
            Value::Uint(self.from as u64),
319
            Value::Uint(self.to as u64),
320
        ])
321
    }
322
    fn from_value(v: &Value) -> Result<AtomRange, ObjectError> {
323
        let a = array(v, "atom range")?;
324
        if a.len() != 3 {
325
            return schema("atom range: expected [hash, from, to]");
326
        }
327
        let r = AtomRange { change: oref(&a[0], "range.change")?, from: u32_(&a[1], "range.from")?, to: u32_(&a[2], "range.to")? };
328
        if r.from >= r.to {
329
            return schema("atom range: empty or inverted");
330
        }
331
        Ok(r)
332
    }
333
    pub fn resolve(self, me: ChangeHash) -> AtomRange {
334
        if self.change.is_zero() { AtomRange { change: me, ..self } } else { self }
335
    }
336
}
337
338
#[derive(Debug, Clone, PartialEq, Eq)]
339
pub enum Edit {
340
    Insert { node: NodeId, after: AtomRef, before: AtomRef, lines: Vec<Vec<u8>> },
341
    Delete { node: NodeId, atoms: Vec<AtomRange> },
342
    Replace { node: NodeId, old: Option<ObjectRef>, new: Option<ObjectRef> },
343
}
344
345
impl Edit {
346
    pub fn node(&self) -> NodeId {
347
        match self {
348
            Edit::Insert { node, .. } | Edit::Delete { node, .. } | Edit::Replace { node, .. } => *node,
349
        }
350
    }
351
352
    fn to_value(&self) -> Value {
353
        let nid = |n: &NodeId| Value::Bytes(n.0.to_vec());
354
        let opt = |o: &Option<ObjectRef>| match o {
355
            Some(r) => Value::Bytes(r.0.to_vec()),
356
            None => Value::Null,
357
        };
358
        match self {
359
            Edit::Insert { node, after, before, lines } => Value::map([
360
                (0, Value::Uint(0)),
361
                (1, nid(node)),
362
                (2, after.to_value()),
363
                (3, before.to_value()),
364
                (4, Value::Array(lines.iter().map(|l| Value::Bytes(l.clone())).collect())),
365
            ]),
366
            Edit::Delete { node, atoms } => Value::map([
367
                (0, Value::Uint(1)),
368
                (1, nid(node)),
369
                (2, Value::Array(atoms.iter().map(|r| r.to_value()).collect())),
370
            ]),
371
            Edit::Replace { node, old, new } => {
372
                Value::map([(0, Value::Uint(2)), (1, nid(node)), (2, opt(old)), (3, opt(new))])
373
            }
374
        }
375
    }
376
377
    fn from_value(v: &Value) -> Result<Edit, ObjectError> {
378
        let m = map(v, "edit")?;
379
        let tag = uint(field(m, 0, "edit tag")?, "edit tag")?;
380
        let node = node_id(field(m, 1, "edit.node")?, "edit.node")?;
381
        let opt = |v: &Value, what: &str| -> Result<Option<ObjectRef>, ObjectError> {
382
            if v.is_null() { Ok(None) } else { Ok(Some(oref(v, what)?)) }
383
        };
384
        Ok(match tag {
385
            0 => {
386
                let lines = array(field(m, 4, "lines")?, "lines")?
387
                    .iter()
388
                    .map(|l| bytes(l, "line").map(|b| b.to_vec()))
389
                    .collect::<Result<Vec<_>, _>>()?;
390
                if lines.is_empty() {
391
                    return schema("insert with no lines");
392
                }
393
                Edit::Insert {
394
                    node,
395
                    after: AtomRef::from_value(field(m, 2, "after")?, "after")?,
396
                    before: AtomRef::from_value(field(m, 3, "before")?, "before")?,
397
                    lines,
398
                }
399
            }
400
            1 => {
401
                let atoms = array(field(m, 2, "atoms")?, "atoms")?
402
                    .iter()
403
                    .map(AtomRange::from_value)
404
                    .collect::<Result<Vec<_>, _>>()?;
405
                if atoms.is_empty() {
406
                    return schema("delete with no atoms");
407
                }
408
                Edit::Delete { node, atoms }
409
            }
410
            2 => Edit::Replace { node, old: opt(field(m, 2, "old")?, "old")?, new: opt(field(m, 3, "new")?, "new")? },
411
            _ => return schema(format!("unknown edit tag {tag}")),
412
        })
413
    }
414
}
415
416
// ---------------------------------------------------------------- change
417
418
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
419
pub enum ChangeKind {
420
    Normal,
421
    Resolution,
422
    WorkingCopy,
423
}
424
425
impl ChangeKind {
426
    pub(crate) fn tag(self) -> u64 {
427
        match self {
428
            ChangeKind::Normal => 0,
429
            ChangeKind::Resolution => 1,
430
            ChangeKind::WorkingCopy => 2,
431
        }
432
    }
433
    pub(crate) fn from_tag(t: u64) -> Result<ChangeKind, ObjectError> {
434
        Ok(match t {
435
            0 => ChangeKind::Normal,
436
            1 => ChangeKind::Resolution,
437
            2 => ChangeKind::WorkingCopy,
438
            _ => return schema(format!("unknown change kind {t}")),
439
        })
440
    }
441
}
442
443
/// A change (spec §6.1). `deps` is kept sorted and unique by `normalize`.
444
#[derive(Debug, Clone, PartialEq, Eq)]
445
pub struct Change {
446
    pub change_id: ChangeId,
447
    pub deps: Vec<ChangeHash>,
448
    pub author: KeyId,
449
    pub hlc: Hlc,
450
    pub message: String,
451
    pub tree_ops: Vec<TreeOp>,
452
    pub edits: Vec<Edit>,
453
    pub kind: ChangeKind,
454
}
455
456
impl Change {
457
    pub fn normalize(&mut self) {
458
        self.deps.sort();
459
        self.deps.dedup();
460
    }
461
462
    pub fn to_value(&self) -> Value {
463
        Value::map([
464
            (0, Value::Uint(FORMAT_VERSION)),
465
            (1, Value::Bytes(self.change_id.0.to_vec())),
466
            (2, Value::Array(self.deps.iter().map(|d| Value::Bytes(d.0.to_vec())).collect())),
467
            (3, Value::Bytes(self.author.0.to_vec())),
468
            (4, self.hlc.to_value()),
469
            (5, Value::Text(self.message.clone())),
470
            (6, Value::Array(self.tree_ops.iter().map(|t| t.to_value()).collect())),
471
            (7, Value::Array(self.edits.iter().map(|e| e.to_value()).collect())),
472
            (8, Value::Uint(self.kind.tag())),
473
            (9, Value::Map(BTreeMap::new())),
474
        ])
475
    }
476
477
    pub fn encode(&self) -> Vec<u8> {
478
        debug_assert!(self.deps.windows(2).all(|w| w[0] < w[1]), "deps must be normalized");
479
        cbor::encode(&self.to_value())
480
    }
481
482
    pub fn hash(&self) -> ChangeHash {
483
        hash_object(ObjectType::Change, &self.encode())
484
    }
485
486
    pub fn decode(bytes: &[u8]) -> Result<Change, ObjectError> {
487
        let v = cbor::decode(bytes)?;
488
        Self::from_value(&v)
489
    }
490
491
    pub fn from_value(v: &Value) -> Result<Change, ObjectError> {
492
        let m = map(v, "change")?;
493
        check_version(m)?;
494
        let change_id = ChangeId::from_slice(bytes(field(m, 1, "change_id")?, "change_id")?)
495
            .ok_or_else(|| ObjectError::Schema("change_id: expected 16 bytes".into()))?;
496
        let deps = array(field(m, 2, "deps")?, "deps")?
497
            .iter()
498
            .map(|d| oref(d, "dep"))
499
            .collect::<Result<Vec<_>, _>>()?;
500
        if !deps.windows(2).all(|w| w[0] < w[1]) {
501
            return schema("deps must be sorted and unique");
502
        }
503
        if deps.iter().any(|d| d.is_zero()) {
504
            return schema("deps may not contain the self reference");
505
        }
506
        let extra = map(field(m, 9, "extra")?, "extra")?;
507
        if !extra.is_empty() {
508
            return schema("extra must be empty in format version 1");
509
        }
510
        Ok(Change {
511
            change_id,
512
            deps,
513
            author: oref(field(m, 3, "author")?, "author")?,
514
            hlc: Hlc::from_value(field(m, 4, "hlc")?)?,
515
            message: text(field(m, 5, "message")?, "message")?.to_owned(),
516
            tree_ops: array(field(m, 6, "tree_ops")?, "tree_ops")?
517
                .iter()
518
                .map(TreeOp::from_value)
519
                .collect::<Result<Vec<_>, _>>()?,
520
            edits: array(field(m, 7, "edits")?, "edits")?
521
                .iter()
522
                .map(Edit::from_value)
523
                .collect::<Result<Vec<_>, _>>()?,
524
            kind: ChangeKind::from_tag(uint(field(m, 8, "kind")?, "kind")?)?,
525
        })
526
    }
527
}
528
529
// ---------------------------------------------------------------- blob
530
531
/// File content as an ordered list of chunks (spec §13.2).
532
#[derive(Debug, Clone, PartialEq, Eq)]
533
pub struct Blob {
534
    pub length: u64,
535
    pub chunks: Vec<ObjectRef>,
536
}
537
538
impl Blob {
539
    pub fn encode(&self) -> Vec<u8> {
540
        cbor::encode(&Value::map([
541
            (0, Value::Uint(FORMAT_VERSION)),
542
            (1, Value::Uint(self.length)),
543
            (2, Value::Array(self.chunks.iter().map(|c| Value::Bytes(c.0.to_vec())).collect())),
544
        ]))
545
    }
546
    pub fn hash(&self) -> ObjectRef {
547
        hash_object(ObjectType::Blob, &self.encode())
548
    }
549
    pub fn decode(bytes: &[u8]) -> Result<Blob, ObjectError> {
550
        let v = cbor::decode(bytes)?;
551
        let m = map(&v, "blob")?;
552
        check_version(m)?;
553
        Ok(Blob {
554
            length: uint(field(m, 1, "length")?, "length")?,
555
            chunks: array(field(m, 2, "chunks")?, "chunks")?
556
                .iter()
557
                .map(|c| oref(c, "chunk"))
558
                .collect::<Result<Vec<_>, _>>()?,
559
        })
560
    }
561
}
562
563
// ---------------------------------------------------------------- checkpoint
564
565
/// A materialized state (spec §11): the heads it stands for, how many changes
566
/// it holds, and a blob with the serialized graphs (`State::checkpoint_payload`).
567
#[derive(Debug, Clone, PartialEq, Eq)]
568
pub struct Checkpoint {
569
    pub heads: Vec<ChangeHash>,
570
    pub count: u64,
571
    pub payload: ObjectRef,
572
}
573
574
impl Checkpoint {
575
    pub fn encode(&self) -> Vec<u8> {
576
        cbor::encode(&Value::map([
577
            (0, Value::Uint(FORMAT_VERSION)),
578
            (1, Value::Array(self.heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())),
579
            (2, Value::Uint(self.count)),
580
            (3, Value::Bytes(self.payload.0.to_vec())),
581
        ]))
582
    }
583
    pub fn hash(&self) -> ObjectRef {
584
        hash_object(ObjectType::Checkpoint, &self.encode())
585
    }
586
    pub fn decode(bytes: &[u8]) -> Result<Checkpoint, ObjectError> {
587
        let v = cbor::decode(bytes)?;
588
        let m = map(&v, "checkpoint")?;
589
        check_version(m)?;
590
        Ok(Checkpoint {
591
            heads: array(field(m, 1, "heads")?, "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?,
592
            count: uint(field(m, 2, "count")?, "count")?,
593
            payload: oref(field(m, 3, "payload")?, "payload")?,
594
        })
595
    }
596
}
597
598
// ---------------------------------------------------------------- state
599
600
/// A state named by its heads (spec §10.1): what a bookmark or tag points at.
601
#[derive(Debug, Clone, PartialEq, Eq, Default)]
602
pub struct StateObject {
603
    pub heads: Vec<ChangeHash>,
604
}
605
606
impl StateObject {
607
    pub fn new(mut heads: Vec<ChangeHash>) -> StateObject {
608
        heads.sort();
609
        heads.dedup();
610
        StateObject { heads }
611
    }
612
    pub fn encode(&self) -> Vec<u8> {
613
        debug_assert!(self.heads.windows(2).all(|w| w[0] < w[1]));
614
        cbor::encode(&Value::map([
615
            (0, Value::Uint(FORMAT_VERSION)),
616
            (1, Value::Array(self.heads.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())),
617
        ]))
618
    }
619
    pub fn hash(&self) -> ObjectRef {
620
        hash_object(ObjectType::State, &self.encode())
621
    }
622
    pub fn decode(bytes: &[u8]) -> Result<StateObject, ObjectError> {
623
        let v = cbor::decode(bytes)?;
624
        let m = map(&v, "state")?;
625
        check_version(m)?;
626
        let heads = array(field(m, 1, "heads")?, "heads")?.iter().map(|h| oref(h, "head")).collect::<Result<Vec<_>, _>>()?;
627
        if !heads.windows(2).all(|w| w[0] < w[1]) {
628
            return schema("state heads must be sorted and unique");
629
        }
630
        Ok(StateObject { heads })
631
    }
632
}
633
634
#[cfg(test)]
635
pub(crate) mod tests {
636
    use super::*;
637
638
    pub(crate) fn sample_change() -> Change {
639
        let n = NodeId([7; 16]);
640
        let dep = ObjectRef([1; 32]);
641
        let mut c = Change {
642
            change_id: ChangeId([9; 16]),
643
            deps: vec![ObjectRef([2; 32]), dep],
644
            author: ObjectRef([3; 32]),
645
            hlc: Hlc { wall_ms: 1_700_000_000_000, logical: 4 },
646
            message: "Add greeting".into(),
647
            tree_ops: vec![
648
                TreeOp::AddNode { node: n, kind: NodeKind::File, mode: Mode::TEXT, parent: NodeId::ROOT, name: "hello.txt".into() },
649
                TreeOp::SetMode { node: n, old: OpRef { change: ObjectRef::ZERO, index: 0 }, mode: Mode { exec: true, opaque: false } },
650
            ],
651
            edits: vec![
652
                Edit::Insert { node: n, after: AtomRef::Start, before: AtomRef::End, lines: vec![b"hello\n".to_vec(), b"world\n".to_vec()] },
653
                Edit::Delete { node: n, atoms: vec![AtomRange { change: dep, from: 0, to: 2 }] },
654
                Edit::Replace { node: n, old: None, new: Some(ObjectRef([5; 32])) },
655
            ],
656
            kind: ChangeKind::Normal,
657
        };
658
        c.normalize();
659
        c
660
    }
661
662
    #[test]
663
    fn change_round_trips_and_hash_is_stable() {
664
        let c = sample_change();
665
        let bytes = c.encode();
666
        let d = Change::decode(&bytes).unwrap();
667
        assert_eq!(c, d);
668
        assert_eq!(c.hash(), d.hash());
669
        assert_eq!(d.encode(), bytes);
670
        // Any semantic difference changes the hash.
671
        let mut e = c.clone();
672
        e.message.push('!');
673
        assert_ne!(e.hash(), c.hash());
674
    }
675
676
    #[test]
677
    fn decode_rejects_bad_changes() {
678
        let c = sample_change();
679
        let mut v = c.to_value();
680
        if let Value::Map(m) = &mut v {
681
            m.insert(0, Value::Uint(2));
682
        }
683
        assert!(matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("version")));
684
685
        let mut v = c.to_value();
686
        if let Value::Map(m) = &mut v {
687
            m.insert(2, Value::Array(vec![Value::Bytes(vec![2; 32]), Value::Bytes(vec![1; 32])]));
688
        }
689
        assert!(matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("sorted")));
690
691
        let mut v = c.to_value();
692
        if let Value::Map(m) = &mut v {
693
            m.insert(9, Value::map([(0, Value::Null)]));
694
        }
695
        assert!(matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("extra")));
696
    }
697
698
    #[test]
699
    fn names_are_validated() {
700
        for bad in ["", ".", "..", "a/b", "a\0b"] {
701
            assert!(validate_name(bad).is_err(), "{bad:?}");
702
        }
703
        assert!(validate_name("Foo.c").is_ok());
704
        assert!(validate_name("...").is_ok());
705
    }
706
707
    #[test]
708
    fn mode_and_kind_tags() {
709
        for m in [Mode::TEXT, Mode::OPAQUE, Mode { exec: true, opaque: true }] {
710
            assert_eq!(Mode::from_bits(m.bits()).unwrap(), m);
711
        }
712
        assert!(Mode::from_bits(4).is_err());
713
        for k in [NodeKind::File, NodeKind::Dir, NodeKind::Symlink, NodeKind::Subrepo] {
714
            assert_eq!(NodeKind::from_tag(k.tag()).unwrap(), k);
715
        }
716
    }
717
718
    #[test]
719
    fn self_references_resolve() {
720
        let me = ObjectRef([8; 32]);
721
        assert_eq!(AtomRef::Atom { change: ObjectRef::ZERO, index: 3 }.resolve(me), AtomRef::Atom { change: me, index: 3 });
722
        assert_eq!(AtomRef::Start.resolve(me), AtomRef::Start);
723
        let other = ObjectRef([1; 32]);
724
        assert_eq!(OpRef { change: other, index: 1 }.resolve(me).change, other);
725
    }
726
727
    #[test]
728
    fn state_object_round_trip() {
729
        let st = StateObject::new(vec![ObjectRef([2; 32]), ObjectRef([1; 32]), ObjectRef([2; 32])]);
730
        assert_eq!(st.heads, vec![ObjectRef([1; 32]), ObjectRef([2; 32])]);
731
        assert_eq!(StateObject::decode(&st.encode()).unwrap(), st);
732
        assert_eq!(StateObject::new(vec![]).heads, vec![]);
733
        assert_ne!(StateObject::new(vec![]).hash(), st.hash());
734
    }
735
736
    #[test]
737
    fn blob_round_trip() {
738
        let b = Blob { length: 10, chunks: vec![ObjectRef([1; 32]), ObjectRef([2; 32])] };
739
        assert_eq!(Blob::decode(&b.encode()).unwrap(), b);
740
    }
741
}