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