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