@openlfcp/shared-objects 0.1.1 → 0.1.2
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.
- package/README.md +2 -2
- package/package.json +3 -3
- package/dist/automerge-bytes.d.ts +0 -37
- package/dist/automerge-bytes.js +0 -95
- package/dist/chunk-limits.d.ts +0 -58
- package/dist/chunk-limits.js +0 -457
- package/dist/data-profile.d.ts +0 -159
- package/dist/data-profile.js +0 -311
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -15
- package/dist/profile-invalid.d.ts +0 -12
- package/dist/profile-invalid.js +0 -15
- package/dist/replica.d.ts +0 -277
- package/dist/replica.js +0 -1039
- package/dist/task.d.ts +0 -159
- package/dist/task.js +0 -192
- package/dist/validate.d.ts +0 -70
- package/dist/validate.js +0 -196
- package/dist/values.d.ts +0 -52
- package/dist/values.js +0 -163
- package/dist/wasm.d.ts +0 -33
- package/dist/wasm.js +0 -58
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ reaches the replica (§95). The replica has no Markdown or editor concepts.
|
|
|
20
20
|
|
|
21
21
|
Part of the OpenLFCP TypeScript SDK, which implements the OpenLFCP MVP 0.1
|
|
22
22
|
subset of [LFCP-WIRE-01](https://github.com/openlfcp/spec/blob/main/wire/LFCP-WIRE-01.md)
|
|
23
|
-
at `mvp-0.1-baseline.
|
|
23
|
+
at `mvp-0.1-baseline.9`, not every deferred WIRE-01 feature. It does not
|
|
24
24
|
claim full LFCP-WIRE-01 conformance. This is the MVP 0.1 release
|
|
25
|
-
(`0.1.
|
|
25
|
+
(`0.1.2`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
|
|
26
26
|
|
|
27
27
|
## Automerge and WebAssembly
|
|
28
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@automerge/automerge": "3.5.0",
|
|
47
47
|
"fflate": "0.8.3",
|
|
48
|
-
"@openlfcp/core": "^0.1.
|
|
49
|
-
"@openlfcp/crypto": "^0.1.
|
|
48
|
+
"@openlfcp/core": "^0.1.2",
|
|
49
|
+
"@openlfcp/crypto": "^0.1.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"fast-check": "4.10.2"
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/** A checked Automerge change: its bytes and decoded header. */
|
|
2
|
-
export interface CheckedChange {
|
|
3
|
-
readonly bytes: Uint8Array;
|
|
4
|
-
/** Hex SHA-256 change hash (the Automerge change identity). */
|
|
5
|
-
readonly hash: string;
|
|
6
|
-
/** Hex actor ID. */
|
|
7
|
-
readonly actor: string;
|
|
8
|
-
readonly seq: number;
|
|
9
|
-
/** Hex hashes of the changes this one depends on. */
|
|
10
|
-
readonly deps: readonly string[];
|
|
11
|
-
/** Hex IDs of the other actors the change refers to (§11.1: each must be known to the document). */
|
|
12
|
-
readonly otherActors: readonly string[];
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Checks that `bytes` is exactly one valid Automerge change (§11, §12) and
|
|
16
|
-
* returns it decoded. Rejects with PROFILE_INVALID / INVALID_AUTOMERGE_BYTES: a chunk that is not a
|
|
17
|
-
* change (a document chunk is a full save, §13), bytes Automerge cannot
|
|
18
|
-
* parse, trailing bytes such as a second concatenated change, and a
|
|
19
|
-
* checksum that does not match the change hash. Automerge JS 3.5.0 parses
|
|
20
|
-
* a change without verifying its checksum, so the check is done here.
|
|
21
|
-
*/
|
|
22
|
-
export declare function checkChange(bytes: Uint8Array): CheckedChange;
|
|
23
|
-
/**
|
|
24
|
-
* Checks that `bytes` starts as an Automerge full save (a document chunk,
|
|
25
|
-
* §13). Whether the whole image loads, checksums included, is decided by
|
|
26
|
-
* Automerge's load.
|
|
27
|
-
*/
|
|
28
|
-
export declare function checkSaveHeader(bytes: Uint8Array): void;
|
|
29
|
-
/** §11: the Data Unit plaintext [1, change] of one checked Automerge change. */
|
|
30
|
-
export declare function frameChange(change: Uint8Array): Uint8Array;
|
|
31
|
-
/** §11: the one Automerge change a Data Unit plaintext carries, checked. Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
32
|
-
export declare function unframeChange(plaintext: Uint8Array): CheckedChange;
|
|
33
|
-
/** §13: the Snapshot plaintext [1, save] of an Automerge full save. */
|
|
34
|
-
export declare function frameSnapshot(save: Uint8Array): Uint8Array;
|
|
35
|
-
/** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
36
|
-
export declare function unframeSnapshot(plaintext: Uint8Array): Uint8Array;
|
|
37
|
-
//# sourceMappingURL=automerge-bytes.d.ts.map
|
package/dist/automerge-bytes.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import * as A from "@automerge/automerge";
|
|
2
|
-
import { toHex } from "@openlfcp/core";
|
|
3
|
-
import { checkChangeExpansion } from "./chunk-limits.js";
|
|
4
|
-
import { ProfileInvalidError } from "./profile-invalid.js";
|
|
5
|
-
import { frameProfilePayload, unframeProfilePayload } from "./values.js";
|
|
6
|
-
/**
|
|
7
|
-
* Automerge byte checks of the SHARED-OBJECTS-PROFILE-01 payloads (LFCP-031):
|
|
8
|
-
* a Data Unit plaintext carries exactly one Automerge change (§11, §12), a
|
|
9
|
-
* Snapshot plaintext one Automerge full save (§13). The framing itself is
|
|
10
|
-
* frameProfilePayload / unframeProfilePayload (LFCP-030); this adds the
|
|
11
|
-
* check that the framed bytes are what they claim to be.
|
|
12
|
-
*
|
|
13
|
-
* Automerge storage chunks start with the magic bytes 85 6f 4a 83, a 4-byte
|
|
14
|
-
* checksum (the first 4 bytes of the chunk hash) and a chunk type: 0 for a
|
|
15
|
-
* document (a full save), 1 for a change, 2 for a compressed change.
|
|
16
|
-
*/
|
|
17
|
-
const MAGIC = [0x85, 0x6f, 0x4a, 0x83];
|
|
18
|
-
const CHUNK_DOCUMENT = 0;
|
|
19
|
-
const CHUNK_CHANGE = 1;
|
|
20
|
-
const CHUNK_COMPRESSED_CHANGE = 2;
|
|
21
|
-
const HEADER = 9; // magic, checksum, chunk type
|
|
22
|
-
/** §11, §13, §74.1: PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
23
|
-
const reject = (why) => {
|
|
24
|
-
throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", why);
|
|
25
|
-
};
|
|
26
|
-
function chunkType(bytes, what) {
|
|
27
|
-
if (bytes.length < HEADER || MAGIC.some((b, i) => bytes[i] !== b))
|
|
28
|
-
return reject(`${what} is not an Automerge chunk`);
|
|
29
|
-
return bytes[8];
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Checks that `bytes` is exactly one valid Automerge change (§11, §12) and
|
|
33
|
-
* returns it decoded. Rejects with PROFILE_INVALID / INVALID_AUTOMERGE_BYTES: a chunk that is not a
|
|
34
|
-
* change (a document chunk is a full save, §13), bytes Automerge cannot
|
|
35
|
-
* parse, trailing bytes such as a second concatenated change, and a
|
|
36
|
-
* checksum that does not match the change hash. Automerge JS 3.5.0 parses
|
|
37
|
-
* a change without verifying its checksum, so the check is done here.
|
|
38
|
-
*/
|
|
39
|
-
export function checkChange(bytes) {
|
|
40
|
-
const type = chunkType(bytes, "the change");
|
|
41
|
-
if (type === CHUNK_COMPRESSED_CHANGE)
|
|
42
|
-
reject("a compressed Automerge change (chunk type 2): a change is uncompressed (§11)");
|
|
43
|
-
if (type !== CHUNK_CHANGE)
|
|
44
|
-
reject(`chunk type ${type} is not an Automerge change (§11)`);
|
|
45
|
-
// §11.1: what the change expands to and its structure, before Automerge decodes it.
|
|
46
|
-
const expansion = checkChangeExpansion(bytes);
|
|
47
|
-
let decoded;
|
|
48
|
-
try {
|
|
49
|
-
decoded = A.decodeChange(bytes);
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
return reject(`invalid Automerge change bytes (§11): ${e.message}`);
|
|
53
|
-
}
|
|
54
|
-
if (toHex(bytes.subarray(4, 8)) !== decoded.hash.slice(0, 8))
|
|
55
|
-
reject("the Automerge change checksum does not match its hash (§11)");
|
|
56
|
-
return Object.freeze({
|
|
57
|
-
bytes: Uint8Array.from(bytes),
|
|
58
|
-
hash: decoded.hash,
|
|
59
|
-
actor: decoded.actor,
|
|
60
|
-
seq: decoded.seq,
|
|
61
|
-
deps: Object.freeze([...decoded.deps]),
|
|
62
|
-
otherActors: expansion.otherActors,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Checks that `bytes` starts as an Automerge full save (a document chunk,
|
|
67
|
-
* §13). Whether the whole image loads, checksums included, is decided by
|
|
68
|
-
* Automerge's load.
|
|
69
|
-
*/
|
|
70
|
-
export function checkSaveHeader(bytes) {
|
|
71
|
-
const type = chunkType(bytes, "the Snapshot image");
|
|
72
|
-
if (type !== CHUNK_DOCUMENT)
|
|
73
|
-
reject(`chunk type ${type} is not an Automerge document (full save, §13)`);
|
|
74
|
-
}
|
|
75
|
-
/** §11: the Data Unit plaintext [1, change] of one checked Automerge change. */
|
|
76
|
-
export function frameChange(change) {
|
|
77
|
-
checkChange(change);
|
|
78
|
-
return frameProfilePayload(change);
|
|
79
|
-
}
|
|
80
|
-
/** §11: the one Automerge change a Data Unit plaintext carries, checked. Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
81
|
-
export function unframeChange(plaintext) {
|
|
82
|
-
return checkChange(unframeProfilePayload(plaintext));
|
|
83
|
-
}
|
|
84
|
-
/** §13: the Snapshot plaintext [1, save] of an Automerge full save. */
|
|
85
|
-
export function frameSnapshot(save) {
|
|
86
|
-
checkSaveHeader(save);
|
|
87
|
-
return frameProfilePayload(save);
|
|
88
|
-
}
|
|
89
|
-
/** §13: the Automerge full save a Snapshot plaintext carries (header checked). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES. */
|
|
90
|
-
export function unframeSnapshot(plaintext) {
|
|
91
|
-
const save = unframeProfilePayload(plaintext);
|
|
92
|
-
checkSaveHeader(save);
|
|
93
|
-
return save;
|
|
94
|
-
}
|
|
95
|
-
//# sourceMappingURL=automerge-bytes.js.map
|
package/dist/chunk-limits.d.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/** The exact limits of a change (§11.1): writers stay within them, receivers reject above. */
|
|
2
|
-
export declare const CHANGE_LIMITS: Readonly<{
|
|
3
|
-
maxRows: 16384;
|
|
4
|
-
maxGroupSum: 262144;
|
|
5
|
-
maxStringBytes: number;
|
|
6
|
-
maxDeps: 1024;
|
|
7
|
-
maxActors: 1024;
|
|
8
|
-
}>;
|
|
9
|
-
/**
|
|
10
|
-
* The floor of a Snapshot's limits (§13.1): every receiver accepts at least
|
|
11
|
-
* these. A receiver MAY configure higher limits; above its own it rejects
|
|
12
|
-
* the Snapshot before the engine and falls back to the data round.
|
|
13
|
-
*/
|
|
14
|
-
export declare const SNAPSHOT_LIMITS_FLOOR: Readonly<{
|
|
15
|
-
maxRows: 262144;
|
|
16
|
-
maxGroupSum: 262144;
|
|
17
|
-
maxStringBytes: number;
|
|
18
|
-
maxInflatedBytes: number;
|
|
19
|
-
maxDeps: 1024;
|
|
20
|
-
maxActors: 1024;
|
|
21
|
-
}>;
|
|
22
|
-
export type SnapshotLimits = typeof SNAPSHOT_LIMITS_FLOOR;
|
|
23
|
-
/** §11.2: no object of a document is deeper than this (the root is depth 0). */
|
|
24
|
-
export declare const MAX_DOCUMENT_DEPTH = 256;
|
|
25
|
-
/** The operation actions that create an object (§11.2): makeMap, makeList, makeText, makeTable. */
|
|
26
|
-
export declare const OBJECT_ACTIONS: ReadonlySet<number>;
|
|
27
|
-
/** What a chunk expands to (for tests and diagnostics). */
|
|
28
|
-
export interface ChunkExpansion {
|
|
29
|
-
/** The largest value count of any column (a change's op count is its action column's). */
|
|
30
|
-
readonly maxRows: number;
|
|
31
|
-
readonly groupSum: number;
|
|
32
|
-
readonly stringBytes: number;
|
|
33
|
-
/** Column data bytes after inflation (a change has no deflated columns). */
|
|
34
|
-
readonly columnBytes: number;
|
|
35
|
-
/** Each column's spec (id << 4 | deflate << 3 | type) and value count, in chunk order. */
|
|
36
|
-
readonly columns: readonly {
|
|
37
|
-
readonly spec: number;
|
|
38
|
-
readonly rows: number;
|
|
39
|
-
}[];
|
|
40
|
-
/** A change's other actors (hex), in order: actor index i + 1 (§11.1). Empty for a Snapshot. */
|
|
41
|
-
readonly otherActors: readonly string[];
|
|
42
|
-
/** A Snapshot's deepest object (§11.2); 0 for a change (its depths depend on the document). */
|
|
43
|
-
readonly maxDepth: number;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* §11.1: checks an uncompressed change chunk (type 1) against the exact
|
|
47
|
-
* change limits before the engine sees it. Throws PROFILE_INVALID /
|
|
48
|
-
* INVALID_AUTOMERGE_BYTES; returns what it expands to.
|
|
49
|
-
*/
|
|
50
|
-
export declare function checkChangeExpansion(bytes: Uint8Array): ChunkExpansion;
|
|
51
|
-
/**
|
|
52
|
-
* §13.1: checks a Snapshot's save (exactly one document chunk) against
|
|
53
|
-
* `limits` (at least SNAPSHOT_LIMITS_FLOOR) before the engine sees it,
|
|
54
|
-
* inflating deflated columns under a running cap. Throws PROFILE_INVALID /
|
|
55
|
-
* INVALID_AUTOMERGE_BYTES.
|
|
56
|
-
*/
|
|
57
|
-
export declare function checkSnapshotExpansion(bytes: Uint8Array, limits?: SnapshotLimits): ChunkExpansion;
|
|
58
|
-
//# sourceMappingURL=chunk-limits.d.ts.map
|
package/dist/chunk-limits.js
DELETED
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
// Expansion limits for Automerge chunks (SHARED-OBJECTS-PROFILE-01 §11.1,
|
|
2
|
-
// §13.1; SPEC-PATCH-07). Automerge's columnar format run-length encodes its
|
|
3
|
-
// columns, and a Snapshot's columns may also be deflated, so a few bytes can
|
|
4
|
-
// declare millions of operations or gigabytes of data: a 112-byte change
|
|
5
|
-
// expanded to 1,000,000 operations and 1.2 GiB in Automerge JS 3.5.0. This
|
|
6
|
-
// walker reads a chunk's structure and the run headers of every column,
|
|
7
|
-
// without materialising any value, and refuses a chunk over the limits
|
|
8
|
-
// BEFORE the Automerge engine sees it. It runs in time linear in the input
|
|
9
|
-
// (plus the capped inflation of a Snapshot's deflated columns).
|
|
10
|
-
//
|
|
11
|
-
// Counting rules (the profile states them; both SDKs count the same way):
|
|
12
|
-
// - every column's value count (its rows): an RLE run of n adds n, a
|
|
13
|
-
// literal run of n adds n, a null run of n adds n; a boolean column adds
|
|
14
|
-
// each run length; a raw value column adds nothing;
|
|
15
|
-
// - every group column's sum (the total of its values: pred and succ
|
|
16
|
-
// entries);
|
|
17
|
-
// - expanded string bytes: each string value counted once per row it
|
|
18
|
-
// occupies (an RLE run of n copies of a k-byte string adds n * k);
|
|
19
|
-
// - structure: no two columns with one specification, every actor index
|
|
20
|
-
// below the number of actors, and (a change) no group value above one
|
|
21
|
-
// plus the number of other actors (an op's predecessors have distinct
|
|
22
|
-
// actors);
|
|
23
|
-
// - for a Snapshot, the bytes of all column data after inflation.
|
|
24
|
-
import { Inflate } from "fflate";
|
|
25
|
-
import { ProfileInvalidError } from "./profile-invalid.js";
|
|
26
|
-
/** The exact limits of a change (§11.1): writers stay within them, receivers reject above. */
|
|
27
|
-
export const CHANGE_LIMITS = Object.freeze({
|
|
28
|
-
maxRows: 16_384,
|
|
29
|
-
maxGroupSum: 262_144,
|
|
30
|
-
maxStringBytes: 4 * 1024 * 1024,
|
|
31
|
-
maxDeps: 1_024,
|
|
32
|
-
maxActors: 1_024,
|
|
33
|
-
});
|
|
34
|
-
/**
|
|
35
|
-
* The floor of a Snapshot's limits (§13.1): every receiver accepts at least
|
|
36
|
-
* these. A receiver MAY configure higher limits; above its own it rejects
|
|
37
|
-
* the Snapshot before the engine and falls back to the data round.
|
|
38
|
-
*/
|
|
39
|
-
export const SNAPSHOT_LIMITS_FLOOR = Object.freeze({
|
|
40
|
-
maxRows: 262_144,
|
|
41
|
-
maxGroupSum: 262_144,
|
|
42
|
-
maxStringBytes: 32 * 1024 * 1024,
|
|
43
|
-
maxInflatedBytes: 32 * 1024 * 1024,
|
|
44
|
-
maxDeps: 1_024,
|
|
45
|
-
maxActors: 1_024,
|
|
46
|
-
});
|
|
47
|
-
/** §11.2: no object of a document is deeper than this (the root is depth 0). */
|
|
48
|
-
export const MAX_DOCUMENT_DEPTH = 256;
|
|
49
|
-
/** The operation actions that create an object (§11.2): makeMap, makeList, makeText, makeTable. */
|
|
50
|
-
export const OBJECT_ACTIONS = new Set([0, 2, 4, 6]);
|
|
51
|
-
const MAGIC = [0x85, 0x6f, 0x4a, 0x83];
|
|
52
|
-
const CHUNK_DOCUMENT = 0;
|
|
53
|
-
const CHUNK_CHANGE = 1;
|
|
54
|
-
const DEFLATE_BIT = 0x08;
|
|
55
|
-
const TYPE_GROUP = 0;
|
|
56
|
-
const TYPE_ACTOR = 1;
|
|
57
|
-
const TYPE_DELTA = 3;
|
|
58
|
-
const TYPE_BOOLEAN = 4;
|
|
59
|
-
const TYPE_STRING = 5;
|
|
60
|
-
const TYPE_RAW = 7;
|
|
61
|
-
/** Input fed to the inflater per push: each push yields at most ~1,032x its size. */
|
|
62
|
-
const INFLATE_SLICE = 1024;
|
|
63
|
-
const refuse = (why) => {
|
|
64
|
-
throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `Automerge chunk refused (§11.1): ${why}`);
|
|
65
|
-
};
|
|
66
|
-
class Cursor {
|
|
67
|
-
bytes;
|
|
68
|
-
end;
|
|
69
|
-
pos = 0;
|
|
70
|
-
constructor(bytes, end = bytes.length) {
|
|
71
|
-
this.bytes = bytes;
|
|
72
|
-
this.end = end;
|
|
73
|
-
}
|
|
74
|
-
get done() {
|
|
75
|
-
return this.pos >= this.end;
|
|
76
|
-
}
|
|
77
|
-
take(n) {
|
|
78
|
-
if (n > this.end - this.pos)
|
|
79
|
-
refuse("a length runs past the end");
|
|
80
|
-
const out = this.bytes.subarray(this.pos, this.pos + n);
|
|
81
|
-
this.pos += n;
|
|
82
|
-
return out;
|
|
83
|
-
}
|
|
84
|
-
/** Unsigned LEB128 as a number; values at or above 2^53 come back as Infinity (over every limit). */
|
|
85
|
-
uleb() {
|
|
86
|
-
let value = 0;
|
|
87
|
-
let scale = 1;
|
|
88
|
-
for (let i = 0; i < 10; i++) {
|
|
89
|
-
if (this.pos >= this.end)
|
|
90
|
-
refuse("a LEB128 number runs past the end");
|
|
91
|
-
const b = this.bytes[this.pos++];
|
|
92
|
-
value += (b & 0x7f) * scale;
|
|
93
|
-
if ((b & 0x80) === 0)
|
|
94
|
-
return value >= 2 ** 53 ? Number.POSITIVE_INFINITY : value;
|
|
95
|
-
scale *= 128;
|
|
96
|
-
}
|
|
97
|
-
return refuse("a LEB128 number is longer than 10 bytes");
|
|
98
|
-
}
|
|
99
|
-
/** Signed LEB128 as a number; magnitudes at or above 2^53 come back as +/-Infinity. */
|
|
100
|
-
sleb() {
|
|
101
|
-
let value = 0;
|
|
102
|
-
let scale = 1;
|
|
103
|
-
for (let i = 0; i < 10; i++) {
|
|
104
|
-
if (this.pos >= this.end)
|
|
105
|
-
refuse("a LEB128 number runs past the end");
|
|
106
|
-
const b = this.bytes[this.pos++];
|
|
107
|
-
value += (b & 0x7f) * scale;
|
|
108
|
-
scale *= 128;
|
|
109
|
-
if ((b & 0x80) === 0) {
|
|
110
|
-
if (b & 0x40)
|
|
111
|
-
value -= scale;
|
|
112
|
-
return Math.abs(value) >= 2 ** 53 ? Math.sign(value) * Number.POSITIVE_INFINITY : value;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return refuse("a LEB128 number is longer than 10 bytes");
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
class Tally {
|
|
119
|
-
limits;
|
|
120
|
-
maxRows = 0;
|
|
121
|
-
groupSum = 0;
|
|
122
|
-
stringBytes = 0;
|
|
123
|
-
columnBytes = 0;
|
|
124
|
-
constructor(limits) {
|
|
125
|
-
this.limits = limits;
|
|
126
|
-
}
|
|
127
|
-
rows(column, limit) {
|
|
128
|
-
if (column > limit)
|
|
129
|
-
refuse(`a column has more than ${limit} values`);
|
|
130
|
-
if (column > this.maxRows)
|
|
131
|
-
this.maxRows = column;
|
|
132
|
-
}
|
|
133
|
-
group(add) {
|
|
134
|
-
this.groupSum += add;
|
|
135
|
-
if (!(this.groupSum <= this.limits.maxGroupSum))
|
|
136
|
-
refuse(`group columns sum to more than ${this.limits.maxGroupSum}`);
|
|
137
|
-
}
|
|
138
|
-
strings(add) {
|
|
139
|
-
this.stringBytes += add;
|
|
140
|
-
if (!(this.stringBytes <= this.limits.maxStringBytes))
|
|
141
|
-
refuse(`strings expand to more than ${this.limits.maxStringBytes} bytes`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
/** Counts one column's values (and group sums, string bytes) from its run headers. */
|
|
145
|
-
function countColumn(type, data, tally, bounds) {
|
|
146
|
-
if (type === TYPE_RAW)
|
|
147
|
-
return 0;
|
|
148
|
-
const c = new Cursor(data);
|
|
149
|
-
let rows = 0;
|
|
150
|
-
const add = (n) => {
|
|
151
|
-
rows += n;
|
|
152
|
-
tally.rows(rows, bounds.rows);
|
|
153
|
-
};
|
|
154
|
-
if (type === TYPE_BOOLEAN) {
|
|
155
|
-
while (!c.done)
|
|
156
|
-
add(c.uleb());
|
|
157
|
-
return rows;
|
|
158
|
-
}
|
|
159
|
-
// One value of an RLE column: its group count, its string length, or skipped.
|
|
160
|
-
const value = () => {
|
|
161
|
-
if (type === TYPE_STRING) {
|
|
162
|
-
const len = c.uleb();
|
|
163
|
-
c.take(len);
|
|
164
|
-
return len;
|
|
165
|
-
}
|
|
166
|
-
if (type === TYPE_DELTA)
|
|
167
|
-
return c.sleb();
|
|
168
|
-
return c.uleb();
|
|
169
|
-
};
|
|
170
|
-
const account = (v, times) => {
|
|
171
|
-
if (type === TYPE_GROUP) {
|
|
172
|
-
if (v > bounds.group)
|
|
173
|
-
refuse("an operation has more predecessors than the change has actors");
|
|
174
|
-
tally.group(v * times);
|
|
175
|
-
}
|
|
176
|
-
else if (type === TYPE_STRING)
|
|
177
|
-
tally.strings(v * times);
|
|
178
|
-
else if (type === TYPE_ACTOR && !(v < bounds.actors))
|
|
179
|
-
refuse("an actor index is out of range");
|
|
180
|
-
};
|
|
181
|
-
while (!c.done) {
|
|
182
|
-
const header = c.sleb();
|
|
183
|
-
if (header > 0) {
|
|
184
|
-
add(header);
|
|
185
|
-
account(value(), header);
|
|
186
|
-
}
|
|
187
|
-
else if (header < 0) {
|
|
188
|
-
const n = -header;
|
|
189
|
-
add(n); // refuses before reading an oversized literal run
|
|
190
|
-
for (let i = 0; i < n; i++)
|
|
191
|
-
account(value(), 1);
|
|
192
|
-
}
|
|
193
|
-
else
|
|
194
|
-
add(c.uleb());
|
|
195
|
-
}
|
|
196
|
-
return rows;
|
|
197
|
-
}
|
|
198
|
-
/** Column metadata. The count has no limit of its own (§11.1): each entry takes at least two bytes. */
|
|
199
|
-
function columnMetas(c) {
|
|
200
|
-
const count = c.uleb();
|
|
201
|
-
if (count > (c.end - c.pos) / 2)
|
|
202
|
-
refuse("the column metadata runs past the end");
|
|
203
|
-
const out = [];
|
|
204
|
-
const specs = new Set();
|
|
205
|
-
for (let i = 0; i < count; i++) {
|
|
206
|
-
const spec = c.uleb();
|
|
207
|
-
if (specs.has(spec))
|
|
208
|
-
refuse("two columns share a specification");
|
|
209
|
-
specs.add(spec);
|
|
210
|
-
out.push({ spec, length: c.uleb() });
|
|
211
|
-
}
|
|
212
|
-
return out;
|
|
213
|
-
}
|
|
214
|
-
function lengthPrefixedList(c, limit, what, fixed) {
|
|
215
|
-
const count = c.uleb();
|
|
216
|
-
if (count > limit)
|
|
217
|
-
refuse(`more than ${limit} ${what}`);
|
|
218
|
-
const out = [];
|
|
219
|
-
for (let i = 0; i < count; i++)
|
|
220
|
-
out.push(c.take(fixed ?? c.uleb()));
|
|
221
|
-
return out;
|
|
222
|
-
}
|
|
223
|
-
const toHexString = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
|
|
224
|
-
/**
|
|
225
|
-
* Each column's value limit: a column that shares its id with a group
|
|
226
|
-
* column holds that group's entries (predecessors, successors, a change's
|
|
227
|
-
* dependencies), so the group sum bounds it; every other column has the
|
|
228
|
-
* row limit.
|
|
229
|
-
*/
|
|
230
|
-
function rowLimits(metas, limits) {
|
|
231
|
-
const grouped = new Set(metas.filter((m) => (m.spec & 7) === TYPE_GROUP).map((m) => m.spec >> 4));
|
|
232
|
-
return (spec) => grouped.has(spec >> 4) && (spec & 7) !== TYPE_GROUP ? limits.maxGroupSum : limits.maxRows;
|
|
233
|
-
}
|
|
234
|
-
/** The chunk's single body: magic, checksum, type, length; nothing may follow it. */
|
|
235
|
-
function body(bytes, want, what) {
|
|
236
|
-
const c = new Cursor(bytes);
|
|
237
|
-
const magic = c.take(4);
|
|
238
|
-
if (!MAGIC.every((b, i) => magic[i] === b))
|
|
239
|
-
refuse(`${what} has no Automerge magic bytes`);
|
|
240
|
-
c.take(4); // checksum: verified elsewhere (checkChange; the engine for a Snapshot)
|
|
241
|
-
const type = c.take(1)[0];
|
|
242
|
-
if (type !== want)
|
|
243
|
-
refuse(`${what} is chunk type ${type}, not ${want}`);
|
|
244
|
-
const length = c.uleb();
|
|
245
|
-
if (length !== bytes.length - c.pos)
|
|
246
|
-
refuse(`${what} must be exactly one chunk with nothing after it`);
|
|
247
|
-
return c;
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* §11.1: checks an uncompressed change chunk (type 1) against the exact
|
|
251
|
-
* change limits before the engine sees it. Throws PROFILE_INVALID /
|
|
252
|
-
* INVALID_AUTOMERGE_BYTES; returns what it expands to.
|
|
253
|
-
*/
|
|
254
|
-
export function checkChangeExpansion(bytes) {
|
|
255
|
-
const c = body(bytes, CHUNK_CHANGE, "a change");
|
|
256
|
-
const tally = new Tally(CHANGE_LIMITS);
|
|
257
|
-
lengthPrefixedList(c, CHANGE_LIMITS.maxDeps, "dependencies", 32);
|
|
258
|
-
c.take(c.uleb()); // actor
|
|
259
|
-
c.uleb(); // seq
|
|
260
|
-
c.uleb(); // start op
|
|
261
|
-
c.sleb(); // time
|
|
262
|
-
c.take(c.uleb()); // message
|
|
263
|
-
const otherActors = lengthPrefixedList(c, CHANGE_LIMITS.maxActors, "other actors");
|
|
264
|
-
const others = otherActors.length;
|
|
265
|
-
const metas = columnMetas(c);
|
|
266
|
-
const limitOf = rowLimits(metas, CHANGE_LIMITS);
|
|
267
|
-
const columns = [];
|
|
268
|
-
for (const m of metas) {
|
|
269
|
-
if (m.spec & DEFLATE_BIT)
|
|
270
|
-
refuse("a change column is deflated");
|
|
271
|
-
const bounds = { actors: 1 + others, group: 1 + others, rows: limitOf(m.spec) };
|
|
272
|
-
columns.push({ spec: m.spec, rows: countColumn(m.spec & 7, c.take(m.length), tally, bounds) });
|
|
273
|
-
tally.columnBytes += m.length;
|
|
274
|
-
}
|
|
275
|
-
// The rest of the chunk is the change's extra bytes: kept, not expanded.
|
|
276
|
-
return Object.freeze({
|
|
277
|
-
maxRows: tally.maxRows,
|
|
278
|
-
groupSum: tally.groupSum,
|
|
279
|
-
stringBytes: tally.stringBytes,
|
|
280
|
-
columnBytes: tally.columnBytes,
|
|
281
|
-
columns: Object.freeze(columns),
|
|
282
|
-
otherActors: Object.freeze(otherActors.map(toHexString)),
|
|
283
|
-
maxDepth: 0,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
/** Inflates raw DEFLATE data, refusing as soon as the output passes `budget` bytes. */
|
|
287
|
-
function inflateCapped(input, budget) {
|
|
288
|
-
const parts = [];
|
|
289
|
-
let total = 0;
|
|
290
|
-
let over = false;
|
|
291
|
-
let failed;
|
|
292
|
-
const inflater = new Inflate((chunk) => {
|
|
293
|
-
total += chunk.length;
|
|
294
|
-
if (total > budget)
|
|
295
|
-
over = true;
|
|
296
|
-
else
|
|
297
|
-
parts.push(chunk);
|
|
298
|
-
});
|
|
299
|
-
try {
|
|
300
|
-
for (let i = 0; i < input.length && !over; i += INFLATE_SLICE)
|
|
301
|
-
inflater.push(input.subarray(i, i + INFLATE_SLICE), i + INFLATE_SLICE >= input.length);
|
|
302
|
-
if (input.length === 0)
|
|
303
|
-
inflater.push(new Uint8Array(0), true);
|
|
304
|
-
}
|
|
305
|
-
catch (e) {
|
|
306
|
-
failed = e;
|
|
307
|
-
}
|
|
308
|
-
if (over)
|
|
309
|
-
refuse(`Snapshot columns inflate to more than ${budget} more bytes`);
|
|
310
|
-
if (failed !== undefined)
|
|
311
|
-
refuse("a deflated Snapshot column is not valid DEFLATE");
|
|
312
|
-
const out = new Uint8Array(total);
|
|
313
|
-
let o = 0;
|
|
314
|
-
for (const p of parts) {
|
|
315
|
-
out.set(p, o);
|
|
316
|
-
o += p.length;
|
|
317
|
-
}
|
|
318
|
-
return out;
|
|
319
|
-
}
|
|
320
|
-
/** The values of an actor (1), integer (2) or delta (3) column: null for a null row. */
|
|
321
|
-
function columnValues(type, data) {
|
|
322
|
-
const c = new Cursor(data);
|
|
323
|
-
const out = [];
|
|
324
|
-
let acc = 0;
|
|
325
|
-
const one = () => {
|
|
326
|
-
if (type === TYPE_DELTA) {
|
|
327
|
-
acc += c.sleb();
|
|
328
|
-
return acc;
|
|
329
|
-
}
|
|
330
|
-
return c.uleb();
|
|
331
|
-
};
|
|
332
|
-
while (!c.done) {
|
|
333
|
-
const header = c.sleb();
|
|
334
|
-
if (header > 0) {
|
|
335
|
-
if (type === TYPE_DELTA) {
|
|
336
|
-
const delta = c.sleb();
|
|
337
|
-
for (let i = 0; i < header; i++) {
|
|
338
|
-
acc += delta;
|
|
339
|
-
out.push(acc);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
else {
|
|
343
|
-
const v = c.uleb();
|
|
344
|
-
for (let i = 0; i < header; i++)
|
|
345
|
-
out.push(v);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
else if (header < 0)
|
|
349
|
-
for (let i = 0; i < -header; i++)
|
|
350
|
-
out.push(one());
|
|
351
|
-
else {
|
|
352
|
-
const nulls = c.uleb();
|
|
353
|
-
for (let i = 0; i < nulls; i++)
|
|
354
|
-
out.push(null);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return out;
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* §11.2: the deepest object of a document chunk, from its operation
|
|
361
|
-
* columns (object, operation ID, action), computed without recursion.
|
|
362
|
-
* Refuses an object deeper than MAX_DOCUMENT_DEPTH, a cycle, or an object
|
|
363
|
-
* written into one the document never created.
|
|
364
|
-
*/
|
|
365
|
-
function documentDepth(columns) {
|
|
366
|
-
const col = (spec) => {
|
|
367
|
-
const data = columns.get(spec);
|
|
368
|
-
return data === undefined ? [] : columnValues(spec & 7, data);
|
|
369
|
-
};
|
|
370
|
-
const action = col((4 << 4) | 2);
|
|
371
|
-
const objActor = col((0 << 4) | 1);
|
|
372
|
-
const objCtr = col((0 << 4) | 2);
|
|
373
|
-
const idActor = col((2 << 4) | 1);
|
|
374
|
-
const idCtr = col((2 << 4) | 3);
|
|
375
|
-
const ROOT = "_root";
|
|
376
|
-
const parent = new Map();
|
|
377
|
-
action.forEach((a, i) => {
|
|
378
|
-
if (a === null || !OBJECT_ACTIONS.has(a))
|
|
379
|
-
return;
|
|
380
|
-
const id = `${idCtr[i]}@${idActor[i]}`;
|
|
381
|
-
const oc = objCtr[i] ?? null;
|
|
382
|
-
parent.set(id, oc === null ? ROOT : `${oc}@${objActor[i]}`);
|
|
383
|
-
});
|
|
384
|
-
const depth = new Map([[ROOT, 0]]);
|
|
385
|
-
let deepest = 0;
|
|
386
|
-
for (const start of parent.keys()) {
|
|
387
|
-
const path = [];
|
|
388
|
-
let at = start;
|
|
389
|
-
while (!depth.has(at)) {
|
|
390
|
-
path.push(at);
|
|
391
|
-
if (path.length > MAX_DOCUMENT_DEPTH)
|
|
392
|
-
refuse(`an object is deeper than ${MAX_DOCUMENT_DEPTH} levels (§11.2)`);
|
|
393
|
-
const up = parent.get(at);
|
|
394
|
-
if (up === undefined)
|
|
395
|
-
refuse("an object is written into one the document never created (§11.2)");
|
|
396
|
-
at = up;
|
|
397
|
-
}
|
|
398
|
-
let d = depth.get(at);
|
|
399
|
-
for (let k = path.length - 1; k >= 0; k--) {
|
|
400
|
-
d += 1;
|
|
401
|
-
if (d > MAX_DOCUMENT_DEPTH)
|
|
402
|
-
refuse(`an object is deeper than ${MAX_DOCUMENT_DEPTH} levels (§11.2)`);
|
|
403
|
-
depth.set(path[k], d);
|
|
404
|
-
}
|
|
405
|
-
if (d > deepest)
|
|
406
|
-
deepest = d;
|
|
407
|
-
}
|
|
408
|
-
return deepest;
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* §13.1: checks a Snapshot's save (exactly one document chunk) against
|
|
412
|
-
* `limits` (at least SNAPSHOT_LIMITS_FLOOR) before the engine sees it,
|
|
413
|
-
* inflating deflated columns under a running cap. Throws PROFILE_INVALID /
|
|
414
|
-
* INVALID_AUTOMERGE_BYTES.
|
|
415
|
-
*/
|
|
416
|
-
export function checkSnapshotExpansion(bytes, limits = SNAPSHOT_LIMITS_FLOOR) {
|
|
417
|
-
const c = body(bytes, CHUNK_DOCUMENT, "a Snapshot");
|
|
418
|
-
const tally = new Tally(limits);
|
|
419
|
-
const actors = lengthPrefixedList(c, limits.maxActors, "actors").length;
|
|
420
|
-
lengthPrefixedList(c, limits.maxDeps, "heads", 32);
|
|
421
|
-
const changeMetas = columnMetas(c);
|
|
422
|
-
const opMetas = columnMetas(c);
|
|
423
|
-
const changeLimit = rowLimits(changeMetas, limits);
|
|
424
|
-
const opLimit = rowLimits(opMetas, limits);
|
|
425
|
-
let inflated = 0;
|
|
426
|
-
const columns = [];
|
|
427
|
-
const opData = new Map();
|
|
428
|
-
for (const [m, limitOf] of [
|
|
429
|
-
...changeMetas.map((m) => [m, changeLimit]),
|
|
430
|
-
...opMetas.map((m) => [m, opLimit]),
|
|
431
|
-
]) {
|
|
432
|
-
let data = c.take(m.length);
|
|
433
|
-
if (m.spec & DEFLATE_BIT)
|
|
434
|
-
data = inflateCapped(data, limits.maxInflatedBytes - inflated);
|
|
435
|
-
inflated += data.length;
|
|
436
|
-
if (inflated > limits.maxInflatedBytes)
|
|
437
|
-
refuse(`Snapshot columns hold more than ${limits.maxInflatedBytes} bytes`);
|
|
438
|
-
const bounds = { actors, group: Number.POSITIVE_INFINITY, rows: limitOf(m.spec) };
|
|
439
|
-
columns.push({ spec: m.spec, rows: countColumn(m.spec & 7, data, tally, bounds) });
|
|
440
|
-
if (limitOf === opLimit)
|
|
441
|
-
opData.set(m.spec & ~DEFLATE_BIT, data);
|
|
442
|
-
}
|
|
443
|
-
// §11.2: depths from the operation columns, once their sizes are known to be bounded.
|
|
444
|
-
const maxDepth = documentDepth(opData);
|
|
445
|
-
tally.columnBytes = inflated;
|
|
446
|
-
// The rest is the document's head indices.
|
|
447
|
-
return Object.freeze({
|
|
448
|
-
maxRows: tally.maxRows,
|
|
449
|
-
groupSum: tally.groupSum,
|
|
450
|
-
stringBytes: tally.stringBytes,
|
|
451
|
-
columnBytes: tally.columnBytes,
|
|
452
|
-
columns: Object.freeze(columns),
|
|
453
|
-
otherActors: Object.freeze([]),
|
|
454
|
-
maxDepth,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
//# sourceMappingURL=chunk-limits.js.map
|