OXYGENHUB

Spec §5.5 encoding conventions; SIGPIPE handling in the CLI; clippy fixes 5b1114a04905

Author
Andy McCall @andymccall <[email protected]>
When
· 2026-09-26 18:13 UTC
Signature
✔ signed by Andy McCall
Hash
5b1114a049057de07d38c50a401abbd4893bd1babc3bd97e068315fbf15bd6dd
Depends on
1983686fb7a7 f9c98deef326 ff0af1ee299d
Depended on by
8b3c9b2ba502 767ac6cd0545 648189388f1a e0d56dfca51d 19527875ebdc d443639bcf35 6f8aeb996558 75167672ebcd 86fe29fd62ea 132b884b9a8d 135212de5370 fce44ae9ad29 86e682c242c6 5e388efbde57
In bookmarks
agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
In tags
v0.1.0 v0.10.0 v0.11.0 v0.2.0 v0.3.0 v0.3.1 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
Edits
12 atoms, 0 tree operations, 6 files

Browse the tree at this change

modified Cargo.lock +1 −0
…
336 unchanged lines
337 337
dependencies = [
338 338
 "anyhow",
339 339
 "clap",
340 +
 "libc",
340 341
 "oxy-core",
341 342
 "tempfile",
342 343
]
…
280 unchanged lines
modified Cargo.toml +1 −0
…
22 unchanged lines
23 23
oxy-core = { path = "crates/oxy-core" }
24 24
clap = { version = "4", features = ["derive"] }
25 25
anyhow = "1"
26 +
libc = "0.2"
26 27
27 28
[dev-dependencies]
28 29
tempfile = "3"
modified crates/oxy-core/src/identity.rs +1 −1
…
292 unchanged lines
293 293
        // Deterministic signatures.
294 294
        assert_eq!(sk.sign(Domain::Change, &key, &target), sig);
295 295
        // Signature list round trip.
296 -
        let l = Signature::decode_list(&Signature::encode_list(&[sig.clone()])).unwrap();
296 +
        let l = Signature::decode_list(&Signature::encode_list(std::slice::from_ref(&sig))).unwrap();
297 297
        assert_eq!(l, vec![sig]);
298 298
    }
299 299
…
44 unchanged lines
modified crates/oxy-core/src/store.rs +1 −1
…
144 unchanged lines
145 145
        let mut sigs = self.signatures(r)?;
146 146
        sigs.retain(|s| s.key != sig.key);
147 147
        sigs.push(sig.clone());
148 -
        sigs.sort_by(|a, b| a.key.cmp(&b.key));
148 +
        sigs.sort_by_key(|s| s.key);
149 149
        let path = self.sig_path(r);
150 150
        fs::create_dir_all(path.parent().unwrap())?;
151 151
        let mut nonce = [0u8; 8];
…
88 unchanged lines
modified docs/spec.md +25 −3
…
1 unchanged lines
2 2
3 3
| | |
4 4
|---|---|
5 -
| **Status** | Draft 0.4 |
5 +
| **Status** | Draft 0.4b |
6 6
| **Format version** | 1 |
7 7
| **Date** | 2026-09-26 |
8 8
| **Audience** | Implementers, security reviewers, and contributors |
…
162 unchanged lines
171 171
172 172
Adding a signature, re-signing, or attaching a seal (§14.4) therefore never changes an ObjectRef.
173 173
174 +
### 5.5 Encoding conventions
175 +
176 +
The field tables in this document give each map key in brackets. Where a structure is a choice between variants, or a compact reference, the following encodings apply:
177 +
178 +
| Item | Encoding |
179 +
|---|---|
180 +
| `TreeOp` | Map with key 0 = variant tag: 0 `AddNode`, 1 `RemoveNode`, 2 `Move`, 3 `SetMode`; key 1 = node id; `AddNode` adds 2 kind, 3 mode, 4 parent, 5 name; `RemoveNode` adds 2 placement ref; `Move` adds 2 old placement ref, 4 parent, 5 name; `SetMode` adds 2 old mode ref, 3 mode. |
181 +
| `Edit` | Map with key 0 = variant tag: 0 `Insert`, 1 `Delete`, 2 `Replace`; key 1 = node id; `Insert` adds 2 after, 3 before, 4 lines (array of byte strings) or 5 `lines_blob`; `Delete` adds 2 atom ranges; `Replace` adds 2 old (bytes or null), 3 new (bytes or null). |
182 +
| `NodeKind` | Unsigned integer: 0 file, 1 dir, 2 symlink. |
183 +
| `Mode` | Unsigned integer bit field: bit 0 executable, bit 1 opaque content mode. Directories carry 0. |
184 +
| `OpRef` (placement or mode reference) | `[bytes(32) change hash, uint index]`; the all-zero hash means this change. |
185 +
| `AtomRef` | `0` for `START`, `1` for `END`, otherwise `[bytes(32) change hash, uint index]` with the all-zero hash meaning this change. |
186 +
| `AtomRange` | `[bytes(32) change hash, uint from, uint to]`, half-open, non-empty. |
187 +
| `View` (in `Op`) | Key 0 bookmarks: array of `[text name, [bytes(32) heads]]` sorted by name; key 1 workspaces: array of `[bytes(16) id, bytes(32) change]` sorted by id. Remote and tag namespaces are added in later drafts. |
188 +
| `Signature` | Map: 0 key id, 1 alg (1 = Ed25519 + ML-DSA-65), 2 Ed25519 bytes(64), 3 ML-DSA-65 bytes(3309). A signature list is a CBOR array of these. |
189 +
| Secret key file | Map: 0 version, 1 key id, 2 Ed25519 seed bytes(32), 3 ML-DSA-65 seed bytes(32). Kept only in the user's key directory. |
190 +
191 +
Atom indices count the atoms one change introduces for one node, in edit order, across all of that change's `Insert` edits on the node.
192 +
174 193
## 6. The change graph
175 194
176 195
### 6.1 Change object
…
270 unchanged lines
447 466
  objects/
448 467
    pack-<id>.pack    append-only pack file
449 468
    pack-<id>.idx     sorted (ObjectRef → offset, length, type) index
450 -
    loose/            objects written since the last pack
469 +
    loose/            objects written since the last pack, as header || body
470 +
    sigs/             signature lists keyed by the ObjectRef they cover (§5.4)
451 471
  ops/                operation objects and op heads
452 472
  refs/               current view cache (derived from ops; rebuildable)
453 473
  workspaces/<id>/    snapshot cache, fsmonitor cookie
…
321 unchanged lines
775 795
776 796
---
777 797
778 -
*Changelog.* Draft 0.4a (2026-09-26): §13.2 pins the FastCDC gear table, rolling hash, and masks so boundaries are identical across implementations.
798 +
*Changelog.* Draft 0.4b (2026-09-26): §5.5 records the encoding conventions for variants, references, views, signatures, and secret key files; §13.1 adds the signature directory.
799 +
800 +
Draft 0.4a (2026-09-26): §13.2 pins the FastCDC gear table, rolling hash, and masks so boundaries are identical across implementations.
779 801
780 802
Draft 0.4 (2026-09-26): overview no longer describes the constrained profile by word size; HLC drift bound replaces timestamp-based rejection; node kinds reduced to file, dir, symlink with a separate content mode and defined mode switching; symlinks always opaque; checkout safety rules (§7.7); workspace conflicts (§9.7) replace the case-folding tree conflict and cover normalization, reserved names, and path length; operation log entries suppressed for empty snapshots.
781 803
…
3 unchanged lines
modified src/main.rs +6 −0
…
84 unchanged lines
85 85
}
86 86
87 87
fn main() {
88 +
    // Behave like other CLI tools when output is piped into `head`: die
89 +
    // quietly on SIGPIPE instead of panicking on a failed write.
90 +
    #[cfg(unix)]
91 +
    unsafe {
92 +
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
93 +
    }
88 94
    match run() {
89 95
        Ok(code) => std::process::exit(code),
90 96
        Err(e) => {
…
228 unchanged lines