@openlfcp/shared-objects 0.2.0-beta.2 → 0.2.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,15 +29,18 @@ export interface CheckedChange {
29
29
  */
30
30
  export declare function checkChange(bytes: Uint8Array): CheckedChange;
31
31
  /**
32
- * The change hash of bytes checkChange refused, read without decoding
33
- * them: the SHA-256 of the chunk from its type byte on (the Automerge
34
- * change hash). A change above a §11.1 limit is named this way, so a
35
- * receiver can report it without expanding it. Undefined when the bytes
36
- * are not one change chunk that could be read: no magic bytes, another
37
- * chunk type, a length that does not cover them exactly, or a header or
38
- * column cut short.
32
+ * The change hash of bytes checkChange refused, read without decoding them:
33
+ * the SHA-256 of the chunk from its type byte on (the Automerge change hash).
34
+ * §14.1 names a refused change by this hash whenever the bytes are one change
35
+ * chunk of type 1 whose length field covers them exactly and whose checksum
36
+ * matches — the inner header is never read, so a change above a §11.1 limit
37
+ * (named without being expanded) and one whose header holds a length running
38
+ * past its end (finding C3) are both named. Undefined when the bytes are not
39
+ * such a chunk: no magic bytes, another chunk type (a compressed chunk
40
+ * included), a length that does not cover them exactly, or a wrong checksum
41
+ * (whose name would be its intact copy's, finding C1).
39
42
  */
40
- export declare function refusedChangeHash(bytes: Uint8Array, error: unknown): string | undefined;
43
+ export declare function refusedChangeHash(bytes: Uint8Array): string | undefined;
41
44
  /** The change as its canonical bytes give it (§11.3), reusing the walk of checkChange. */
42
45
  export declare function parsedOf(change: CheckedChange): ParsedChange;
43
46
  /** The decoded change, reusing the decode of checkChange when there was one. */
@@ -3,7 +3,7 @@ import { LfcpError, toHex } from "@openlfcp/core";
3
3
  import { sha256 } from "@openlfcp/crypto";
4
4
  import { ProfileInvalidError } from "../profile-invalid.js";
5
5
  import { checkCanonicalChange } from "./canonical.js";
6
- import { checkChangeExpansion, MalformedChunkError } from "./limits.js";
6
+ import { checkChangeExpansion } from "./limits.js";
7
7
  /**
8
8
  * Automerge byte checks of the SHARED-OBJECTS-PROFILE-01 payloads (LFCP-031):
9
9
  * a Data Unit plaintext carries exactly one Automerge change (§11, §12), a
@@ -70,17 +70,18 @@ export function checkChange(bytes) {
70
70
  return checked;
71
71
  }
72
72
  /**
73
- * The change hash of bytes checkChange refused, read without decoding
74
- * them: the SHA-256 of the chunk from its type byte on (the Automerge
75
- * change hash). A change above a §11.1 limit is named this way, so a
76
- * receiver can report it without expanding it. Undefined when the bytes
77
- * are not one change chunk that could be read: no magic bytes, another
78
- * chunk type, a length that does not cover them exactly, or a header or
79
- * column cut short.
73
+ * The change hash of bytes checkChange refused, read without decoding them:
74
+ * the SHA-256 of the chunk from its type byte on (the Automerge change hash).
75
+ * §14.1 names a refused change by this hash whenever the bytes are one change
76
+ * chunk of type 1 whose length field covers them exactly and whose checksum
77
+ * matches — the inner header is never read, so a change above a §11.1 limit
78
+ * (named without being expanded) and one whose header holds a length running
79
+ * past its end (finding C3) are both named. Undefined when the bytes are not
80
+ * such a chunk: no magic bytes, another chunk type (a compressed chunk
81
+ * included), a length that does not cover them exactly, or a wrong checksum
82
+ * (whose name would be its intact copy's, finding C1).
80
83
  */
81
- export function refusedChangeHash(bytes, error) {
82
- if (error instanceof MalformedChunkError)
83
- return undefined;
84
+ export function refusedChangeHash(bytes) {
84
85
  if (bytes.length < HEADER || MAGIC.some((b, i) => bytes[i] !== b))
85
86
  return undefined;
86
87
  if (bytes[8] !== CHUNK_CHANGE)
@@ -100,7 +101,16 @@ export function refusedChangeHash(bytes, error) {
100
101
  }
101
102
  if (pos + length !== bytes.length)
102
103
  return undefined;
103
- return toHex(sha256(bytes.subarray(8)));
104
+ // §14.1 (baseline.7, finding C1): name the bytes only when their checksum
105
+ // matches. The name is the SHA-256 of the chunk from its type byte on, and
106
+ // the checksum (bytes 4 to 8) is the first four bytes of that same hash; a
107
+ // chunk with a wrong checksum would be named by its intact copy's hash, so
108
+ // it is left unnamed and the intact copy is checked as any change is.
109
+ const hash = sha256(bytes.subarray(8));
110
+ for (let i = 0; i < 4; i++)
111
+ if (bytes[4 + i] !== hash[i])
112
+ return undefined;
113
+ return toHex(hash);
104
114
  }
105
115
  /** The operations checkChange read from the bytes (§11.3), for the references of §11.4. */
106
116
  const PARSED = new WeakMap();
@@ -292,7 +292,7 @@ export class SectionReplica {
292
292
  }
293
293
  catch (e) {
294
294
  // Never decoded: a change refused by the checks is named from its bytes only.
295
- refused.push(refusal(index, refusedChangeHash(unit.bytes, e), e, false));
295
+ refused.push(refusal(index, refusedChangeHash(unit.bytes), e, false));
296
296
  return;
297
297
  }
298
298
  if (unit.signer !== undefined &&
@@ -189,8 +189,9 @@ export function deriveTree(doc, validation = validateSection(doc)) {
189
189
  * the node or of an ancestor, from the change history. A change deletes
190
190
  * when the last lifecycle value it writes to an owner (a Task node's Task,
191
191
  * another node itself) is "deleted"; content is a node's creation, an
192
- * operation on its Text, or a Task's title or status. Two changes are
193
- * concurrent when neither is in the other's dependencies.
192
+ * operation on its Text, a Task's title or status, or a move of the node
193
+ * (a write to its `placement` register — node.move or node.resolve_placement,
194
+ * §8). Two changes are concurrent when neither is in the other's dependencies.
194
195
  */
195
196
  function retainedEdits(doc, hidden, parentOf, sectionId) {
196
197
  const root = doc;
@@ -201,8 +202,13 @@ function retainedEdits(doc, hidden, parentOf, sectionId) {
201
202
  const textOf = new Map();
202
203
  const ownerOf = new Map();
203
204
  const taskOf = new Map();
205
+ // The object ID of each node's own map, for a move written to its `placement`.
206
+ const nodeMapOf = new Map();
204
207
  for (const id of Object.keys(nodes)) {
205
208
  const n = nodes[id];
209
+ const nodeMap = A.getObjectId(n);
210
+ if (nodeMap !== null)
211
+ nodeMapOf.set(nodeMap, id);
206
212
  const text = A.getObjectId(n, "text");
207
213
  if (text !== null)
208
214
  textOf.set(text, id);
@@ -236,6 +242,12 @@ function retainedEdits(doc, hidden, parentOf, sectionId) {
236
242
  const task = taskOf.get(op.obj);
237
243
  if (task !== undefined && hidden.has(task) && (key === "title" || key === "status"))
238
244
  touched.add(task);
245
+ // §8, §9: a move writes the node's `placement` register (node.move or
246
+ // node.resolve_placement). A creation also writes it and is already
247
+ // counted above; what this adds is a move of an existing node.
248
+ const moved = nodeMapOf.get(op.obj);
249
+ if (moved !== undefined && key === "placement" && hidden.has(moved))
250
+ touched.add(moved);
239
251
  const owner = ownerOf.get(op.obj);
240
252
  if (owner !== undefined && key === "lifecycle")
241
253
  lastLifecycle.set(owner, value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/shared-objects",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.0-beta.3",
4
4
  "description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
5
5
  "keywords": [
6
6
  "openlfcp",
@@ -53,8 +53,8 @@
53
53
  "dependencies": {
54
54
  "@automerge/automerge": "3.5.0",
55
55
  "fflate": "0.8.3",
56
- "@openlfcp/core": "^0.2.0-beta.2",
57
- "@openlfcp/crypto": "^0.2.0-beta.2"
56
+ "@openlfcp/core": "^0.2.0-beta.3",
57
+ "@openlfcp/crypto": "^0.2.0-beta.3"
58
58
  },
59
59
  "devDependencies": {
60
60
  "fast-check": "4.10.2"