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