@daugasauron/dolly 0.1.4 → 0.1.5
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 +134 -14
- package/coi-serviceworker.js +28 -0
- package/dist/dolly-build-id.mjs +1 -1
- package/dist/dolly-dso-0.wasm +0 -0
- package/dist/dolly-image-build-id.mjs +1 -1
- package/dist/dolly-process-0.wasm +0 -0
- package/dist/dolly-process-abi.mjs +1 -1
- package/dist/dolly-process-dso.mjs +382 -53
- package/dist/dolly-process-gate-0.wasm +0 -0
- package/dist/dolly-process-worker.mjs +164 -35
- package/dist/dolly.wasm +0 -0
- package/host/dso/abi.mjs +3 -1
- package/host/dso/dso.mjs +1 -2
- package/host/http/abi.mjs +2 -2
- package/host/http/broker.mjs +18 -8
- package/host/http/http.mjs +0 -1
- package/host/http/policy.mjs +8 -4
- package/host/input/abi.mjs +47 -0
- package/host/modules.mjs +4 -3
- package/host/nena/abi.mjs +1 -1
- package/host/nena/nena.mjs +16 -18
- package/host/packages/service.mjs +35 -15
- package/host/packages/worker.mjs +21 -0
- package/host/runtime/abi.mjs +1 -0
- package/host/runtime/boot-files.mjs +8 -2
- package/host/runtime/grower.mjs +11 -8
- package/host/runtime/lease-bridge.mjs +8 -5
- package/host/runtime/module.json +1 -1
- package/host/runtime/runtime.mjs +37 -18
- package/host/snapshot/abi.mjs +7 -9
- package/host/snapshot/snapshot.mjs +14 -12
- package/host/snapshot/transport.mjs +4 -12
- package/host/snapshot/ui.mjs +17 -7
- package/host/stack/stack.mjs +1 -2
- package/host/threads/abi.mjs +1 -1
- package/host/threads/threads.mjs +2 -1
- package/package.json +1 -1
- package/src/browser.mjs +34 -37
- package/src/build-log.mjs +13 -2
- package/src/build-worker.mjs +182 -0
- package/src/custom-image.mjs +59 -0
- package/src/dollyfile-graph.mjs +127 -0
- package/src/dollyfile-view.mjs +335 -0
- package/src/image-artifact.mjs +248 -0
- package/src/image-build.mjs +89 -0
- package/src/image-builder.mjs +63 -0
- package/src/images.mjs +13 -2
- package/src/pack-store.mjs +141 -0
- package/src/packaged-snapshot.mjs +263 -91
- package/src/page-chords.mjs +4 -2
- package/src/process-constants.mjs +4 -2
- package/src/process-supervisor.mjs +209 -122
- package/src/runtime-worker.mjs +9 -10
- package/src/session-store.mjs +5 -7
- package/src/sha256.mjs +65 -0
- package/src/snapshot-records.mjs +135 -23
- package/src/static-asset.mjs +29 -2
- package/src/terminal-text.mjs +8 -5
- package/src/version.mjs +1 -1
- package/src/worker-image.mjs +3 -5
package/src/session-store.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { alphabetical } from "./alphabetical.mjs";
|
|
2
2
|
import { validName } from "./image-names.mjs";
|
|
3
3
|
import { imageInputs } from "./image-inputs.mjs";
|
|
4
|
+
import { MAX_SNAPSHOT_BYTES } from "./snapshot-records.mjs";
|
|
4
5
|
import { DOLLY_VERSION } from "./version.mjs";
|
|
5
6
|
|
|
6
|
-
export const DOLLY_SESSION_FORMAT_VERSION =
|
|
7
|
+
export const DOLLY_SESSION_FORMAT_VERSION = 3;
|
|
7
8
|
export const DOLLY_SESSION_MAX_BYTES = 512 * 1024 * 1024;
|
|
8
9
|
export const DOLLY_SESSION_METADATA_MAX_BYTES = 1024 * 1024;
|
|
9
10
|
|
|
@@ -31,7 +32,7 @@ export function customSessionIdentity(custom) {
|
|
|
31
32
|
typeof artifact.recipeSha256 !== "string" || typeof artifact.sha256 !== "string" ||
|
|
32
33
|
!/^[0-9a-f]{64}$/.test(artifact.recipeSha256) || !/^[0-9a-f]{64}$/.test(artifact.sha256) ||
|
|
33
34
|
!Number.isSafeInteger(artifact.byteLength) || artifact.byteLength <= 0 ||
|
|
34
|
-
artifact.byteLength >
|
|
35
|
+
artifact.byteLength > MAX_SNAPSHOT_BYTES ||
|
|
35
36
|
(policies !== undefined && (!Array.isArray(policies) || policies.length === 0 || policies.length > 16 ||
|
|
36
37
|
new TextEncoder().encode(JSON.stringify(policies)).byteLength > 65536))) {
|
|
37
38
|
throw new TypeError("invalid custom session base");
|
|
@@ -111,11 +112,9 @@ export async function encodeSessionStream(produce) {
|
|
|
111
112
|
export async function decodeSessionSnapshot(record) {
|
|
112
113
|
if (record === null || typeof record !== "object" ||
|
|
113
114
|
!(record.bytes instanceof ArrayBuffer) || record.bytes.byteLength === 0 ||
|
|
114
|
-
record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES ||
|
|
115
|
-
!["gzip", "identity"].includes(record.encoding)) {
|
|
115
|
+
record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES || record.encoding !== "gzip") {
|
|
116
116
|
throw new Error("Stored Dolly session is invalid");
|
|
117
117
|
}
|
|
118
|
-
if (record.encoding === "identity") return record.bytes.slice(0);
|
|
119
118
|
let offset = 0;
|
|
120
119
|
const stream = new ReadableStream({
|
|
121
120
|
pull(controller) {
|
|
@@ -178,8 +177,7 @@ export function validateSessionRecord(record) {
|
|
|
178
177
|
typeof record.imageIdentity !== "string" || record.imageIdentity.length > 256 ||
|
|
179
178
|
!Number.isSafeInteger(record.updatedAt) ||
|
|
180
179
|
!(record.bytes instanceof ArrayBuffer) || record.bytes.byteLength === 0 ||
|
|
181
|
-
record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES ||
|
|
182
|
-
!["gzip", "identity"].includes(record.encoding)) {
|
|
180
|
+
record.bytes.byteLength > DOLLY_SESSION_MAX_BYTES || record.encoding !== "gzip") {
|
|
183
181
|
throw new TypeError("invalid Dolly session record");
|
|
184
182
|
}
|
|
185
183
|
if (record.image === "custom" ? record.imageIdentity !== customSessionIdentity(record.customImage)
|
package/src/sha256.mjs
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// SHA-256 over bytes given in pieces, for a digest of more bytes than the page
|
|
2
|
+
// should hold at once; WebCrypto digests only a whole buffer.
|
|
3
|
+
const K = Int32Array.of(
|
|
4
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
5
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
6
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
7
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
8
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
9
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
10
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
11
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2);
|
|
12
|
+
|
|
13
|
+
export class Sha256 {
|
|
14
|
+
state = Int32Array.of(0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19);
|
|
15
|
+
block = new Uint8Array(64);
|
|
16
|
+
used = 0;
|
|
17
|
+
length = 0;
|
|
18
|
+
words = new Int32Array(64);
|
|
19
|
+
|
|
20
|
+
compress(view, offset) {
|
|
21
|
+
const w = this.words, s = this.state;
|
|
22
|
+
for (let i = 0; i < 16; i++) w[i] = view.getInt32(offset + 4 * i);
|
|
23
|
+
for (let i = 16; i < 64; i++) {
|
|
24
|
+
const x = w[i - 15], y = w[i - 2];
|
|
25
|
+
w[i] = (((x >>> 7 | x << 25) ^ (x >>> 18 | x << 14) ^ x >>> 3) + w[i - 7] +
|
|
26
|
+
((y >>> 17 | y << 15) ^ (y >>> 19 | y << 13) ^ y >>> 10) + w[i - 16]) | 0;
|
|
27
|
+
}
|
|
28
|
+
let a = s[0], b = s[1], c = s[2], d = s[3], e = s[4], f = s[5], g = s[6], h = s[7];
|
|
29
|
+
for (let i = 0; i < 64; i++) {
|
|
30
|
+
const t1 = (h + ((e >>> 6 | e << 26) ^ (e >>> 11 | e << 21) ^ (e >>> 25 | e << 7)) + (e & f ^ ~e & g) + K[i] + w[i]) | 0;
|
|
31
|
+
const t2 = (((a >>> 2 | a << 30) ^ (a >>> 13 | a << 19) ^ (a >>> 22 | a << 10)) + (a & b ^ a & c ^ b & c)) | 0;
|
|
32
|
+
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
|
33
|
+
}
|
|
34
|
+
s[0] += a; s[1] += b; s[2] += c; s[3] += d; s[4] += e; s[5] += f; s[6] += g; s[7] += h;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
update(bytes) {
|
|
38
|
+
this.length += bytes.length;
|
|
39
|
+
let offset = 0;
|
|
40
|
+
if (this.used !== 0) {
|
|
41
|
+
offset = Math.min(64 - this.used, bytes.length);
|
|
42
|
+
this.block.set(bytes.subarray(0, offset), this.used);
|
|
43
|
+
this.used += offset;
|
|
44
|
+
if (this.used < 64) return;
|
|
45
|
+
this.compress(new DataView(this.block.buffer), 0);
|
|
46
|
+
this.used = 0;
|
|
47
|
+
}
|
|
48
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
49
|
+
for (; offset + 64 <= bytes.length; offset += 64) this.compress(view, offset);
|
|
50
|
+
this.block.set(bytes.subarray(offset));
|
|
51
|
+
this.used = bytes.length - offset;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The lowercase hex digest; the hash takes no more bytes after it.
|
|
55
|
+
hex() {
|
|
56
|
+
const bits = this.length * 8;
|
|
57
|
+
this.update(Uint8Array.of(0x80));
|
|
58
|
+
this.update(new Uint8Array((120 - this.used) % 64));
|
|
59
|
+
const view = new DataView(this.block.buffer);
|
|
60
|
+
view.setUint32(56, Math.floor(bits / 2 ** 32));
|
|
61
|
+
view.setUint32(60, bits >>> 0);
|
|
62
|
+
this.compress(view, 0);
|
|
63
|
+
return [...this.state].map(word => (word >>> 0).toString(16).padStart(8, "0")).join("");
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/snapshot-records.mjs
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { unretainedPath } from "./image-names.mjs";
|
|
2
2
|
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
3
3
|
export const MAX_SNAPSHOT_BYTES = 2 * 1024 * 1024 * 1024;
|
|
4
|
+
// Packs average 2 MiB or more (scripts/share-pages-snapshots.mjs): an image of
|
|
5
|
+
// MAX_SNAPSHOT_BYTES needs about 1,024, which this bound leaves room for.
|
|
6
|
+
const MAX_SNAPSHOT_PACKS = MAX_SNAPSHOT_BYTES / (1024 * 1024);
|
|
4
7
|
|
|
5
8
|
function compareBytes(left, right) {
|
|
6
9
|
for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
|
|
@@ -9,47 +12,156 @@ function compareBytes(left, right) {
|
|
|
9
12
|
return left.length - right.length;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
// The 16-byte snapshot header: its record count.
|
|
16
|
+
function decodeHeader(bytes) {
|
|
14
17
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
15
|
-
if (bytes.length < 16 || bytes.
|
|
16
|
-
decoder.decode(bytes.subarray(0, 8)) !== "DOLLYSNP" || view.getUint32(8, true) !== 2) {
|
|
18
|
+
if (bytes.length < 16 || decoder.decode(bytes.subarray(0, 8)) !== "DOLLYSNP" || view.getUint32(8, true) !== 2) {
|
|
17
19
|
throw new Error("invalid Dolly snapshot header");
|
|
18
20
|
}
|
|
19
21
|
const count = view.getUint32(12, true);
|
|
20
22
|
if (count === 0 || count > 100_000) throw new Error("invalid Dolly snapshot record count");
|
|
23
|
+
return count;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A record's 16-byte header: its kind, path length and data size.
|
|
27
|
+
function decodeRecordHeader(bytes) {
|
|
28
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
29
|
+
const kind = view.getUint32(0, true), pathLength = view.getUint32(4, true), size = view.getBigUint64(8, true);
|
|
30
|
+
if (kind < 1 || kind > 3 || pathLength < 2 || pathLength >= 4096 ||
|
|
31
|
+
size > BigInt(MAX_SNAPSHOT_BYTES) || (kind === 1 && size !== 0n) ||
|
|
32
|
+
(kind === 3 && (size === 0n || size >= 4096n))) throw new Error("invalid snapshot record size");
|
|
33
|
+
return { kind, pathLength, size: Number(size) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The path of PATHBYTES, valid and after PREVIOUS in byte order.
|
|
37
|
+
function decodePath(pathBytes, previous) {
|
|
38
|
+
const path = decoder.decode(pathBytes);
|
|
39
|
+
if (!path.startsWith("/") || /[\0\\\r\n]/.test(path) ||
|
|
40
|
+
path.slice(1).split("/").some(part => !part || part === "." || part === "..") ||
|
|
41
|
+
unretainedPath(path) ||
|
|
42
|
+
previous && compareBytes(previous, pathBytes) >= 0) throw new Error(`invalid snapshot path ${path}`);
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function checkParents(records) {
|
|
47
|
+
for (const path of records.keys()) {
|
|
48
|
+
for (let slash = path.indexOf("/", 1); slash !== -1; slash = path.indexOf("/", slash + 1)) {
|
|
49
|
+
const parent = records.get(path.slice(0, slash));
|
|
50
|
+
if (parent && parent.kind !== 1) throw new Error(`snapshot parent is not a directory: ${path}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function decodeSnapshotRecords(input) {
|
|
56
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
57
|
+
if (bytes.length > MAX_SNAPSHOT_BYTES) throw new Error("invalid Dolly snapshot header");
|
|
58
|
+
const count = decodeHeader(bytes.subarray(0, 16));
|
|
21
59
|
const records = new Map();
|
|
22
60
|
let offset = 16, previous;
|
|
23
61
|
for (let index = 0; index < count; index += 1) {
|
|
24
62
|
if (offset > bytes.length - 16) throw new Error("truncated snapshot record");
|
|
25
|
-
const kind
|
|
26
|
-
const size = view.getBigUint64(offset + 8, true);
|
|
63
|
+
const { kind, pathLength, size } = decodeRecordHeader(bytes.subarray(offset, offset + 16));
|
|
27
64
|
offset += 16;
|
|
28
|
-
if (
|
|
29
|
-
size > BigInt(MAX_SNAPSHOT_BYTES) || (kind === 1 && size !== 0n) ||
|
|
30
|
-
(kind === 3 && (size === 0n || size >= 4096n))) throw new Error("invalid snapshot record size");
|
|
31
|
-
if (offset > bytes.length - pathLength - Number(size)) throw new Error("truncated snapshot record");
|
|
65
|
+
if (offset > bytes.length - pathLength - size) throw new Error("truncated snapshot record");
|
|
32
66
|
const pathBytes = bytes.subarray(offset, offset + pathLength);
|
|
33
|
-
const path =
|
|
34
|
-
if (!path.startsWith("/") || /[\0\\\r\n]/.test(path) ||
|
|
35
|
-
path.slice(1).split("/").some(part => !part || part === "." || part === "..") ||
|
|
36
|
-
unretainedPath(path) ||
|
|
37
|
-
previous && compareBytes(previous, pathBytes) >= 0) throw new Error(`invalid snapshot path ${path}`);
|
|
67
|
+
const path = decodePath(pathBytes, previous);
|
|
38
68
|
previous = pathBytes;
|
|
39
69
|
offset += pathLength;
|
|
40
|
-
const data = bytes.subarray(offset, offset +
|
|
70
|
+
const data = bytes.subarray(offset, offset + size);
|
|
41
71
|
if (kind === 3 && data.includes(0)) throw new Error("invalid snapshot symlink");
|
|
42
72
|
records.set(path, { kind, data });
|
|
43
|
-
offset +=
|
|
73
|
+
offset += size;
|
|
44
74
|
}
|
|
45
75
|
if (offset !== bytes.length) throw new Error("trailing snapshot bytes");
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
checkParents(records);
|
|
77
|
+
return records;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Bytes that arrive in pieces (an iterable of Uint8Array), read in order.
|
|
81
|
+
class PieceReader {
|
|
82
|
+
constructor(pieces) {
|
|
83
|
+
this.pieces = pieces[Symbol.asyncIterator]?.() ?? pieces[Symbol.iterator]();
|
|
84
|
+
this.chunk = new Uint8Array(0);
|
|
85
|
+
this.position = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async #fill() {
|
|
89
|
+
const { done, value } = await this.pieces.next();
|
|
90
|
+
if (done) return false;
|
|
91
|
+
if (!(value instanceof Uint8Array)) throw new TypeError("invalid snapshot piece");
|
|
92
|
+
this.chunk = value;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#advance(length) {
|
|
97
|
+
this.position += length;
|
|
98
|
+
if (this.position > MAX_SNAPSHOT_BYTES) throw new Error("snapshot exceeds its size limit");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The next LENGTH bytes, a view into the current piece when they lie in one.
|
|
102
|
+
async take(length) {
|
|
103
|
+
if (length === 0) return new Uint8Array(0);
|
|
104
|
+
if (this.chunk.length === 0 && !await this.#fill()) throw new Error("truncated snapshot record");
|
|
105
|
+
if (this.chunk.length >= length) {
|
|
106
|
+
const bytes = this.chunk.subarray(0, length);
|
|
107
|
+
this.chunk = this.chunk.subarray(length);
|
|
108
|
+
this.#advance(length);
|
|
109
|
+
return bytes;
|
|
50
110
|
}
|
|
111
|
+
const bytes = new Uint8Array(length);
|
|
112
|
+
for (let filled = 0; filled < length;) {
|
|
113
|
+
if (this.chunk.length === 0 && !await this.#fill()) throw new Error("truncated snapshot record");
|
|
114
|
+
const taken = Math.min(this.chunk.length, length - filled);
|
|
115
|
+
bytes.set(this.chunk.subarray(0, taken), filled);
|
|
116
|
+
this.chunk = this.chunk.subarray(taken);
|
|
117
|
+
filled += taken;
|
|
118
|
+
}
|
|
119
|
+
this.#advance(length);
|
|
120
|
+
return bytes;
|
|
51
121
|
}
|
|
52
|
-
|
|
122
|
+
|
|
123
|
+
async skip(length) {
|
|
124
|
+
while (length !== 0) {
|
|
125
|
+
if (this.chunk.length === 0 && !await this.#fill()) throw new Error("truncated snapshot record");
|
|
126
|
+
const taken = Math.min(this.chunk.length, length);
|
|
127
|
+
this.chunk = this.chunk.subarray(taken);
|
|
128
|
+
this.#advance(taken);
|
|
129
|
+
length -= taken;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async ended() {
|
|
134
|
+
while (this.chunk.length === 0) if (!await this.#fill()) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The records of a snapshot that arrives in pieces, checked as
|
|
140
|
+
// decodeSnapshotRecords checks them without holding the snapshot: every path
|
|
141
|
+
// with its kind, where its data lies (offset, size), and the data of the paths
|
|
142
|
+
// KEEP selects (symlink targets always). Resolves to the records and the
|
|
143
|
+
// snapshot's size.
|
|
144
|
+
export async function scanSnapshotRecords(pieces, keep) {
|
|
145
|
+
const reader = new PieceReader(pieces);
|
|
146
|
+
const count = decodeHeader(await reader.take(16));
|
|
147
|
+
const records = new Map();
|
|
148
|
+
let previous;
|
|
149
|
+
for (let index = 0; index < count; index += 1) {
|
|
150
|
+
const { kind, pathLength, size } = decodeRecordHeader(await reader.take(16));
|
|
151
|
+
const pathBytes = await reader.take(pathLength);
|
|
152
|
+
const path = decodePath(pathBytes, previous);
|
|
153
|
+
previous = pathBytes;
|
|
154
|
+
const offset = reader.position;
|
|
155
|
+
let data;
|
|
156
|
+
if (kind === 3 || (kind === 2 && keep(path))) {
|
|
157
|
+
data = await reader.take(size);
|
|
158
|
+
if (kind === 3 && data.includes(0)) throw new Error("invalid snapshot symlink");
|
|
159
|
+
} else await reader.skip(size);
|
|
160
|
+
records.set(path, { kind, data, offset, size });
|
|
161
|
+
}
|
|
162
|
+
if (!await reader.ended()) throw new Error("trailing snapshot bytes");
|
|
163
|
+
checkParents(records);
|
|
164
|
+
return { records, byteLength: reader.position };
|
|
53
165
|
}
|
|
54
166
|
|
|
55
167
|
export function encodeSnapshotRecords(records) {
|
|
@@ -91,7 +203,7 @@ export function mergeSnapshotRecords(parts) {
|
|
|
91
203
|
}
|
|
92
204
|
|
|
93
205
|
export function validateSnapshotPacks(metadata) {
|
|
94
|
-
if (!Array.isArray(metadata.packs) || metadata.packs.length === 0 || metadata.packs.length >
|
|
206
|
+
if (!Array.isArray(metadata.packs) || metadata.packs.length === 0 || metadata.packs.length > MAX_SNAPSHOT_PACKS) throw new Error("invalid snapshot packs");
|
|
95
207
|
const hashes = new Set();
|
|
96
208
|
let size = 16;
|
|
97
209
|
for (const pack of metadata.packs) {
|
package/src/static-asset.mjs
CHANGED
|
@@ -55,12 +55,39 @@ export async function boundedAssetBody(response, limit, signal) {
|
|
|
55
55
|
return bytes;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// A file a host stores in parts is a JSON manifest at its path that begins
|
|
59
|
+
// with these bytes, and no file stored whole does (the Pages exporter checks),
|
|
60
|
+
// so the page knows a manifest by its content: a host needs no header for it.
|
|
61
|
+
// Pages before v0.1.5 read the X-Dolly-Parts header instead.
|
|
62
|
+
export const PARTS_MANIFEST_START = '{"byteLength":';
|
|
63
|
+
|
|
64
|
+
// Whether a whole response's body begins as a manifest, read from a copy of it.
|
|
65
|
+
async function isPartsManifest(response) {
|
|
66
|
+
if (response.status !== 200 || !response.body) return false;
|
|
67
|
+
const start = new TextEncoder().encode(PARTS_MANIFEST_START), reader = response.clone().body.getReader();
|
|
68
|
+
const head = new Uint8Array(start.length);
|
|
69
|
+
let size = 0;
|
|
70
|
+
try {
|
|
71
|
+
while (size < start.length) {
|
|
72
|
+
const { done, value } = await reader.read();
|
|
73
|
+
if (done) break;
|
|
74
|
+
const taken = value.subarray(0, start.length - size);
|
|
75
|
+
head.set(taken, size);
|
|
76
|
+
size += taken.length;
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
// Not awaited: cancelling a copy settles only once the response is read too.
|
|
80
|
+
reader.cancel().catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
return size === start.length && head.every((byte, index) => byte === start[index]);
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
// Streams verified parts, holding at most one 20 MiB part per request. Each
|
|
59
86
|
// part matches the manifest hash before any of its bytes are delivered.
|
|
60
87
|
export async function decodeStaticAsset(response, target, init, maximumBytes, fetchRequest = globalThis.fetch.bind(globalThis)) {
|
|
61
|
-
if (response
|
|
88
|
+
if (!await isPartsManifest(response)) return response;
|
|
62
89
|
const url = new URL(target);
|
|
63
|
-
if (
|
|
90
|
+
if ((init.method ?? "GET") !== "GET" || url.search || url.hash ||
|
|
64
91
|
url.username || url.password || !/^https?:$/.test(url.protocol)) throw new Error("invalid multipart asset request");
|
|
65
92
|
const manifest = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(await boundedAssetBody(response, 65536, init.signal)));
|
|
66
93
|
if (!Number.isSafeInteger(manifest.byteLength) || manifest.byteLength <= 0 ||
|
package/src/terminal-text.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { DOLLY_POINTER_ACTION_DRAG as drag, DOLLY_POINTER_ACTION_PRESS as press,
|
|
2
|
+
DOLLY_POINTER_ACTION_RELEASE as release } from "../host/input/abi.mjs";
|
|
3
|
+
|
|
1
4
|
// How the page's handle (docs/embedding.md) reads the terminal, over the page APIs of
|
|
2
5
|
// display@0, input@0 and the runtime: the screen's text is the terminal's own
|
|
3
6
|
// selection of every cell, made with pointer records.
|
|
@@ -36,10 +39,10 @@ export function terminalText(page) {
|
|
|
36
39
|
const y = geometry.paddingY + Math.floor(geometry.cellHeight / 2);
|
|
37
40
|
const endX = x + (dimensions.cols - 1) * geometry.cellWidth, endY = y + (dimensions.rows - 1) * geometry.cellHeight;
|
|
38
41
|
let selected;
|
|
39
|
-
await inputTransport.pushPointer(x, y,
|
|
42
|
+
await inputTransport.pushPointer(x, y, press, shifted);
|
|
40
43
|
await waitFor(async () => await transport.copySelection() === null, "terminal selection reset");
|
|
41
|
-
await inputTransport.pushPointer(endX, endY,
|
|
42
|
-
await inputTransport.pushPointer(endX, endY,
|
|
44
|
+
await inputTransport.pushPointer(endX, endY, drag, shifted);
|
|
45
|
+
await inputTransport.pushPointer(endX, endY, release, shifted);
|
|
43
46
|
if (await waitFor(async () => (selected = await transport.copySelection()) !== null, null, attempts)) {
|
|
44
47
|
return selected.split("\n").slice(-dimensions.rows).join("\n");
|
|
45
48
|
}
|
|
@@ -62,8 +65,8 @@ export function terminalText(page) {
|
|
|
62
65
|
const geometry = transport.geometry();
|
|
63
66
|
const x = geometry.paddingX + Math.floor(geometry.cellWidth / 2);
|
|
64
67
|
const y = geometry.paddingY + Math.floor(geometry.cellHeight / 2);
|
|
65
|
-
await inputTransport.pushPointer(x, y,
|
|
66
|
-
await inputTransport.pushPointer(x, y,
|
|
68
|
+
await inputTransport.pushPointer(x, y, press, shifted);
|
|
69
|
+
await inputTransport.pushPointer(x, y, release, shifted);
|
|
67
70
|
await waitFor(() => inputTransport.inputIdle(), "terminal selection cleanup");
|
|
68
71
|
return pid;
|
|
69
72
|
}
|
package/src/version.mjs
CHANGED
package/src/worker-image.mjs
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// image into that kernel, and runs its ENTRY. runtime-worker.mjs restores an
|
|
4
4
|
// image of the image directory; the site's build-worker.mjs builds one or
|
|
5
5
|
// restores a custom one.
|
|
6
|
-
import { DOLLY_BUILD_ID } from "../dist/dolly-build-id.mjs";
|
|
7
6
|
import { loadImages } from "./images.mjs";
|
|
8
7
|
import { decodeImageEntry } from "./image-entry.mjs";
|
|
9
8
|
import { MAX_SNAPSHOT_BYTES } from "./snapshot-records.mjs";
|
|
@@ -28,7 +27,7 @@ const configured = new Promise((resolve, reject) => {
|
|
|
28
27
|
const { hostRegistry } = await import("../host/modules.mjs");
|
|
29
28
|
|
|
30
29
|
// The runtime's own files: the kernel and the contracts beside this module.
|
|
31
|
-
|
|
30
|
+
const runtimeBase = new URL("../", import.meta.url);
|
|
32
31
|
export const kernelArtifact = path => {
|
|
33
32
|
if (path !== "dolly.wasm" && path !== "dolly.data") throw new Error("unknown fixed kernel artifact");
|
|
34
33
|
return new URL(`dist/${path}`, runtimeBase).href;
|
|
@@ -45,7 +44,7 @@ export function checkedMemoryRange(memory, addressValue, sizeValue) {
|
|
|
45
44
|
return { address, size };
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
const stage = text => self.postMessage({ type: "bootstrap", text: `${text}\n
|
|
47
|
+
const stage = text => self.postMessage({ type: "bootstrap", text: `${text}\n` });
|
|
49
48
|
|
|
50
49
|
function acknowledged(type, failure) {
|
|
51
50
|
return new Promise((resolve, reject) => {
|
|
@@ -111,8 +110,7 @@ export async function runImage(image) {
|
|
|
111
110
|
}
|
|
112
111
|
stage("starting image entry...");
|
|
113
112
|
const entryReady = acknowledged("entry-ready-ack", "browser did not acknowledge image startup");
|
|
114
|
-
self.postMessage({ type: "ready", image: runtimeImage
|
|
115
|
-
hostModules: host.enabled });
|
|
113
|
+
self.postMessage({ type: "ready", image: runtimeImage });
|
|
116
114
|
await entryReady;
|
|
117
115
|
|
|
118
116
|
// The image ends with its ENTRY process, however that ended: a program that
|