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