OXYGENHUB
# e10ba80000e1 oxygen / crates / oxy-core / src / objects.rs
56.2 KiB · Rust · last changed Read a state index's file histories only when used 26 days ago source blame history raw
ff0af1ee unknown 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};
01a47cd7 Andy McCall ↶ 13
use crate::lines::Lines;
ff0af1ee unknown 14
use std::collections::BTreeMap;
15
16
pub const FORMAT_VERSION: u64 = 1;
17
6f8aeb99 Andy McCall ↶ 18
/// The change format writers produce (spec §6.1). Version 2 stores each
0f93f49c Andy McCall ↶ 19
/// large insert's lines as a text blob holding a CBOR array of them;
20
/// version 3 holds the lines' bytes themselves when splitting them by the
21
/// line rule gives them back. Readers accept 1 to 3, and a change keeps
22
/// the version it was made in, so its hash never moves.
23
pub const CHANGE_VERSION: u64 = 3;
6f8aeb99 Andy McCall ↶ 24
0f93f49c Andy McCall ↶ 25
/// How a text blob holds an insert's lines (spec §7.4).
26
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27
pub enum TextForm {
28
    /// A CBOR array of the lines' byte strings: version 2.
29
    Cbor,
30
    /// The lines' bytes, split again by the line rule: version 3.
31
    Raw,
32
    /// A CBOR array in a version 3 change, for lines the line rule would
33
    /// not split back (marked by key 7): never for lines it would.
34
    CborFallback,
35
}
36
6f8aeb99 Andy McCall ↶ 37
/// In a version 2 change, an insert whose lines encode (as a CBOR array of
38
/// byte strings) to at least this many bytes is stored as a text blob
39
/// holding exactly that encoding; a shorter one is inline (spec §6.1).
40
pub const TEXT_BLOB_MIN: usize = 4096;
41
ff0af1ee unknown 42
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
43
pub enum ObjectError {
44
    #[error("cbor: {0}")]
45
    Cbor(#[from] cbor::CborError),
46
    #[error("schema: {0}")]
47
    Schema(String),
5552e69c Andy McCall ↶ 48
    /// A cache this repository derived and can derive again (a state
49
    /// index) does not match the hash it was written with.
50
    #[error("damaged index: {0}")]
51
    DamagedIndex(String),
ff0af1ee unknown 52
}
53
54
pub(crate) fn schema<T>(msg: impl Into<String>) -> Result<T, ObjectError> {
55
    Err(ObjectError::Schema(msg.into()))
56
}
57
58
pub(crate) type Map = BTreeMap<u64, Value>;
59
60
pub(crate) fn field<'a>(m: &'a Map, k: u64, what: &str) -> Result<&'a Value, ObjectError> {
75167672 Andy McCall 61
    m.get(&k)
62
        .ok_or_else(|| ObjectError::Schema(format!("missing field {k} ({what})")))
ff0af1ee unknown 63
}
64
pub(crate) fn uint(v: &Value, what: &str) -> Result<u64, ObjectError> {
75167672 Andy McCall 65
    v.as_uint()
66
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected unsigned integer")))
ff0af1ee unknown 67
}
68
pub(crate) fn u32_(v: &Value, what: &str) -> Result<u32, ObjectError> {
69
    u32::try_from(uint(v, what)?).map_err(|_| ObjectError::Schema(format!("{what}: exceeds u32")))
70
}
71
pub(crate) fn text<'a>(v: &'a Value, what: &str) -> Result<&'a str, ObjectError> {
75167672 Andy McCall 72
    v.as_text()
73
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected text")))
ff0af1ee unknown 74
}
75
pub(crate) fn array<'a>(v: &'a Value, what: &str) -> Result<&'a [Value], ObjectError> {
75167672 Andy McCall 76
    v.as_array()
77
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected array")))
ff0af1ee unknown 78
}
79
pub(crate) fn map<'a>(v: &'a Value, what: &str) -> Result<&'a Map, ObjectError> {
75167672 Andy McCall 80
    v.as_map()
81
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected map")))
ff0af1ee unknown 82
}
83
pub(crate) fn bytes<'a>(v: &'a Value, what: &str) -> Result<&'a [u8], ObjectError> {
75167672 Andy McCall 84
    v.as_bytes()
85
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected bytes")))
ff0af1ee unknown 86
}
87
pub(crate) fn oref(v: &Value, what: &str) -> Result<ObjectRef, ObjectError> {
75167672 Andy McCall 88
    ObjectRef::from_slice(bytes(v, what)?)
89
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected 32 bytes")))
ff0af1ee unknown 90
}
91
pub(crate) fn node_id(v: &Value, what: &str) -> Result<NodeId, ObjectError> {
75167672 Andy McCall 92
    NodeId::from_slice(bytes(v, what)?)
93
        .ok_or_else(|| ObjectError::Schema(format!("{what}: expected 16 bytes")))
ff0af1ee unknown 94
}
95
pub(crate) fn check_version(m: &Map) -> Result<(), ObjectError> {
96
    let v = uint(field(m, 0, "version")?, "version")?;
97
    if v != FORMAT_VERSION {
14edbaaf Andy McCall ↶ 98
        let exe = std::env::current_exe()
99
            .map(|p| format!(" at {}", p.display()))
100
            .unwrap_or_default();
101
        let which = format!("oxy {}{exe}", crate::VERSION);
102
        return schema(if v > FORMAT_VERSION {
103
            format!(
104
                "this data is format {v}, written by a newer oxy, and {which} reads format {FORMAT_VERSION}: install the current oxy, and check that no older copy comes first on PATH"
105
            )
106
        } else {
107
            format!(
108
                "this data is format {v}, written by an older oxy, and {which} reads format {FORMAT_VERSION} only"
109
            )
110
        });
ff0af1ee unknown 111
    }
112
    Ok(())
113
}
114
115
// ---------------------------------------------------------------- Hlc
116
117
/// Hybrid logical clock (spec §6.4).
118
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
119
pub struct Hlc {
120
    pub wall_ms: u64,
121
    pub logical: u32,
122
}
123
124
impl Hlc {
125
    pub fn now() -> Hlc {
126
        let wall_ms = std::time::SystemTime::now()
127
            .duration_since(std::time::UNIX_EPOCH)
128
            .map(|d| d.as_millis() as u64)
129
            .unwrap_or(0);
75167672 Andy McCall 130
        Hlc {
131
            wall_ms,
132
            logical: 0,
133
        }
ff0af1ee unknown 134
    }
135
    pub(crate) fn to_value(self) -> Value {
75167672 Andy McCall 136
        Value::map([
137
            (0, Value::Uint(self.wall_ms)),
138
            (1, Value::Uint(self.logical as u64)),
139
        ])
ff0af1ee unknown 140
    }
141
    pub(crate) fn from_value(v: &Value) -> Result<Hlc, ObjectError> {
142
        let m = map(v, "hlc")?;
143
        Ok(Hlc {
144
            wall_ms: uint(field(m, 0, "hlc.wall_ms")?, "hlc.wall_ms")?,
145
            logical: u32_(field(m, 1, "hlc.logical")?, "hlc.logical")?,
146
        })
147
    }
148
}
149
150
// ---------------------------------------------------------------- tree
151
152
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153
pub enum NodeKind {
154
    File,
155
    Dir,
156
    Symlink,
4dee277c Andy McCall ↶ 157
    /// A nested repository pinned to a state (spec §7.8); content is a pin.
158
    Subrepo,
ff0af1ee unknown 159
}
160
161
impl NodeKind {
252e8cb2 Andy McCall ↶ 162
    pub(crate) fn tag(self) -> u64 {
ff0af1ee unknown 163
        match self {
164
            NodeKind::File => 0,
165
            NodeKind::Dir => 1,
166
            NodeKind::Symlink => 2,
4dee277c Andy McCall ↶ 167
            NodeKind::Subrepo => 3,
ff0af1ee unknown 168
        }
169
    }
252e8cb2 Andy McCall ↶ 170
    pub(crate) fn from_tag(t: u64) -> Result<NodeKind, ObjectError> {
ff0af1ee unknown 171
        Ok(match t {
172
            0 => NodeKind::File,
173
            1 => NodeKind::Dir,
174
            2 => NodeKind::Symlink,
4dee277c Andy McCall ↶ 175
            3 => NodeKind::Subrepo,
ff0af1ee unknown 176
            _ => return schema(format!("unknown node kind {t}")),
177
        })
178
    }
179
}
180
181
/// Executable bit plus content mode (spec §7.1, §7.5).
182
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
183
pub struct Mode {
184
    pub exec: bool,
185
    pub opaque: bool,
186
}
187
188
impl Mode {
75167672 Andy McCall 189
    pub const TEXT: Mode = Mode {
190
        exec: false,
191
        opaque: false,
192
    };
193
    pub const OPAQUE: Mode = Mode {
194
        exec: false,
195
        opaque: true,
196
    };
252e8cb2 Andy McCall ↶ 197
    pub(crate) fn bits(self) -> u64 {
ff0af1ee unknown 198
        (self.exec as u64) | ((self.opaque as u64) << 1)
199
    }
252e8cb2 Andy McCall ↶ 200
    pub(crate) fn from_bits(b: u64) -> Result<Mode, ObjectError> {
ff0af1ee unknown 201
        if b > 3 {
202
            return schema(format!("unknown mode bits {b}"));
203
        }
75167672 Andy McCall 204
        Ok(Mode {
205
            exec: b & 1 != 0,
206
            opaque: b & 2 != 0,
207
        })
ff0af1ee unknown 208
    }
209
}
210
211
/// Reference to a tree operation of some change: the placement or mode atom it
212
/// introduced. `change == ObjectRef::ZERO` means this change.
213
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
214
pub struct OpRef {
215
    pub change: ChangeHash,
216
    pub index: u32,
217
}
218
219
impl OpRef {
220
    fn to_value(self) -> Value {
75167672 Andy McCall 221
        Value::Array(vec![
222
            Value::Bytes(self.change.0.to_vec()),
223
            Value::Uint(self.index as u64),
224
        ])
ff0af1ee unknown 225
    }
226
    fn from_value(v: &Value, what: &str) -> Result<OpRef, ObjectError> {
227
        let a = array(v, what)?;
228
        if a.len() != 2 {
229
            return schema(format!("{what}: expected [hash, index]"));
230
        }
75167672 Andy McCall 231
        Ok(OpRef {
232
            change: oref(&a[0], what)?,
233
            index: u32_(&a[1], what)?,
234
        })
ff0af1ee unknown 235
    }
236
    /// Replace a self reference with the concrete change hash.
237
    pub fn resolve(self, me: ChangeHash) -> OpRef {
75167672 Andy McCall 238
        if self.change.is_zero() {
239
            OpRef { change: me, ..self }
240
        } else {
241
            self
242
        }
ff0af1ee unknown 243
    }
244
}
245
246
#[derive(Debug, Clone, PartialEq, Eq)]
247
pub enum TreeOp {
75167672 Andy McCall 248
    AddNode {
249
        node: NodeId,
250
        kind: NodeKind,
251
        mode: Mode,
252
        parent: NodeId,
253
        name: String,
254
    },
255
    RemoveNode {
256
        node: NodeId,
257
        placement: OpRef,
258
    },
259
    Move {
260
        node: NodeId,
261
        old: OpRef,
262
        parent: NodeId,
263
        name: String,
264
    },
265
    SetMode {
266
        node: NodeId,
267
        old: OpRef,
268
        mode: Mode,
269
    },
ff0af1ee unknown 270
}
271
272
/// Names must be valid, non-empty, not `.`/`..`, and free of `/` and NUL (spec §7.1).
273
pub fn validate_name(name: &str) -> Result<(), ObjectError> {
274
    if name.is_empty() || name == "." || name == ".." || name.contains('/') || name.contains('\0') {
275
        return schema(format!("invalid placement name {name:?}"));
276
    }
277
    Ok(())
278
}
279
280
impl TreeOp {
281
    pub fn node(&self) -> NodeId {
282
        match self {
283
            TreeOp::AddNode { node, .. }
284
            | TreeOp::RemoveNode { node, .. }
285
            | TreeOp::Move { node, .. }
286
            | TreeOp::SetMode { node, .. } => *node,
287
        }
288
    }
289
290
    fn to_value(&self) -> Value {
291
        let nid = |n: &NodeId| Value::Bytes(n.0.to_vec());
292
        match self {
75167672 Andy McCall 293
            TreeOp::AddNode {
294
                node,
295
                kind,
296
                mode,
297
                parent,
298
                name,
299
            } => Value::map([
ff0af1ee unknown 300
                (0, Value::Uint(0)),
301
                (1, nid(node)),
302
                (2, Value::Uint(kind.tag())),
303
                (3, Value::Uint(mode.bits())),
304
                (4, nid(parent)),
305
                (5, Value::Text(name.clone())),
306
            ]),
75167672 Andy McCall 307
            TreeOp::RemoveNode { node, placement } => Value::map([
308
                (0, Value::Uint(1)),
309
                (1, nid(node)),
310
                (2, placement.to_value()),
311
            ]),
312
            TreeOp::Move {
313
                node,
314
                old,
315
                parent,
316
                name,
317
            } => Value::map([
ff0af1ee unknown 318
                (0, Value::Uint(2)),
319
                (1, nid(node)),
320
                (2, old.to_value()),
321
                (4, nid(parent)),
322
                (5, Value::Text(name.clone())),
323
            ]),
324
            TreeOp::SetMode { node, old, mode } => Value::map([
325
                (0, Value::Uint(3)),
326
                (1, nid(node)),
327
                (2, old.to_value()),
328
                (3, Value::Uint(mode.bits())),
329
            ]),
330
        }
331
    }
332
333
    fn from_value(v: &Value) -> Result<TreeOp, ObjectError> {
334
        let m = map(v, "tree_op")?;
335
        let tag = uint(field(m, 0, "tree_op tag")?, "tree_op tag")?;
336
        let node = node_id(field(m, 1, "tree_op.node")?, "tree_op.node")?;
337
        let op = match tag {
338
            0 => {
339
                let name = text(field(m, 5, "name")?, "name")?.to_owned();
340
                validate_name(&name)?;
341
                TreeOp::AddNode {
342
                    node,
343
                    kind: NodeKind::from_tag(uint(field(m, 2, "kind")?, "kind")?)?,
344
                    mode: Mode::from_bits(uint(field(m, 3, "mode")?, "mode")?)?,
345
                    parent: node_id(field(m, 4, "parent")?, "parent")?,
346
                    name,
347
                }
348
            }
75167672 Andy McCall 349
            1 => TreeOp::RemoveNode {
350
                node,
351
                placement: OpRef::from_value(field(m, 2, "placement")?, "placement")?,
352
            },
ff0af1ee unknown 353
            2 => {
354
                let name = text(field(m, 5, "name")?, "name")?.to_owned();
355
                validate_name(&name)?;
356
                TreeOp::Move {
357
                    node,
358
                    old: OpRef::from_value(field(m, 2, "old")?, "old")?,
359
                    parent: node_id(field(m, 4, "parent")?, "parent")?,
360
                    name,
361
                }
362
            }
363
            3 => TreeOp::SetMode {
364
                node,
365
                old: OpRef::from_value(field(m, 2, "old")?, "old")?,
366
                mode: Mode::from_bits(uint(field(m, 3, "mode")?, "mode")?)?,
367
            },
368
            _ => return schema(format!("unknown tree op tag {tag}")),
369
        };
370
        if node.is_root() {
371
            return schema("tree op on the root node");
372
        }
373
        Ok(op)
374
    }
375
}
376
377
// ---------------------------------------------------------------- edits
378
379
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
380
pub enum AtomRef {
381
    Start,
382
    End,
383
    Atom { change: ChangeHash, index: u32 },
384
}
385
386
impl AtomRef {
387
    fn to_value(self) -> Value {
388
        match self {
389
            AtomRef::Start => Value::Uint(0),
390
            AtomRef::End => Value::Uint(1),
75167672 Andy McCall 391
            AtomRef::Atom { change, index } => Value::Array(vec![
392
                Value::Bytes(change.0.to_vec()),
393
                Value::Uint(index as u64),
394
            ]),
ff0af1ee unknown 395
        }
396
    }
397
    fn from_value(v: &Value, what: &str) -> Result<AtomRef, ObjectError> {
398
        match v {
399
            Value::Uint(0) => Ok(AtomRef::Start),
400
            Value::Uint(1) => Ok(AtomRef::End),
75167672 Andy McCall 401
            Value::Array(a) if a.len() == 2 => Ok(AtomRef::Atom {
402
                change: oref(&a[0], what)?,
403
                index: u32_(&a[1], what)?,
404
            }),
ff0af1ee unknown 405
            _ => schema(format!("{what}: expected atom reference")),
406
        }
407
    }
408
    pub fn resolve(self, me: ChangeHash) -> AtomRef {
409
        match self {
75167672 Andy McCall 410
            AtomRef::Atom { change, index } if change.is_zero() => {
411
                AtomRef::Atom { change: me, index }
412
            }
ff0af1ee unknown 413
            other => other,
414
        }
415
    }
416
}
417
418
/// Half-open range of atoms introduced by one change.
419
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
420
pub struct AtomRange {
421
    pub change: ChangeHash,
422
    pub from: u32,
423
    pub to: u32,
424
}
425
426
impl AtomRange {
427
    fn to_value(self) -> Value {
428
        Value::Array(vec![
429
            Value::Bytes(self.change.0.to_vec()),
430
            Value::Uint(self.from as u64),
431
            Value::Uint(self.to as u64),
432
        ])
433
    }
434
    fn from_value(v: &Value) -> Result<AtomRange, ObjectError> {
435
        let a = array(v, "atom range")?;
436
        if a.len() != 3 {
437
            return schema("atom range: expected [hash, from, to]");
438
        }
75167672 Andy McCall 439
        let r = AtomRange {
440
            change: oref(&a[0], "range.change")?,
441
            from: u32_(&a[1], "range.from")?,
442
            to: u32_(&a[2], "range.to")?,
443
        };
ff0af1ee unknown 444
        if r.from >= r.to {
445
            return schema("atom range: empty or inverted");
446
        }
447
        Ok(r)
448
    }
449
    pub fn resolve(self, me: ChangeHash) -> AtomRange {
75167672 Andy McCall 450
        if self.change.is_zero() {
451
            AtomRange { change: me, ..self }
452
        } else {
453
            self
454
        }
ff0af1ee unknown 455
    }
456
}
457
458
#[derive(Debug, Clone, PartialEq, Eq)]
459
pub enum Edit {
75167672 Andy McCall 460
    Insert {
461
        node: NodeId,
462
        after: AtomRef,
463
        before: AtomRef,
01a47cd7 Andy McCall ↶ 464
        lines: crate::lines::Lines,
75167672 Andy McCall 465
    },
466
    Delete {
467
        node: NodeId,
468
        atoms: Vec<AtomRange>,
469
    },
470
    Replace {
471
        node: NodeId,
472
        old: Option<ObjectRef>,
473
        new: Option<ObjectRef>,
474
    },
ff0af1ee unknown 475
}
476
477
impl Edit {
478
    pub fn node(&self) -> NodeId {
479
        match self {
75167672 Andy McCall 480
            Edit::Insert { node, .. } | Edit::Delete { node, .. } | Edit::Replace { node, .. } => {
481
                *node
482
            }
ff0af1ee unknown 483
        }
6f8aeb99 Andy McCall ↶ 484
    }
485
486
    /// Whether this insert is stored as a text blob in a change of `version`.
487
    fn stored_text(&self, version: u64) -> bool {
488
        matches!(self, Edit::Insert { lines, .. }
489
            if version >= 2 && cbor::byte_strings_len(lines) >= TEXT_BLOB_MIN)
ff0af1ee unknown 490
    }
491
192fad1e Andy McCall ↶ 492
    /// The edit's value; `stored` is the text blob of an insert stored as
0f93f49c Andy McCall ↶ 493
    /// one, and its form (see [`Change::value_with`], which works them out).
5b4c5130 Andy McCall ↶ 494
    /// The edit's value; `compact` writes an inline insert's lines as
495
    /// their encoding rather than a value per line (same bytes encoded).
496
    fn to_value(&self, stored: Option<(ObjectRef, TextForm)>, compact: bool) -> Value {
ff0af1ee unknown 497
        let nid = |n: &NodeId| Value::Bytes(n.0.to_vec());
498
        let opt = |o: &Option<ObjectRef>| match o {
499
            Some(r) => Value::Bytes(r.0.to_vec()),
500
            None => Value::Null,
501
        };
192fad1e Andy McCall ↶ 502
        match (self, stored) {
503
            (
504
                Edit::Insert {
505
                    node,
506
                    after,
507
                    before,
508
                    lines,
509
                },
0f93f49c Andy McCall ↶ 510
                Some((r, form)),
511
            ) => {
512
                let mut fields = vec![
513
                    (0, Value::Uint(0)),
514
                    (1, nid(node)),
515
                    (2, after.to_value()),
516
                    (3, before.to_value()),
517
                    (5, Value::Bytes(r.0.to_vec())),
518
                    (6, Value::Uint(lines.len() as u64)),
519
                ];
520
                if form == TextForm::CborFallback {
521
                    fields.push((7, Value::Uint(1)));
522
                }
523
                Value::map(fields)
524
            }
192fad1e Andy McCall ↶ 525
            (
526
                Edit::Insert {
527
                    node,
528
                    after,
529
                    before,
530
                    lines,
531
                },
532
                None,
533
            ) => Value::map([
534
                (0, Value::Uint(0)),
535
                (1, nid(node)),
536
                (2, after.to_value()),
537
                (3, before.to_value()),
75167672 Andy McCall 538
                (
539
                    4,
5b4c5130 Andy McCall ↶ 540
                    if compact {
541
                        Value::Encoded(cbor::encode_byte_strings(lines))
542
                    } else {
543
                        Value::Array(lines.iter().map(|l| Value::Bytes(l.to_vec())).collect())
544
                    },
75167672 Andy McCall 545
                ),
ff0af1ee unknown 546
            ]),
192fad1e Andy McCall ↶ 547
            (Edit::Delete { node, atoms }, _) => Value::map([
ff0af1ee unknown 548
                (0, Value::Uint(1)),
549
                (1, nid(node)),
75167672 Andy McCall 550
                (
551
                    2,
552
                    Value::Array(atoms.iter().map(|r| r.to_value()).collect()),
553
                ),
ff0af1ee unknown 554
            ]),
192fad1e Andy McCall ↶ 555
            (Edit::Replace { node, old, new }, _) => Value::map([
75167672 Andy McCall 556
                (0, Value::Uint(2)),
557
                (1, nid(node)),
558
                (2, opt(old)),
559
                (3, opt(new)),
560
            ]),
ff0af1ee unknown 561
        }
562
    }
563
6f8aeb99 Andy McCall ↶ 564
    fn from_value(v: &Value, version: u64, resolve: &mut Resolver) -> Result<Edit, ObjectError> {
ff0af1ee unknown 565
        let m = map(v, "edit")?;
566
        let tag = uint(field(m, 0, "edit tag")?, "edit tag")?;
567
        let node = node_id(field(m, 1, "edit.node")?, "edit.node")?;
568
        let opt = |v: &Value, what: &str| -> Result<Option<ObjectRef>, ObjectError> {
75167672 Andy McCall 569
            if v.is_null() {
570
                Ok(None)
571
            } else {
572
                Ok(Some(oref(v, what)?))
573
            }
ff0af1ee unknown 574
        };
575
        Ok(match tag {
576
            0 => {
6f8aeb99 Andy McCall ↶ 577
                let lines = match (m.get(&4), m.get(&5)) {
578
                    (Some(inline), None) => {
01a47cd7 Andy McCall ↶ 579
                        let lines = array_lines(inline)?;
6f8aeb99 Andy McCall ↶ 580
                        if version >= 2 && cbor::byte_strings_len(&lines) >= TEXT_BLOB_MIN {
581
                            return schema("an insert this large is stored as a text blob in a version 2 change");
582
                        }
86e1824f Andy McCall ↶ 583
                        if lines.is_empty() {
584
                            return schema("insert with no lines");
585
                        }
6f8aeb99 Andy McCall ↶ 586
                        lines
587
                    }
588
                    (None, Some(text)) if version >= 2 => {
589
                        let text = oref(text, "insert text")?;
590
                        let count = u32_(field(m, 6, "insert line count")?, "insert line count")?;
591
                        if count == 0 {
592
                            return schema("insert with no lines");
593
                        }
0f93f49c Andy McCall ↶ 594
                        let form = match (version, m.get(&7)) {
595
                            (2, None) => TextForm::Cbor,
596
                            (_, None) => TextForm::Raw,
597
                            (3.., Some(Value::Uint(1))) => TextForm::CborFallback,
598
                            _ => return schema("unknown text blob form"),
599
                        };
6f8aeb99 Andy McCall ↶ 600
                        // The resolver checks the count (`text_lines`).
0f93f49c Andy McCall ↶ 601
                        resolve(&text, count, form)?
6f8aeb99 Andy McCall ↶ 602
                    }
603
                    _ => {
604
                        return schema(
605
                            "an insert has either its lines or, in a version 2 change, a text blob",
606
                        )
607
                    }
608
                };
ff0af1ee unknown 609
                Edit::Insert {
610
                    node,
611
                    after: AtomRef::from_value(field(m, 2, "after")?, "after")?,
612
                    before: AtomRef::from_value(field(m, 3, "before")?, "before")?,
613
                    lines,
614
                }
615
            }
616
            1 => {
617
                let atoms = array(field(m, 2, "atoms")?, "atoms")?
618
                    .iter()
619
                    .map(AtomRange::from_value)
620
                    .collect::<Result<Vec<_>, _>>()?;
621
                if atoms.is_empty() {
622
                    return schema("delete with no atoms");
623
                }
624
                Edit::Delete { node, atoms }
625
            }
75167672 Andy McCall 626
            2 => Edit::Replace {
627
                node,
628
                old: opt(field(m, 2, "old")?, "old")?,
629
                new: opt(field(m, 3, "new")?, "new")?,
630
            },
ff0af1ee unknown 631
            _ => return schema(format!("unknown edit tag {tag}")),
632
        })
633
    }
634
}
635
636
// ---------------------------------------------------------------- change
637
638
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
639
pub enum ChangeKind {
640
    Normal,
641
    Resolution,
642
    WorkingCopy,
643
}
644
645
impl ChangeKind {
252e8cb2 Andy McCall ↶ 646
    pub(crate) fn tag(self) -> u64 {
ff0af1ee unknown 647
        match self {
648
            ChangeKind::Normal => 0,
649
            ChangeKind::Resolution => 1,
650
            ChangeKind::WorkingCopy => 2,
651
        }
652
    }
252e8cb2 Andy McCall ↶ 653
    pub(crate) fn from_tag(t: u64) -> Result<ChangeKind, ObjectError> {
ff0af1ee unknown 654
        Ok(match t {
655
            0 => ChangeKind::Normal,
656
            1 => ChangeKind::Resolution,
657
            2 => ChangeKind::WorkingCopy,
658
            _ => return schema(format!("unknown change kind {t}")),
659
        })
660
    }
661
}
662
663
/// A change (spec §6.1). `deps` is kept sorted and unique by `normalize`.
664
#[derive(Debug, Clone, PartialEq, Eq)]
665
pub struct Change {
6f8aeb99 Andy McCall ↶ 666
    /// The format this change is encoded in: [`CHANGE_VERSION`] for a new
667
    /// one, whatever it was made in for one read back.
668
    pub version: u64,
ff0af1ee unknown 669
    pub change_id: ChangeId,
670
    pub deps: Vec<ChangeHash>,
671
    pub author: KeyId,
672
    pub hlc: Hlc,
673
    pub message: String,
674
    pub tree_ops: Vec<TreeOp>,
675
    pub edits: Vec<Edit>,
676
    pub kind: ChangeKind,
677
}
678
679
impl Change {
680
    pub fn normalize(&mut self) {
681
        self.deps.sort();
682
        self.deps.dedup();
683
    }
684
685
    pub fn to_value(&self) -> Value {
5b4c5130 Andy McCall ↶ 686
        self.value_with(&mut |_| true, false)
6f8aeb99 Andy McCall ↶ 687
    }
688
192fad1e Andy McCall ↶ 689
    /// The text blob of each edit stored as one, `None` for the others;
690
    /// the stored texts go to `texts`, in order, when it is given. Worked
691
    /// out on every core, a bounded batch at a time: chunking and hashing
692
    /// a whole tree's text is most of encoding a change that adds it.
0f93f49c Andy McCall ↶ 693
    fn stored_texts(&self, sink: &mut TextSink) -> Vec<Option<(ObjectRef, TextForm)>> {
192fad1e Andy McCall ↶ 694
        use rayon::prelude::*;
0f93f49c Andy McCall ↶ 695
        let mut refs: Vec<Option<(ObjectRef, TextForm)>> = vec![None; self.edits.len()];
192fad1e Andy McCall ↶ 696
        let stored: Vec<usize> = (0..self.edits.len())
697
            .filter(|i| self.edits[*i].stored_text(self.version))
698
            .collect();
5b4c5130 Andy McCall ↶ 699
        // At most 256 texts or 32 MiB of them a batch: some directories
700
        // hold hundreds of generated files of megabytes each, and a batch
701
        // is copied and compressed at once.
702
        let mut batches: Vec<&[usize]> = Vec::new();
703
        let (mut start, mut bytes) = (0, 0usize);
704
        for (k, i) in stored.iter().enumerate() {
705
            if let Edit::Insert { lines, .. } = &self.edits[*i] {
706
                bytes += lines.bytes().len();
707
            }
708
            if k + 1 - start == 256 || bytes >= 32 << 20 {
709
                batches.push(&stored[start..=k]);
710
                (start, bytes) = (k + 1, 0);
711
            }
712
        }
713
        if start < stored.len() {
714
            batches.push(&stored[start..]);
715
        }
716
        for batch in batches {
192fad1e Andy McCall ↶ 717
            let made: Vec<StoredText> = batch
718
                .par_iter()
719
                .map(|i| {
720
                    let Edit::Insert { lines, .. } = &self.edits[*i] else {
721
                        unreachable!("only inserts are stored as text");
0f93f49c Andy McCall ↶ 722
                    };
723
                    let form = text_form(self.version, lines);
724
                    let content = match form {
725
                        TextForm::Raw => lines.bytes().to_vec(),
726
                        TextForm::Cbor | TextForm::CborFallback => cbor::encode_byte_strings(lines),
192fad1e Andy McCall ↶ 727
                    };
728
                    StoredText {
729
                        blob: Blob::of(&content),
730
                        content,
0f93f49c Andy McCall ↶ 731
                        form,
192fad1e Andy McCall ↶ 732
                    }
733
                })
734
                .collect();
17f94b13 Andy McCall ↶ 735
            for (i, t) in batch.iter().zip(&made) {
0f93f49c Andy McCall ↶ 736
                refs[*i] = Some((t.blob.hash(), t.form));
17f94b13 Andy McCall ↶ 737
            }
738
            if !sink(made) {
739
                break;
192fad1e Andy McCall ↶ 740
            }
741
        }
742
        refs
743
    }
744
5b4c5130 Andy McCall ↶ 745
    /// The change's value, its stored texts handed to `sink`; `compact`
746
    /// as [`Edit::to_value`] takes it, for a value that is only encoded.
747
    fn value_with(&self, sink: &mut TextSink, compact: bool) -> Value {
ff0af1ee unknown 748
        Value::map([
6f8aeb99 Andy McCall ↶ 749
            (0, Value::Uint(self.version)),
ff0af1ee unknown 750
            (1, Value::Bytes(self.change_id.0.to_vec())),
75167672 Andy McCall 751
            (
752
                2,
753
                Value::Array(
754
                    self.deps
755
                        .iter()
756
                        .map(|d| Value::Bytes(d.0.to_vec()))
757
                        .collect(),
758
                ),
759
            ),
ff0af1ee unknown 760
            (3, Value::Bytes(self.author.0.to_vec())),
761
            (4, self.hlc.to_value()),
762
            (5, Value::Text(self.message.clone())),
75167672 Andy McCall 763
            (
764
                6,
765
                Value::Array(self.tree_ops.iter().map(|t| t.to_value()).collect()),
766
            ),
767
            (
768
                7,
6f8aeb99 Andy McCall ↶ 769
                Value::Array(
770
                    self.edits
771
                        .iter()
17f94b13 Andy McCall ↶ 772
                        .zip(self.stored_texts(sink))
5b4c5130 Andy McCall ↶ 773
                        .map(|(e, stored)| e.to_value(stored, compact))
6f8aeb99 Andy McCall ↶ 774
                        .collect(),
775
                ),
75167672 Andy McCall 776
            ),
ff0af1ee unknown 777
            (8, Value::Uint(self.kind.tag())),
778
            (9, Value::Map(BTreeMap::new())),
779
        ])
780
    }
781
782
    pub fn encode(&self) -> Vec<u8> {
75167672 Andy McCall 783
        debug_assert!(
784
            self.deps.windows(2).all(|w| w[0] < w[1]),
785
            "deps must be normalized"
786
        );
5b4c5130 Andy McCall ↶ 787
        cbor::encode(&self.value_with(&mut |_| true, true))
ff0af1ee unknown 788
    }
789
6f8aeb99 Andy McCall ↶ 790
    /// The encoding and the text blobs its large inserts are stored as,
791
    /// which must be stored with it.
792
    pub fn encode_parts(&self) -> (Vec<u8>, Vec<StoredText>) {
793
        debug_assert!(
794
            self.deps.windows(2).all(|w| w[0] < w[1]),
795
            "deps must be normalized"
796
        );
797
        let mut texts = Vec::new();
5b4c5130 Andy McCall ↶ 798
        let bytes = cbor::encode(&self.value_with(
799
            &mut |batch| {
800
                texts.extend(batch);
801
                true
802
            },
803
            true,
804
        ));
6f8aeb99 Andy McCall ↶ 805
        (bytes, texts)
17f94b13 Andy McCall ↶ 806
    }
807
808
    /// The encoding, handing the stored texts to `store` a batch at a time
809
    /// as they are made, so a change adding a whole tree never holds all
810
    /// its text twice. The first error from `store` is returned.
811
    pub fn encode_streaming<E>(
812
        &self,
813
        mut store: impl FnMut(Vec<StoredText>) -> Result<(), E>,
814
    ) -> Result<Vec<u8>, E> {
815
        debug_assert!(
816
            self.deps.windows(2).all(|w| w[0] < w[1]),
817
            "deps must be normalized"
818
        );
819
        let mut failed = None;
5b4c5130 Andy McCall ↶ 820
        let value = self.value_with(
821
            &mut |batch| match store(batch) {
822
                Ok(()) => true,
823
                Err(e) => {
824
                    failed = Some(e);
825
                    false
826
                }
827
            },
828
            true,
829
        );
17f94b13 Andy McCall ↶ 830
        match failed {
831
            Some(e) => Err(e),
832
            None => Ok(cbor::encode(&value)),
833
        }
6f8aeb99 Andy McCall ↶ 834
    }
835
836
    /// Whether any insert is stored as a text blob.
837
    pub fn has_stored_text(&self) -> bool {
838
        self.edits.iter().any(|e| e.stored_text(self.version))
839
    }
840
ff0af1ee unknown 841
    pub fn hash(&self) -> ChangeHash {
842
        hash_object(ObjectType::Change, &self.encode())
843
    }
844
6f8aeb99 Andy McCall ↶ 845
    /// Decode a change that stores no text blob; one that does needs
846
    /// [`Change::decode_with`] and something to read them from.
ff0af1ee unknown 847
    pub fn decode(bytes: &[u8]) -> Result<Change, ObjectError> {
0f93f49c Andy McCall ↶ 848
        Self::decode_with(bytes, &mut |r, _, _| {
6f8aeb99 Andy McCall ↶ 849
            schema(format!(
850
                "text blob {} needs a store to read it from",
851
                r.short(12)
852
            ))
853
        })
854
    }
855
856
    /// Decode a change, reading each stored insert's lines with `resolve`
857
    /// (given the text blob and the line count the insert claims).
858
    pub fn decode_with(bytes: &[u8], resolve: &mut Resolver) -> Result<Change, ObjectError> {
ff0af1ee unknown 859
        let v = cbor::decode(bytes)?;
6f8aeb99 Andy McCall ↶ 860
        Self::from_value_with(&v, resolve)
ff0af1ee unknown 861
    }
862
863
    pub fn from_value(v: &Value) -> Result<Change, ObjectError> {
0f93f49c Andy McCall ↶ 864
        Self::from_value_with(v, &mut |r, _, _| {
6f8aeb99 Andy McCall ↶ 865
            schema(format!(
866
                "text blob {} needs a store to read it from",
867
                r.short(12)
868
            ))
869
        })
870
    }
871
a4fad5b0 Andy McCall ↶ 872
    pub(crate) fn from_value_with(
873
        v: &Value,
874
        resolve: &mut Resolver,
875
    ) -> Result<Change, ObjectError> {
ff0af1ee unknown 876
        let m = map(v, "change")?;
6f8aeb99 Andy McCall ↶ 877
        let version = uint(field(m, 0, "version")?, "version")?;
878
        if !(1..=CHANGE_VERSION).contains(&version) {
879
            return schema(format!("unsupported change format version {version}"));
880
        }
ff0af1ee unknown 881
        let change_id = ChangeId::from_slice(bytes(field(m, 1, "change_id")?, "change_id")?)
882
            .ok_or_else(|| ObjectError::Schema("change_id: expected 16 bytes".into()))?;
883
        let deps = array(field(m, 2, "deps")?, "deps")?
884
            .iter()
885
            .map(|d| oref(d, "dep"))
886
            .collect::<Result<Vec<_>, _>>()?;
887
        if !deps.windows(2).all(|w| w[0] < w[1]) {
888
            return schema("deps must be sorted and unique");
889
        }
890
        if deps.iter().any(|d| d.is_zero()) {
891
            return schema("deps may not contain the self reference");
892
        }
893
        let extra = map(field(m, 9, "extra")?, "extra")?;
894
        if !extra.is_empty() {
895
            return schema("extra must be empty in format version 1");
896
        }
897
        Ok(Change {
6f8aeb99 Andy McCall ↶ 898
            version,
ff0af1ee unknown 899
            change_id,
900
            deps,
901
            author: oref(field(m, 3, "author")?, "author")?,
902
            hlc: Hlc::from_value(field(m, 4, "hlc")?)?,
903
            message: text(field(m, 5, "message")?, "message")?.to_owned(),
904
            tree_ops: array(field(m, 6, "tree_ops")?, "tree_ops")?
905
                .iter()
906
                .map(TreeOp::from_value)
907
                .collect::<Result<Vec<_>, _>>()?,
908
            edits: array(field(m, 7, "edits")?, "edits")?
909
                .iter()
6f8aeb99 Andy McCall ↶ 910
                .map(|e| Edit::from_value(e, version, resolve))
ff0af1ee unknown 911
                .collect::<Result<Vec<_>, _>>()?,
912
            kind: ChangeKind::from_tag(uint(field(m, 8, "kind")?, "kind")?)?,
913
        })
914
    }
915
}
916
6f8aeb99 Andy McCall ↶ 917
/// Reads a stored insert's lines, given the text blob and the line count
918
/// the insert claims; [`text_lines`] reads and checks a blob's content.
86e1824f Andy McCall ↶ 919
/// A resolver may also return no lines, leaving a placeholder for a reader
920
/// that fills it in later (history-only states, spec §11).
0f93f49c Andy McCall ↶ 921
pub type Resolver<'a> = dyn FnMut(&ObjectRef, u32, TextForm) -> Result<Lines, ObjectError> + 'a;
17f94b13 Andy McCall ↶ 922
923
/// Receives the stored texts of a change being encoded, a batch at a
924
/// time; returns false to stop making more.
925
type TextSink<'a> = dyn FnMut(Vec<StoredText>) -> bool + 'a;
a4fad5b0 Andy McCall ↶ 926
927
/// Where a history-only load finds an insert's lines when it builds that
928
/// file's graph (spec §11): it keeps no line until then.
929
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
930
pub enum TextRef {
0f93f49c Andy McCall ↶ 931
    /// A stored insert: its text blob, line count, and the blob's form.
932
    Blob(ObjectRef, u32, TextForm),
a4fad5b0 Andy McCall ↶ 933
    /// An inline insert: the byte range of its lines' array in the
934
    /// encoding of `change`.
935
    Inline {
936
        change: ChangeHash,
937
        start: u32,
938
        end: u32,
939
    },
940
}
941
942
/// The lines of an inline insert from its array's bytes (see
943
/// [`TextRef::Inline`]).
01a47cd7 Andy McCall ↶ 944
pub fn inline_lines(array: &[u8]) -> Result<Lines, ObjectError> {
a4fad5b0 Andy McCall ↶ 945
    let v = cbor::decode(array)?;
946
    let lines = array_lines(&v)?;
947
    if lines.is_empty() {
948
        return schema("insert with no lines");
949
    }
950
    Ok(lines)
951
}
952
01a47cd7 Andy McCall ↶ 953
/// The lines of a CBOR array of byte strings, copied once into one buffer.
954
fn array_lines(v: &Value) -> Result<Lines, ObjectError> {
955
    Ok(array(v, "lines")?
a4fad5b0 Andy McCall ↶ 956
        .iter()
01a47cd7 Andy McCall ↶ 957
        .map(|l| bytes(l, "line"))
958
        .collect::<Result<Vec<&[u8]>, _>>()?
959
        .into_iter()
960
        .collect())
a4fad5b0 Andy McCall ↶ 961
}
962
963
impl Change {
964
    /// Decode a change for a history-only state (spec §11): every insert is
965
    /// left without its lines (an empty placeholder), and the result says
966
    /// where each one's lines are, in order. The inline lines are stepped
967
    /// over, not decoded: for a change that adds a whole tree, most of its
968
    /// bytes. `me` is the change's hash.
969
    pub fn decode_skeleton(
970
        bytes: &[u8],
971
        me: ChangeHash,
972
    ) -> Result<(Change, Vec<TextRef>), ObjectError> {
973
        let mut c = cbor::Cursor::new(bytes);
974
        let (major, n) = c.head()?;
975
        if major != 5 {
976
            return schema("change: expected map");
977
        }
978
        let mut fields = Map::new();
979
        let mut edits = Vec::new();
980
        let mut texts = Vec::new();
981
        let mut version = None;
982
        let mut last: Option<u64> = None;
983
        for _ in 0..n {
984
            let k = map_key(&mut c, &mut last)?;
985
            if k == 7 {
986
                let Some(v) = version else {
987
                    return schema("change: version must come first");
988
                };
989
                let (am, an) = c.head()?;
990
                if am != 4 {
991
                    return schema("edits: expected array");
992
                }
993
                for _ in 0..an {
994
                    edits.push(edit_skeleton(&mut c, v, me, &mut texts)?);
995
                }
996
                fields.insert(7, Value::Array(Vec::new()));
997
            } else {
998
                let val = c.value()?;
999
                if k == 0 {
1000
                    version = Some(uint(&val, "version")?);
1001
                }
1002
                fields.insert(k, val);
1003
            }
1004
        }
1005
        if !c.at_end() {
1006
            return Err(ObjectError::Cbor(cbor::CborError::TrailingBytes));
1007
        }
1008
        let mut change = Change::from_value(&Value::Map(fields))?;
1009
        change.edits = edits;
1010
        Ok((change, texts))
1011
    }
1012
}
1013
4d6c9c1b Andy McCall ↶ 1014
impl Change {
1015
    /// A change's metadata alone: its tree operations and edits are
1016
    /// stepped over, not decoded. For log, signature checks and walks of
1017
    /// the dependency graph, which read nothing else.
1018
    pub fn decode_meta(bytes: &[u8]) -> Result<crate::state::ChangeMeta, ObjectError> {
1019
        let mut c = cbor::Cursor::new(bytes);
1020
        let (major, n) = c.head()?;
1021
        if major != 5 {
1022
            return schema("change: expected map");
1023
        }
1024
        let mut fields = Map::new();
1025
        let mut last: Option<u64> = None;
1026
        for _ in 0..n {
1027
            let k = map_key(&mut c, &mut last)?;
1028
            if k == 6 || k == 7 {
1029
                c.skip()?;
1030
                fields.insert(k, Value::Array(Vec::new()));
1031
            } else {
1032
                fields.insert(k, c.value()?);
1033
            }
1034
        }
1035
        if !c.at_end() {
1036
            return Err(ObjectError::Cbor(cbor::CborError::TrailingBytes));
1037
        }
1038
        Ok(crate::state::ChangeMeta::of(&Change::from_value(
1039
            &Value::Map(fields),
1040
        )?))
1041
    }
1042
}
1043
a4fad5b0 Andy McCall ↶ 1044
/// A canonical map key: an unsigned integer above the previous one.
1045
fn map_key(c: &mut cbor::Cursor, last: &mut Option<u64>) -> Result<u64, ObjectError> {
1046
    let (km, k) = c.head()?;
1047
    if km != 0 || last.is_some_and(|l| k <= l) {
1048
        return Err(ObjectError::Cbor(cbor::CborError::BadMapKey));
1049
    }
1050
    *last = Some(k);
1051
    Ok(k)
1052
}
1053
1054
/// One edit of [`Change::decode_skeleton`]: an insert's lines are left out.
1055
fn edit_skeleton(
1056
    c: &mut cbor::Cursor,
1057
    version: u64,
1058
    me: ChangeHash,
1059
    texts: &mut Vec<TextRef>,
1060
) -> Result<Edit, ObjectError> {
1061
    let (major, n) = c.head()?;
1062
    if major != 5 {
1063
        return schema("edit: expected map");
1064
    }
1065
    let mut fields = Map::new();
1066
    let mut inline: Option<(usize, usize, u64)> = None;
1067
    let mut last: Option<u64> = None;
1068
    for _ in 0..n {
1069
        let k = map_key(c, &mut last)?;
1070
        if k == 4 {
1071
            let start = c.pos();
1072
            let (am, count) = c.head()?;
1073
            if am != 4 {
1074
                return schema("lines: expected array");
1075
            }
1076
            for _ in 0..count {
1077
                c.skip()?;
1078
            }
1079
            inline = Some((start, c.pos(), count));
1080
            continue;
1081
        }
1082
        fields.insert(k, c.value()?);
1083
    }
1084
    let Some((start, end, count)) = inline else {
0f93f49c Andy McCall ↶ 1085
        return Edit::from_value(&Value::Map(fields), version, &mut |text, count, form| {
1086
            texts.push(TextRef::Blob(*text, count, form));
01a47cd7 Andy McCall ↶ 1087
            Ok(Lines::default())
a4fad5b0 Andy McCall ↶ 1088
        });
1089
    };
1090
    if uint(field(&fields, 0, "edit tag")?, "edit tag")? != 0 || fields.contains_key(&5) {
1091
        return schema("only an insert has inline lines, and not with a text blob");
1092
    }
1093
    if count == 0 {
1094
        return schema("insert with no lines");
1095
    }
1096
    if version >= 2 && end - start >= TEXT_BLOB_MIN {
1097
        return schema("an insert this large is stored as a text blob in a version 2 change");
1098
    }
1099
    let (Ok(start), Ok(end)) = (u32::try_from(start), u32::try_from(end)) else {
1100
        return schema("change too large");
1101
    };
1102
    texts.push(TextRef::Inline {
1103
        change: me,
1104
        start,
1105
        end,
1106
    });
1107
    Ok(Edit::Insert {
1108
        node: node_id(field(&fields, 1, "edit.node")?, "edit.node")?,
1109
        after: AtomRef::from_value(field(&fields, 2, "after")?, "after")?,
1110
        before: AtomRef::from_value(field(&fields, 3, "before")?, "before")?,
01a47cd7 Andy McCall ↶ 1111
        lines: Lines::default(),
a4fad5b0 Andy McCall ↶ 1112
    })
1113
}
6f8aeb99 Andy McCall ↶ 1114
1115
/// A large insert's lines as stored (spec §6.1): the blob and its content,
1116
/// the canonical CBOR array of the lines' byte strings.
1117
#[derive(Debug, Clone, PartialEq, Eq)]
1118
pub struct StoredText {
1119
    pub blob: Blob,
1120
    pub content: Vec<u8>,
0f93f49c Andy McCall ↶ 1121
    pub form: TextForm,
6f8aeb99 Andy McCall ↶ 1122
}
1123
0f93f49c Andy McCall ↶ 1124
/// The form a change of `version` stores these lines in.
1125
fn text_form(version: u64, lines: &Lines) -> TextForm {
1126
    match version {
1127
        ..=2 => TextForm::Cbor,
1128
        _ if lines.is_split_form() => TextForm::Raw,
1129
        _ => TextForm::CborFallback,
1130
    }
1131
}
1132
6f8aeb99 Andy McCall ↶ 1133
/// The lines a text blob's `content` holds, checked against the rules for
1134
/// one: canonical, as many as the insert claims, and not short enough to
1135
/// have been inline.
0f93f49c Andy McCall ↶ 1136
pub fn text_lines(content: &[u8], count: u32, form: TextForm) -> Result<Lines, ObjectError> {
1137
    let lines = match form {
1138
        TextForm::Raw => Lines::split(content),
1139
        TextForm::Cbor | TextForm::CborFallback => array_lines(&cbor::decode(content)?)?,
1140
    };
1141
    // One encoding per change: the size rule and the form are both fixed
1142
    // by the lines.
1143
    if cbor::byte_strings_len(&lines) < TEXT_BLOB_MIN {
6f8aeb99 Andy McCall ↶ 1144
        return schema(format!(
1145
            "a text blob of {} bytes is short enough to be inline",
1146
            content.len()
1147
        ));
1148
    }
0f93f49c Andy McCall ↶ 1149
    if form == TextForm::CborFallback && lines.is_split_form() {
1150
        return schema("these lines are stored as their bytes in a version 3 change");
1151
    }
6f8aeb99 Andy McCall ↶ 1152
    if lines.len() != count as usize {
1153
        return schema(format!(
1154
            "a text blob holds {} lines, the insert says {count}",
1155
            lines.len()
1156
        ));
1157
    }
1158
    Ok(lines)
1159
}
1160
ff0af1ee unknown 1161
// ---------------------------------------------------------------- blob
1162
1163
/// File content as an ordered list of chunks (spec §13.2).
1164
#[derive(Debug, Clone, PartialEq, Eq)]
1165
pub struct Blob {
1166
    pub length: u64,
1167
    pub chunks: Vec<ObjectRef>,
1168
}
1169
1170
impl Blob {
6f8aeb99 Andy McCall ↶ 1171
    /// The blob for `content`: its chunks as content-defined chunking cuts
1172
    /// them (spec §13.2).
1173
    pub fn of(content: &[u8]) -> Blob {
1174
        Blob {
1175
            length: content.len() as u64,
1176
            chunks: crate::chunk::chunks(content)
1177
                .into_iter()
1178
                .map(|c| hash_object(ObjectType::Chunk, c))
1179
                .collect(),
1180
        }
1181
    }
1182
ff0af1ee unknown 1183
    pub fn encode(&self) -> Vec<u8> {
1184
        cbor::encode(&Value::map([
1185
            (0, Value::Uint(FORMAT_VERSION)),
1186
            (1, Value::Uint(self.length)),
75167672 Andy McCall 1187
            (
1188
                2,
1189
                Value::Array(
1190
                    self.chunks
1191
                        .iter()
1192
                        .map(|c| Value::Bytes(c.0.to_vec()))
1193
                        .collect(),
1194
                ),
1195
            ),
ff0af1ee unknown 1196
        ]))
1197
    }
1198
    pub fn hash(&self) -> ObjectRef {
1199
        hash_object(ObjectType::Blob, &self.encode())
1200
    }
1201
    pub fn decode(bytes: &[u8]) -> Result<Blob, ObjectError> {
1202
        let v = cbor::decode(bytes)?;
1203
        let m = map(&v, "blob")?;
1204
        check_version(m)?;
1205
        Ok(Blob {
1206
            length: uint(field(m, 1, "length")?, "length")?,
1207
            chunks: array(field(m, 2, "chunks")?, "chunks")?
1208
                .iter()
1209
                .map(|c| oref(c, "chunk"))
1210
                .collect::<Result<Vec<_>, _>>()?,
1211
        })
1212
    }
1213
}
1214
252e8cb2 Andy McCall ↶ 1215
// ---------------------------------------------------------------- checkpoint
1216
1217
/// A materialized state (spec §11): the heads it stands for, how many changes
1218
/// it holds, and a blob with the serialized graphs (`State::checkpoint_payload`).
1219
#[derive(Debug, Clone, PartialEq, Eq)]
1220
pub struct Checkpoint {
1221
    pub heads: Vec<ChangeHash>,
1222
    pub count: u64,
1223
    pub payload: ObjectRef,
1224
}
1225
1226
impl Checkpoint {
1227
    pub fn encode(&self) -> Vec<u8> {
1228
        cbor::encode(&Value::map([
1229
            (0, Value::Uint(FORMAT_VERSION)),
75167672 Andy McCall 1230
            (
1231
                1,
1232
                Value::Array(
1233
                    self.heads
1234
                        .iter()
1235
                        .map(|h| Value::Bytes(h.0.to_vec()))
1236
                        .collect(),
1237
                ),
1238
            ),
252e8cb2 Andy McCall ↶ 1239
            (2, Value::Uint(self.count)),
1240
            (3, Value::Bytes(self.payload.0.to_vec())),
1241
        ]))
1242
    }
1243
    pub fn hash(&self) -> ObjectRef {
1244
        hash_object(ObjectType::Checkpoint, &self.encode())
1245
    }
1246
    pub fn decode(bytes: &[u8]) -> Result<Checkpoint, ObjectError> {
1247
        let v = cbor::decode(bytes)?;
1248
        let m = map(&v, "checkpoint")?;
1249
        check_version(m)?;
1250
        Ok(Checkpoint {
75167672 Andy McCall 1251
            heads: array(field(m, 1, "heads")?, "heads")?
1252
                .iter()
1253
                .map(|h| oref(h, "head"))
1254
                .collect::<Result<Vec<_>, _>>()?,
252e8cb2 Andy McCall ↶ 1255
            count: uint(field(m, 2, "count")?, "count")?,
1256
            payload: oref(field(m, 3, "payload")?, "payload")?,
1257
        })
1258
    }
1259
}
1260
86e682c2 Andy McCall ↶ 1261
// ---------------------------------------------------------------- state
1262
1263
/// A state named by its heads (spec §10.1): what a bookmark or tag points at.
1264
#[derive(Debug, Clone, PartialEq, Eq, Default)]
1265
pub struct StateObject {
1266
    pub heads: Vec<ChangeHash>,
1267
}
1268
1269
impl StateObject {
1270
    pub fn new(mut heads: Vec<ChangeHash>) -> StateObject {
1271
        heads.sort();
1272
        heads.dedup();
1273
        StateObject { heads }
1274
    }
1275
    pub fn encode(&self) -> Vec<u8> {
1276
        debug_assert!(self.heads.windows(2).all(|w| w[0] < w[1]));
1277
        cbor::encode(&Value::map([
1278
            (0, Value::Uint(FORMAT_VERSION)),
75167672 Andy McCall 1279
            (
1280
                1,
1281
                Value::Array(
1282
                    self.heads
1283
                        .iter()
1284
                        .map(|h| Value::Bytes(h.0.to_vec()))
1285
                        .collect(),
1286
                ),
1287
            ),
86e682c2 Andy McCall ↶ 1288
        ]))
1289
    }
1290
    pub fn hash(&self) -> ObjectRef {
1291
        hash_object(ObjectType::State, &self.encode())
1292
    }
1293
    pub fn decode(bytes: &[u8]) -> Result<StateObject, ObjectError> {
1294
        let v = cbor::decode(bytes)?;
1295
        let m = map(&v, "state")?;
1296
        check_version(m)?;
75167672 Andy McCall 1297
        let heads = array(field(m, 1, "heads")?, "heads")?
1298
            .iter()
1299
            .map(|h| oref(h, "head"))
1300
            .collect::<Result<Vec<_>, _>>()?;
86e682c2 Andy McCall ↶ 1301
        if !heads.windows(2).all(|w| w[0] < w[1]) {
1302
            return schema("state heads must be sorted and unique");
1303
        }
1304
        Ok(StateObject { heads })
1305
    }
1306
}
1307
ff0af1ee unknown 1308
#[cfg(test)]
1309
pub(crate) mod tests {
1310
    use super::*;
1311
1312
    pub(crate) fn sample_change() -> Change {
1313
        let n = NodeId([7; 16]);
1314
        let dep = ObjectRef([1; 32]);
1315
        let mut c = Change {
6f8aeb99 Andy McCall ↶ 1316
            version: crate::objects::CHANGE_VERSION,
ff0af1ee unknown 1317
            change_id: ChangeId([9; 16]),
1318
            deps: vec![ObjectRef([2; 32]), dep],
1319
            author: ObjectRef([3; 32]),
75167672 Andy McCall 1320
            hlc: Hlc {
1321
                wall_ms: 1_700_000_000_000,
1322
                logical: 4,
1323
            },
ff0af1ee unknown 1324
            message: "Add greeting".into(),
1325
            tree_ops: vec![
75167672 Andy McCall 1326
                TreeOp::AddNode {
1327
                    node: n,
1328
                    kind: NodeKind::File,
1329
                    mode: Mode::TEXT,
1330
                    parent: NodeId::ROOT,
1331
                    name: "hello.txt".into(),
1332
                },
1333
                TreeOp::SetMode {
1334
                    node: n,
1335
                    old: OpRef {
1336
                        change: ObjectRef::ZERO,
1337
                        index: 0,
1338
                    },
1339
                    mode: Mode {
1340
                        exec: true,
1341
                        opaque: false,
1342
                    },
1343
                },
ff0af1ee unknown 1344
            ],
1345
            edits: vec![
75167672 Andy McCall 1346
                Edit::Insert {
1347
                    node: n,
1348
                    after: AtomRef::Start,
1349
                    before: AtomRef::End,
01a47cd7 Andy McCall ↶ 1350
                    lines: vec![b"hello\n".to_vec(), b"world\n".to_vec()].into(),
75167672 Andy McCall 1351
                },
1352
                Edit::Delete {
1353
                    node: n,
1354
                    atoms: vec![AtomRange {
1355
                        change: dep,
1356
                        from: 0,
1357
                        to: 2,
1358
                    }],
1359
                },
1360
                Edit::Replace {
1361
                    node: n,
1362
                    old: None,
1363
                    new: Some(ObjectRef([5; 32])),
1364
                },
ff0af1ee unknown 1365
            ],
1366
            kind: ChangeKind::Normal,
1367
        };
1368
        c.normalize();
1369
        c
1370
    }
1371
1372
    #[test]
1373
    fn change_round_trips_and_hash_is_stable() {
1374
        let c = sample_change();
1375
        let bytes = c.encode();
1376
        let d = Change::decode(&bytes).unwrap();
1377
        assert_eq!(c, d);
1378
        assert_eq!(c.hash(), d.hash());
1379
        assert_eq!(d.encode(), bytes);
1380
        // Any semantic difference changes the hash.
1381
        let mut e = c.clone();
1382
        e.message.push('!');
1383
        assert_ne!(e.hash(), c.hash());
5b4c5130 Andy McCall ↶ 1384
    }
1385
1386
    #[test]
1387
    fn compact_encoding_is_the_plain_one() {
1388
        // Inline lines written as their encoding give the same bytes, so
1389
        // the same hash, as a value per line.
1390
        let c = sample_change();
1391
        assert!(!c.has_stored_text(), "the sample's insert is inline");
1392
        let plain = cbor::encode(&c.to_value());
1393
        assert_eq!(c.encode(), plain);
1394
        assert_eq!(c.encode_parts().0, plain);
1395
        let streamed: Result<Vec<u8>, ()> = c.encode_streaming(|_| Ok(()));
1396
        assert_eq!(streamed.unwrap(), plain);
1397
        assert_eq!(Change::decode(&plain).unwrap(), c);
ff0af1ee unknown 1398
    }
1399
1400
    #[test]
1401
    fn decode_rejects_bad_changes() {
1402
        let c = sample_change();
1403
        let mut v = c.to_value();
1404
        if let Value::Map(m) = &mut v {
0f93f49c Andy McCall ↶ 1405
            m.insert(0, Value::Uint(4));
ff0af1ee unknown 1406
        }
75167672 Andy McCall 1407
        assert!(
1408
            matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("version"))
1409
        );
ff0af1ee unknown 1410
1411
        let mut v = c.to_value();
1412
        if let Value::Map(m) = &mut v {
75167672 Andy McCall 1413
            m.insert(
1414
                2,
1415
                Value::Array(vec![Value::Bytes(vec![2; 32]), Value::Bytes(vec![1; 32])]),
1416
            );
ff0af1ee unknown 1417
        }
75167672 Andy McCall 1418
        assert!(
1419
            matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("sorted"))
1420
        );
ff0af1ee unknown 1421
1422
        let mut v = c.to_value();
1423
        if let Value::Map(m) = &mut v {
1424
            m.insert(9, Value::map([(0, Value::Null)]));
1425
        }
75167672 Andy McCall 1426
        assert!(
1427
            matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("extra"))
1428
        );
6f8aeb99 Andy McCall ↶ 1429
    }
1430
01a47cd7 Andy McCall ↶ 1431
    fn big_lines(n: usize) -> Lines {
6f8aeb99 Andy McCall ↶ 1432
        (0..n)
1433
            .map(|i| format!("line {i} of a large insert\n").into_bytes())
1434
            .collect()
1435
    }
1436
01a47cd7 Andy McCall ↶ 1437
    fn with_insert(mut c: Change, lines: Lines) -> Change {
6f8aeb99 Andy McCall ↶ 1438
        c.edits[0] = Edit::Insert {
1439
            node: NodeId([7; 16]),
1440
            after: AtomRef::Start,
1441
            before: AtomRef::End,
1442
            lines,
1443
        };
1444
        c
1445
    }
1446
1447
    #[test]
1448
    fn large_inserts_are_stored_as_text_blobs() {
1449
        assert!(
1450
            !sample_change().has_stored_text(),
1451
            "a small insert stays inline"
1452
        );
1453
        let lines = big_lines(500);
1454
        let c = with_insert(sample_change(), lines.clone());
1455
        assert!(c.has_stored_text());
1456
        let (bytes, texts) = c.encode_parts();
1457
        assert_eq!(bytes, c.encode());
1458
        assert!(bytes.len() < 1024, "{}", bytes.len());
1459
        assert_eq!(texts.len(), 1);
0f93f49c Andy McCall ↶ 1460
        // Version 3: the lines' bytes themselves.
1461
        assert_eq!(texts[0].form, TextForm::Raw);
1462
        assert_eq!(texts[0].content, lines.bytes());
6f8aeb99 Andy McCall ↶ 1463
        assert_eq!(texts[0].blob, Blob::of(&texts[0].content));
1464
        assert!(Change::decode(&bytes).is_err(), "reading it needs the text");
0f93f49c Andy McCall ↶ 1465
        let d = Change::decode_with(&bytes, &mut |r, n, form| {
6f8aeb99 Andy McCall ↶ 1466
            assert_eq!(*r, texts[0].blob.hash());
0f93f49c Andy McCall ↶ 1467
            assert_eq!(form, TextForm::Raw);
1468
            text_lines(&texts[0].content, n, form)
6f8aeb99 Andy McCall ↶ 1469
        })
1470
        .unwrap();
1471
        assert_eq!(d, c);
1472
        assert_eq!(d.hash(), c.hash());
0f93f49c Andy McCall ↶ 1473
        // Version 2 held the same lines as a CBOR array, and still reads.
1474
        let mut v2 = c.clone();
1475
        v2.version = 2;
1476
        let (b2, t2) = v2.encode_parts();
1477
        assert_eq!(t2[0].form, TextForm::Cbor);
1478
        assert_eq!(t2[0].content, cbor::encode_byte_strings(&lines));
1479
        let d2 = Change::decode_with(&b2, &mut |_, n, form| text_lines(&t2[0].content, n, form))
1480
            .unwrap();
1481
        assert_eq!(d2, v2);
1482
        // Lines the line rule would not split back stay a CBOR array,
1483
        // marked; and a marked array of lines that would is refused.
1484
        let odd: Lines = (0..500)
1485
            .map(|i| format!("no newline {i}").into_bytes())
1486
            .collect();
1487
        let c3 = with_insert(sample_change(), odd.clone());
1488
        let (b3, t3) = c3.encode_parts();
1489
        assert_eq!(t3[0].form, TextForm::CborFallback);
1490
        let d3 = Change::decode_with(&b3, &mut |_, n, form| text_lines(&t3[0].content, n, form))
1491
            .unwrap();
1492
        assert_eq!(d3, c3);
1493
        let marked = cbor::encode_byte_strings(&lines);
1494
        assert!(text_lines(&marked, 500, TextForm::CborFallback).is_err());
6f8aeb99 Andy McCall ↶ 1495
    }
1496
1497
    #[test]
1498
    fn version_1_changes_keep_their_inline_inserts_and_hashes() {
1499
        let mut c = with_insert(sample_change(), big_lines(500));
1500
        c.version = 1;
1501
        let bytes = c.encode();
1502
        assert!(bytes.len() > TEXT_BLOB_MIN);
1503
        let (again, texts) = c.encode_parts();
1504
        assert!(texts.is_empty());
1505
        assert_eq!(again, bytes);
1506
        let d = Change::decode(&bytes).unwrap();
1507
        assert_eq!(d.version, 1);
1508
        assert_eq!(d.encode(), bytes);
1509
        // The same inline insert in a version 2 change is not canonical.
1510
        let mut v = c.to_value();
1511
        if let Value::Map(m) = &mut v {
1512
            m.insert(0, Value::Uint(2));
1513
        }
1514
        assert!(
1515
            matches!(Change::from_value(&v), Err(ObjectError::Schema(s)) if s.contains("text blob"))
1516
        );
a4fad5b0 Andy McCall ↶ 1517
    }
1518
1519
    #[test]
1520
    fn a_skeleton_leaves_every_insert_out_and_says_where_its_lines_are() {
1521
        let mut c = with_insert(sample_change(), big_lines(500));
1522
        c.edits.push(Edit::Insert {
1523
            node: NodeId([8; 16]),
1524
            after: AtomRef::Start,
1525
            before: AtomRef::End,
01a47cd7 Andy McCall ↶ 1526
            lines: vec![b"small\n".to_vec(), b"insert\n".to_vec()].into(),
a4fad5b0 Andy McCall ↶ 1527
        });
1528
        let (bytes, stored) = c.encode_parts();
1529
        let me = ObjectRef([4; 32]);
1530
        let (skel, texts) = Change::decode_skeleton(&bytes, me).unwrap();
1531
        assert_eq!(texts.len(), 2);
1532
        let mut filled = skel.clone();
1533
        let mut next = texts.iter();
1534
        for e in filled.edits.iter_mut() {
1535
            if let Edit::Insert { lines, .. } = e {
1536
                assert!(lines.is_empty(), "no line kept");
1537
                *lines = match next.next().unwrap() {
0f93f49c Andy McCall ↶ 1538
                    TextRef::Blob(r, n, form) => {
a4fad5b0 Andy McCall ↶ 1539
                        assert_eq!(*r, stored[0].blob.hash());
0f93f49c Andy McCall ↶ 1540
                        text_lines(&stored[0].content, *n, *form).unwrap()
a4fad5b0 Andy McCall ↶ 1541
                    }
1542
                    TextRef::Inline { change, start, end } => {
1543
                        assert_eq!(*change, me);
1544
                        inline_lines(&bytes[*start as usize..*end as usize]).unwrap()
1545
                    }
1546
                };
1547
            }
1548
        }
1549
        assert_eq!(filled, c, "filled in, it is the change");
1550
        // A version 1 change's large inline insert is fine; in version 2 it is not.
1551
        let mut v1 = with_insert(sample_change(), big_lines(500));
1552
        v1.version = 1;
1553
        assert!(Change::decode_skeleton(&v1.encode(), me).is_ok());
1554
        let mut v = v1.to_value();
1555
        if let Value::Map(m) = &mut v {
1556
            m.insert(0, Value::Uint(2));
1557
        }
1558
        assert!(Change::decode_skeleton(&cbor::encode(&v), me).is_err());
4d6c9c1b Andy McCall ↶ 1559
    }
1560
1561
    #[test]
1562
    fn metadata_reads_without_the_operations() {
1563
        let c = with_insert(sample_change(), big_lines(500));
1564
        let (bytes, _) = c.encode_parts();
1565
        let m = Change::decode_meta(&bytes).unwrap();
1566
        assert_eq!(m, crate::state::ChangeMeta::of(&c));
6f8aeb99 Andy McCall ↶ 1567
    }
1568
1569
    #[test]
1570
    fn text_blobs_are_checked() {
1571
        let lines = big_lines(500);
1572
        let content = cbor::encode_byte_strings(&lines);
1573
        assert_eq!(content.len(), cbor::byte_strings_len(&lines));
1574
        assert_eq!(
1575
            content,
1576
            cbor::encode(&Value::Array(
01a47cd7 Andy McCall ↶ 1577
                lines.iter().map(|l| Value::Bytes(l.to_vec())).collect()
6f8aeb99 Andy McCall ↶ 1578
            ))
0f93f49c Andy McCall ↶ 1579
        );
1580
        assert_eq!(text_lines(&content, 500, TextForm::Cbor).unwrap(), lines);
1581
        assert!(text_lines(&content, 499, TextForm::Cbor).is_err());
1582
        assert_eq!(
1583
            text_lines(lines.bytes(), 500, TextForm::Raw).unwrap(),
1584
            lines
6f8aeb99 Andy McCall ↶ 1585
        );
0f93f49c Andy McCall ↶ 1586
        assert!(text_lines(lines.bytes(), 501, TextForm::Raw).is_err());
01a47cd7 Andy McCall ↶ 1587
        let small = cbor::encode_byte_strings(&vec![b"x\n".to_vec()].into());
0f93f49c Andy McCall ↶ 1588
        assert!(
1589
            text_lines(&small, 1, TextForm::Cbor).is_err(),
1590
            "short enough to be inline"
1591
        );
1592
        assert!(
1593
            text_lines(b"x\n", 1, TextForm::Raw).is_err(),
1594
            "short enough to be inline"
1595
        );
ff0af1ee unknown 1596
    }
1597
1598
    #[test]
1599
    fn names_are_validated() {
1600
        for bad in ["", ".", "..", "a/b", "a\0b"] {
1601
            assert!(validate_name(bad).is_err(), "{bad:?}");
1602
        }
1603
        assert!(validate_name("Foo.c").is_ok());
1604
        assert!(validate_name("...").is_ok());
1605
    }
1606
1607
    #[test]
1608
    fn mode_and_kind_tags() {
75167672 Andy McCall 1609
        for m in [
1610
            Mode::TEXT,
1611
            Mode::OPAQUE,
1612
            Mode {
1613
                exec: true,
1614
                opaque: true,
1615
            },
1616
        ] {
ff0af1ee unknown 1617
            assert_eq!(Mode::from_bits(m.bits()).unwrap(), m);
1618
        }
1619
        assert!(Mode::from_bits(4).is_err());
75167672 Andy McCall 1620
        for k in [
1621
            NodeKind::File,
1622
            NodeKind::Dir,
1623
            NodeKind::Symlink,
1624
            NodeKind::Subrepo,
1625
        ] {
ff0af1ee unknown 1626
            assert_eq!(NodeKind::from_tag(k.tag()).unwrap(), k);
1627
        }
1628
    }
1629
1630
    #[test]
1631
    fn self_references_resolve() {
1632
        let me = ObjectRef([8; 32]);
75167672 Andy McCall 1633
        assert_eq!(
1634
            AtomRef::Atom {
1635
                change: ObjectRef::ZERO,
1636
                index: 3
1637
            }
1638
            .resolve(me),
1639
            AtomRef::Atom {
1640
                change: me,
1641
                index: 3
1642
            }
1643
        );
ff0af1ee unknown 1644
        assert_eq!(AtomRef::Start.resolve(me), AtomRef::Start);
1645
        let other = ObjectRef([1; 32]);
75167672 Andy McCall 1646
        assert_eq!(
1647
            OpRef {
1648
                change: other,
1649
                index: 1
1650
            }
1651
            .resolve(me)
1652
            .change,
1653
            other
1654
        );
ff0af1ee unknown 1655
    }
1656
1657
    #[test]
86e682c2 Andy McCall ↶ 1658
    fn state_object_round_trip() {
75167672 Andy McCall 1659
        let st = StateObject::new(vec![
1660
            ObjectRef([2; 32]),
1661
            ObjectRef([1; 32]),
1662
            ObjectRef([2; 32]),
1663
        ]);
86e682c2 Andy McCall ↶ 1664
        assert_eq!(st.heads, vec![ObjectRef([1; 32]), ObjectRef([2; 32])]);
1665
        assert_eq!(StateObject::decode(&st.encode()).unwrap(), st);
1666
        assert_eq!(StateObject::new(vec![]).heads, vec![]);
1667
        assert_ne!(StateObject::new(vec![]).hash(), st.hash());
1668
    }
1669
1670
    #[test]
ff0af1ee unknown 1671
    fn blob_round_trip() {
75167672 Andy McCall 1672
        let b = Blob {
1673
            length: 10,
1674
            chunks: vec![ObjectRef([1; 32]), ObjectRef([2; 32])],
1675
        };
ff0af1ee unknown 1676
        assert_eq!(Blob::decode(&b.encode()).unwrap(), b);
1677
    }
1678
}