@threadedstack/wmd-web 0.0.0-stage → 0.1.13

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.
Files changed (92) hide show
  1. package/README.md +2601 -2
  2. package/RELEASE.json +123 -0
  3. package/export-page/coi-sw.js +20 -0
  4. package/export-page/index.html +17 -0
  5. package/export-page/run.css +10 -0
  6. package/export-page/run.js +236 -0
  7. package/guest/arm64/Image +4 -0
  8. package/guest/arm64/Image-4k +4 -0
  9. package/guest/arm64/initramfs.cpio.gz +0 -0
  10. package/guest/arm64/vm.wasm +0 -0
  11. package/package.json +36 -4
  12. package/page/sw.js +381 -0
  13. package/relay/control.ts +139 -0
  14. package/relay/engine.ts +787 -0
  15. package/relay/hosted.ts +660 -0
  16. package/relay/keys.ts +53 -0
  17. package/relay/local-tls.ts +121 -0
  18. package/relay/lock.ts +81 -0
  19. package/relay/logfile.ts +138 -0
  20. package/relay/main.ts +642 -0
  21. package/relay/policy.ts +241 -0
  22. package/relay/relay.ts +266 -0
  23. package/relay/token.ts +116 -0
  24. package/relay/v1.ts +107 -0
  25. package/relay/wisp.ts +615 -0
  26. package/src/base.js +325 -0
  27. package/src/boot.js +2037 -0
  28. package/src/calib-worker.js +40 -0
  29. package/src/fs/9p/codec.js +303 -0
  30. package/src/fs/9p/server.js +521 -0
  31. package/src/fs/errno.js +43 -0
  32. package/src/fs/graph.js +380 -0
  33. package/src/fs/overlay.js +1018 -0
  34. package/src/fs/pack-write.js +165 -0
  35. package/src/fs/pack.js +427 -0
  36. package/src/fs/seed.js +22 -0
  37. package/src/fs/svc-snapshot.js +65 -0
  38. package/src/fs/upper-pack.js +112 -0
  39. package/src/host/base-cache.js +195 -0
  40. package/src/host/base-file.js +328 -0
  41. package/src/host/base-io.js +93 -0
  42. package/src/host/boot-events.js +209 -0
  43. package/src/host/chan-service.js +300 -0
  44. package/src/host/chan-services.js +689 -0
  45. package/src/host/chan.js +655 -0
  46. package/src/host/ckpt-file.js +395 -0
  47. package/src/host/ckpt-io.js +207 -0
  48. package/src/host/ckpt-point.js +160 -0
  49. package/src/host/ckpt-save.js +64 -0
  50. package/src/host/ckpt.js +434 -0
  51. package/src/host/clock.js +111 -0
  52. package/src/host/ctl.js +778 -0
  53. package/src/host/errno.js +18 -0
  54. package/src/host/fs.js +167 -0
  55. package/src/host/gcnudge.js +83 -0
  56. package/src/host/i64shim.js +81 -0
  57. package/src/host/jit-cache.js +341 -0
  58. package/src/host/jit-hub-core.js +208 -0
  59. package/src/host/jit-hub.js +93 -0
  60. package/src/host/jit-snap.js +226 -0
  61. package/src/host/jit.js +1903 -0
  62. package/src/host/layout.js +100 -0
  63. package/src/host/monitor.js +110 -0
  64. package/src/host/p9mount.js +27 -0
  65. package/src/host/ram-mem.js +125 -0
  66. package/src/host/stdio.js +143 -0
  67. package/src/host/threads.js +109 -0
  68. package/src/host/wasi.js +335 -0
  69. package/src/host/wmd-imports.js +71 -0
  70. package/src/host/yamux.js +387 -0
  71. package/src/image-files.js +154 -0
  72. package/src/net/dhcp-dns.js +126 -0
  73. package/src/net/net-worker.js +172 -0
  74. package/src/net/packet.js +227 -0
  75. package/src/net/peer.js +230 -0
  76. package/src/net/proto.js +189 -0
  77. package/src/net/relay-client.js +226 -0
  78. package/src/net/stack.js +867 -0
  79. package/src/net/switch.js +160 -0
  80. package/src/net/wisp-client.js +570 -0
  81. package/src/net/wisp-transport.js +196 -0
  82. package/src/net/wisp.js +511 -0
  83. package/src/registry/client.js +176 -0
  84. package/src/registry/converter.js +68 -0
  85. package/src/registry/hub.js +221 -0
  86. package/src/registry/oci.js +76 -0
  87. package/src/registry/pack-worker.js +70 -0
  88. package/src/registry/pull.js +258 -0
  89. package/src/registry/ref.js +110 -0
  90. package/src/registry/wasi-mem.js +355 -0
  91. package/src/worker.js +315 -0
  92. package/wmdpack.wasm +0 -0
@@ -0,0 +1,1018 @@
1
+ // A container root file system: read-only pack layers (lowers) under a
2
+ // writable in-memory upper, with the layering rules of wmd's internal/union
3
+ // and internal/memfs (Docker's overlay storage):
4
+ //
5
+ // - Layers are given top first. In a directory, the topmost layer that has a
6
+ // name decides what it is. A whiteout entry hides the name in the layers
7
+ // below its own; an opaque directory hides everything the layers below hold
8
+ // in it; a non-directory hides any directory of that name below it. The
9
+ // directories of one path merge, top first, until one of them is opaque or
10
+ // a lower layer has a non-directory there.
11
+ // - Names starting ".wh." are the layers' bookkeeping, never entries.
12
+ // - A change to a lower entry "copies it up": its inode, metadata included,
13
+ // is already in memory, and a regular file's contents become an upper
14
+ // Content whose unwritten parts still read from the pack.
15
+ // - A removed lower name is recorded as a whiteout of its directory; a
16
+ // directory made again over it is opaque.
17
+ //
18
+ // The tree is inode based: an Inode object is one file, whatever its names
19
+ // (a lower hard-link group is one Inode, nlink its group size), so 9P fids
20
+ // survive renames of their names or directories, and a directory moved with
21
+ // lower contents keeps showing them (overlayfs' redirect_dir; memfs says
22
+ // EXDEV there instead).
23
+ //
24
+ // Inode numbers are memfs's: a lower entry's is 1<<62 | layer<<40 | index
25
+ // of the first entry of its hard-link group in that layer's pack; upper
26
+ // entries count from 1<<46; a copied-up entry keeps its number.
27
+ //
28
+ // Names and paths are byte strings (see pack.js). Errors are FsError with a
29
+ // Linux errno. No permissions are checked: the guest kernel checks its own
30
+ // processes.
31
+
32
+ import {
33
+ EEXIST, EFBIG, EINVAL, EISDIR, ELOOP, ENAMETOOLONG, ENODATA, ENOENT, ENOSPC,
34
+ ENOTDIR, ENOTEMPTY, ENXIO, EOPNOTSUPP, EPERM, fail,
35
+ } from './errno.js'
36
+ import { S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, bytesOf, strOf } from './pack.js'
37
+
38
+ export const NAME_MAX = 255
39
+ export const PATH_MAX = 4096
40
+ export const MAX_SYMLINKS = 40
41
+ export const WHITEOUT_PREFIX = '.wh.'
42
+ /** The first inode number of the upper layer (memfs.UpperInos). */
43
+ export const UPPER_INOS = 1n << 46n
44
+ /** Upper file contents are kept in chunks of this many bytes, only those written. */
45
+ export const CHUNK = 65536
46
+ /** Largest file size: offsets stay exact in a JavaScript number. */
47
+ export const MAX_FILE_SIZE = 2 ** 52
48
+
49
+ export const XATTR_CREATE = 1
50
+ export const XATTR_REPLACE = 2
51
+
52
+ export const QTDIR = 0x80
53
+ export const QTSYMLINK = 0x02
54
+ export const QTFILE = 0
55
+
56
+ const EMPTY = new Uint8Array(0)
57
+
58
+ /** Wall clock in Unix nanoseconds, as a BigInt. */
59
+ export function nowNs() {
60
+ const us = Math.round((performance.timeOrigin + performance.now()) * 1000)
61
+ return BigInt(us) * 1000n
62
+ }
63
+
64
+ const LOWER_INOS = 1n << 62n
65
+
66
+ // A lower inode number, kept as a Number (layer << 40 | index, under 2^53)
67
+ // until asked for: Inode.ino makes the BigInt.
68
+ function lowerIno(layer, index) {
69
+ return layer * 2 ** 40 + index
70
+ }
71
+
72
+ /**
73
+ * A regular file's contents in the upper, over an optional pack base.
74
+ *
75
+ * The file is cut into CHUNK-sized chunks; only chunks written hold memory.
76
+ * A chunk is a Uint8Array that covers the first c.length bytes of its range,
77
+ * no more than were written there (or copied from the base below a write):
78
+ * its first allocation is exactly the bytes needed, later growth doubles up
79
+ * to CHUNK so appends stay linear. Bytes of a chunk's range past c.length
80
+ * read from the base below baseLen and as zeros above it. So a 6-byte file
81
+ * holds 6 bytes, and a big sparse file only the chunks written.
82
+ *
83
+ * Invariant: no byte at or past size is anything but zero in a chunk, so
84
+ * extending the size (truncate up) shows zeros. Writes and truncates keep it.
85
+ *
86
+ * Memory held is `allocated`, charged to the overlay (maxUpperBytes) as it
87
+ * grows and given back as it shrinks or by free() when the file is gone.
88
+ */
89
+ export class Content {
90
+ constructor(fs, base, size) {
91
+ this.fs = fs
92
+ this.base = base // {pack, off, size}: the pack entry's contents, or null
93
+ this.baseLen = base ? size : 0
94
+ this.size = size
95
+ // Chunk 0 (all of a small file) in c0; the others in a Map made when needed.
96
+ this.c0 = null // Uint8Array (length <= CHUNK)
97
+ this.chunks = null // Map chunk index (>= 1) -> Uint8Array (length <= CHUNK)
98
+ this.allocated = 0 // bytes the chunks hold, charged to fs
99
+ }
100
+
101
+ read(off, len) {
102
+ if (off >= this.size || len <= 0) return EMPTY
103
+ const n = Math.min(len, this.size - off)
104
+ const out = new Uint8Array(n)
105
+ const end = off + n
106
+ let pos = off
107
+ while (pos < end) {
108
+ const ci = Math.floor(pos / CHUNK)
109
+ const cs = ci * CHUNK
110
+ const stop = Math.min(end, cs + CHUNK)
111
+ const c = this.#get(ci)
112
+ let p = pos
113
+ if (c && p < cs + c.length) {
114
+ const s = Math.min(stop, cs + c.length)
115
+ out.set(c.subarray(p - cs, s - cs), p - off)
116
+ p = s
117
+ }
118
+ if (p < stop && p < this.baseLen) {
119
+ const bs = Math.min(stop, this.baseLen)
120
+ out.set(this.base.pack.read(this.base, p, bs - p), p - off)
121
+ }
122
+ // else: a hole, zeros.
123
+ pos = stop
124
+ }
125
+ return out
126
+ }
127
+
128
+ #get(ci) {
129
+ if (ci === 0) return this.c0
130
+ return this.chunks ? this.chunks.get(ci) : undefined
131
+ }
132
+
133
+ // Sets chunk ci to c, or removes it (c null).
134
+ #put(ci, c) {
135
+ if (ci === 0) {
136
+ this.c0 = c
137
+ } else if (c) {
138
+ if (!this.chunks) this.chunks = new Map()
139
+ this.chunks.set(ci, c)
140
+ } else if (this.chunks) {
141
+ this.chunks.delete(ci)
142
+ if (this.chunks.size === 0) this.chunks = null
143
+ }
144
+ }
145
+
146
+ // The length chunk ci (now have bytes) grows to so it covers need bytes.
147
+ static #grown(have, need) {
148
+ if (have === 0) return need
149
+ return Math.max(need, Math.min(CHUNK, have * 2))
150
+ }
151
+
152
+ // Makes chunk ci cover len bytes (charging the growth), filling new bytes
153
+ // from the base where there is one.
154
+ #grow(ci, c, len) {
155
+ const have = c ? c.length : 0
156
+ this.fs.charge(len - have)
157
+ const g = new Uint8Array(len)
158
+ if (c) g.set(c)
159
+ const cs = ci * CHUNK
160
+ const from = cs + have
161
+ const to = Math.min(cs + len, this.baseLen)
162
+ if (from < to) g.set(this.base.pack.read(this.base, from, to - from), have)
163
+ this.#put(ci, g)
164
+ this.allocated += len - have
165
+ return g
166
+ }
167
+
168
+ // The bytes the chunks must grow by for a write of [off, end), with the
169
+ // doubling slack (slack) or exactly.
170
+ #cost(off, end, slack) {
171
+ let cost = 0
172
+ for (let ci = Math.floor(off / CHUNK); ci * CHUNK < end; ci++) {
173
+ const cs = ci * CHUNK
174
+ const c = this.#get(ci)
175
+ const have = c ? c.length : 0
176
+ const need = Math.min(end, cs + CHUNK) - cs
177
+ if (need > have) cost += (slack ? Content.#grown(have, need) : need) - have
178
+ }
179
+ return cost
180
+ }
181
+
182
+ /**
183
+ * Writes data at off, all of it or (ENOSPC, nothing changed) none: the
184
+ * room is checked before any byte is stored. A guest write(2) larger than
185
+ * one Twrite still comes out short, as POSIX has it: v9fs returns the
186
+ * bytes of the Twrites that succeeded before the one that got ENOSPC.
187
+ */
188
+ write(off, data) {
189
+ const end = off + data.length
190
+ if (end > MAX_FILE_SIZE) fail(EFBIG)
191
+ if (data.length === 0) return 0
192
+ const room = this.fs.room()
193
+ if (this.#cost(off, end, false) > room) this.fs.full()
194
+ // Doubling slack only when it fits too.
195
+ const slack = this.#cost(off, end, true) <= room
196
+ let pos = off
197
+ while (pos < end) {
198
+ const ci = Math.floor(pos / CHUNK)
199
+ const cs = ci * CHUNK
200
+ const stop = Math.min(end, cs + CHUNK)
201
+ let c = this.#get(ci)
202
+ const have = c ? c.length : 0
203
+ if (stop - cs > have) c = this.#grow(ci, c, slack ? Content.#grown(have, stop - cs) : stop - cs)
204
+ c.set(data.subarray(pos - off, stop - off), pos - cs)
205
+ // Size follows each chunk: should a later allocation throw, no byte
206
+ // stored is past EOF (a truncate up must show zeros).
207
+ if (stop > this.size) this.size = stop
208
+ pos = stop
209
+ }
210
+ return data.length
211
+ }
212
+
213
+ truncate(size) {
214
+ if (size > MAX_FILE_SIZE) fail(EFBIG)
215
+ if (size < this.size) {
216
+ const all = this.chunks ? [...this.chunks] : []
217
+ if (this.c0) all.push([0, this.c0])
218
+ for (const [ci, c] of all) {
219
+ const cs = ci * CHUNK
220
+ if (cs >= size) {
221
+ this.#put(ci, null)
222
+ this.#uncharge(c.length)
223
+ } else if (cs + c.length > size) {
224
+ // The chunk with the new end: drop what lies past it.
225
+ const keep = size - cs
226
+ this.#put(ci, c.slice(0, keep))
227
+ this.#uncharge(c.length - keep)
228
+ }
229
+ }
230
+ }
231
+ if (this.baseLen > size) this.baseLen = size
232
+ this.size = size
233
+ }
234
+
235
+ #uncharge(n) {
236
+ this.allocated -= n
237
+ this.fs.charge(-n)
238
+ }
239
+
240
+ /** Gives all the memory back: the file is gone (no names, no fids). */
241
+ free() {
242
+ if (this.allocated > 0) this.#uncharge(this.allocated)
243
+ this.c0 = null
244
+ this.chunks = null
245
+ this.size = 0
246
+ this.baseLen = 0
247
+ this.base = null
248
+ }
249
+ }
250
+
251
+ /**
252
+ * One file of the tree. Directories are DirInode. Lower entries' inodes are
253
+ * made when their directory is first read, so they are kept small: a few
254
+ * dozen bytes of fields, the pack's times shared, and the inode number
255
+ * kept as a Number until asked for.
256
+ */
257
+ export class Inode {
258
+ constructor(ino, mode) {
259
+ this.lino = ino // the qid path: a BigInt, or a lower one as lowerIno's Number
260
+ this.mode = mode
261
+ this.uid = 0
262
+ this.gid = 0
263
+ this.nlink = 1
264
+ this.rdev = 0
265
+ this.atime = 0n
266
+ this.mtime = 0n
267
+ this.ctime = 0n
268
+ this.xattrs = null // Map name -> Uint8Array
269
+ this.upper = false // changed or made in the upper
270
+ this.refs = 0 // holders other than names (9P fids)
271
+ // Symlink.
272
+ this.target = ''
273
+ // Regular file: the pack entry's contents it shows until written (see
274
+ // low), data the upper contents.
275
+ this.lowPack = null
276
+ this.lowOff = 0
277
+ this.lowSize = 0
278
+ this.data = null
279
+ }
280
+
281
+ /** A lower regular file's pack contents, {pack, off, size}, or null. */
282
+ get low() {
283
+ return this.lowPack ? { pack: this.lowPack, off: this.lowOff, size: this.lowSize } : null
284
+ }
285
+
286
+ /** The inode number (qid path), a BigInt. */
287
+ get ino() {
288
+ const v = this.lino
289
+ return typeof v === 'number' ? LOWER_INOS | BigInt(v) : v
290
+ }
291
+
292
+ set ino(v) {
293
+ this.lino = v
294
+ }
295
+
296
+ get type() {
297
+ return this.mode & S_IFMT
298
+ }
299
+
300
+ isDir() {
301
+ return (this.mode & S_IFMT) === S_IFDIR
302
+ }
303
+
304
+ get qtype() {
305
+ const t = this.mode & S_IFMT
306
+ return t === S_IFDIR ? QTDIR : t === S_IFLNK ? QTSYMLINK : QTFILE
307
+ }
308
+ }
309
+
310
+ /** A directory. */
311
+ export class DirInode extends Inode {
312
+ constructor(ino, mode) {
313
+ super(ino, mode)
314
+ // kids is null until the lower layers are read.
315
+ this.kids = null
316
+ this.lowerDirs = null // [{layer, path}] the lower directories merged here, top first
317
+ this.whiteouts = null // lower names removed here
318
+ this.opaque = false
319
+ this.parent = null
320
+ this.removed = false
321
+ }
322
+ }
323
+
324
+ function checkName(name) {
325
+ if (name === '' || name === '.' || name === '..' || name.includes('/') || name.includes('\0')) fail(EINVAL)
326
+ if (name.length > NAME_MAX) fail(ENAMETOOLONG)
327
+ if (name.startsWith(WHITEOUT_PREFIX)) fail(EPERM)
328
+ }
329
+
330
+ function xattrNamespace(name) {
331
+ if (name.startsWith('security.') || name.startsWith('trusted.') || name.startsWith('user.')) return
332
+ fail(EOPNOTSUPP)
333
+ }
334
+
335
+ export class Overlay {
336
+ /**
337
+ * @param {object} o
338
+ * @param {import('./pack.js').Pack[]} [o.layers] lower packs, top first
339
+ * @param {string} [o.at] where the layers show: '' for the root, or a path
340
+ * such as 'rootfs' under an upper-only root (the guest init's layout).
341
+ * @param {number} [o.maxUpperBytes] bytes of file contents the upper may hold
342
+ * @param {(fs: Overlay) => void} [o.onFull] called whenever a write is refused for it
343
+ * @param {() => bigint} [o.now] clock, Unix ns
344
+ */
345
+ constructor({ layers = [], at = '', maxUpperBytes = Infinity, now = nowNs, onFull = null } = {}) {
346
+ this.layers = layers
347
+ this.now = now
348
+ this.maxUpperBytes = maxUpperBytes
349
+ this.used = 0
350
+ this.fullHits = 0 // ENOSPC answers because of maxUpperBytes
351
+ this.onFull = onFull // called on each of them (the host tells the user)
352
+ this.nextIno = UPPER_INOS
353
+ this.linkInodes = new Map() // `${layer}:${first}` -> Inode
354
+ const lroot = this.#lowerRoot()
355
+ const parts = at.split('/').filter((s) => s !== '' && s !== '.')
356
+ if (parts.length === 0) {
357
+ this.root = lroot
358
+ } else {
359
+ // An upper-only root, qid path 1 as wmd's Go server's synthetic root.
360
+ this.root = this.#newInode(S_IFDIR | 0o755, 0, 0)
361
+ this.root.ino = 1n
362
+ this.nextIno = UPPER_INOS
363
+ let dir = this.root
364
+ for (let i = 0; i < parts.length - 1; i++) dir = this.mkdir(dir, parts[i], 0o755, 0, 0)
365
+ checkName(parts.at(-1))
366
+ this.#insert(dir, parts.at(-1), lroot, false)
367
+ }
368
+ this.root.parent = null
369
+ }
370
+
371
+ /** Accounts n more (or fewer) bytes of upper contents. */
372
+ charge(n) {
373
+ if (n > 0 && this.used + n > this.maxUpperBytes) this.full()
374
+ this.used += n
375
+ }
376
+
377
+ /** Refuses with ENOSPC: the upper is at maxUpperBytes. */
378
+ full() {
379
+ this.fullHits++
380
+ this.onFull?.(this)
381
+ fail(ENOSPC)
382
+ }
383
+
384
+ /** The bytes of contents the upper can still take (Infinity without a cap). */
385
+ room() {
386
+ return Math.max(0, this.maxUpperBytes - this.used)
387
+ }
388
+
389
+ /**
390
+ * Counts a holder of n other than its names (a 9P fid). A file with no
391
+ * names left keeps its contents until the last holder lets go (unref).
392
+ */
393
+ ref(n) {
394
+ n.refs++
395
+ }
396
+
397
+ unref(n) {
398
+ if (n.refs > 0) n.refs--
399
+ this.#release(n)
400
+ }
401
+
402
+ // Gives back a file's contents once it has neither names nor holders.
403
+ #release(n) {
404
+ if (n.nlink === 0 && n.refs === 0 && n.data) n.data.free()
405
+ }
406
+
407
+ #newIno() {
408
+ this.nextIno += 1n
409
+ return this.nextIno
410
+ }
411
+
412
+ #newInode(mode, uid, gid) {
413
+ const n = (mode & S_IFMT) === S_IFDIR ? new DirInode(this.#newIno(), mode) : new Inode(this.#newIno(), mode)
414
+ n.uid = uid >>> 0
415
+ n.gid = gid >>> 0
416
+ const t = this.now()
417
+ n.atime = n.mtime = n.ctime = t
418
+ n.upper = true
419
+ switch (mode & S_IFMT) {
420
+ case S_IFDIR:
421
+ n.nlink = 2
422
+ n.kids = new Map()
423
+ n.lowerDirs = []
424
+ n.whiteouts = new Set()
425
+ break
426
+ case S_IFREG:
427
+ n.data = new Content(this, null, 0)
428
+ break
429
+ }
430
+ return n
431
+ }
432
+
433
+ #lowerRoot() {
434
+ if (this.layers.length === 0) return this.#newInode(S_IFDIR | 0o755, 0, 0)
435
+ const dirs = []
436
+ for (let i = 0; i < this.layers.length; i++) {
437
+ dirs.push({ layer: i, path: '' })
438
+ const r = this.layers[i].lookup('')
439
+ if (r && r.opaque) break
440
+ }
441
+ const top = this.layers[0]
442
+ const idx = top.index('')
443
+ const n = new DirInode(lowerIno(0, idx < 0 ? 0 : idx), S_IFDIR | 0o755)
444
+ if (idx >= 0) this.#copyAttrs(n, top.entry(idx))
445
+ n.nlink = 2 // memfs's root
446
+ n.lowerDirs = dirs
447
+ return n
448
+ }
449
+
450
+ #copyAttrs(n, e) {
451
+ n.mode = e.mode
452
+ n.uid = e.uid
453
+ n.gid = e.gid
454
+ n.rdev = e.rdev
455
+ n.atime = e.atime
456
+ n.mtime = e.mtime
457
+ n.ctime = e.ctime
458
+ if (e.xattrs) n.xattrs = new Map(e.xattrs)
459
+ }
460
+
461
+ // The inode of lower entry e (index i of layer `layer`'s pack).
462
+ #fromEntry(layer, i, e, parent, dirs) {
463
+ if (dirs) {
464
+ const n = new DirInode(lowerIno(layer, i), e.mode)
465
+ this.#copyAttrs(n, e)
466
+ n.lowerDirs = dirs
467
+ n.parent = parent
468
+ return n
469
+ }
470
+ let first = i
471
+ let nlink = 1
472
+ let key = null
473
+ if (e.link) {
474
+ const g = this.layers[layer].linkGroup(e.link)
475
+ first = g.first
476
+ nlink = g.n
477
+ key = `${layer}:${first}`
478
+ const had = this.linkInodes.get(key)
479
+ if (had) return had
480
+ }
481
+ const n = new Inode(lowerIno(layer, first), e.mode)
482
+ this.#copyAttrs(n, e)
483
+ n.nlink = nlink
484
+ switch (e.mode & S_IFMT) {
485
+ case S_IFREG:
486
+ n.lowPack = this.layers[layer]
487
+ n.lowOff = e.off
488
+ n.lowSize = e.size
489
+ break
490
+ case S_IFLNK:
491
+ n.target = e.target
492
+ break
493
+ }
494
+ if (key) this.linkInodes.set(key, n)
495
+ return n
496
+ }
497
+
498
+ // Reads directory dir's lower layers once: its merged entries.
499
+ #load(dir) {
500
+ if (dir.kids) return dir.kids
501
+ if (dir.lowerDirs.length === 1) return this.#loadOne(dir)
502
+ const pend = new Map()
503
+ const hidden = new Set()
504
+ for (const { layer, path } of dir.lowerDirs) {
505
+ const p = this.layers[layer]
506
+ const wh = []
507
+ for (const i of p.children(path)) {
508
+ const e = p.entry(i)
509
+ const name = e.name
510
+ if (name.startsWith(WHITEOUT_PREFIX)) continue
511
+ if (e.whiteout) {
512
+ wh.push(name)
513
+ continue
514
+ }
515
+ if (hidden.has(name)) continue
516
+ const isDir = (e.mode & S_IFMT) === S_IFDIR
517
+ const c = pend.get(name)
518
+ if (!c) {
519
+ pend.set(name, { layer, i, e, dirs: isDir ? [{ layer, path: e.path }] : null, stop: !isDir || e.opaque })
520
+ } else if (!c.stop) {
521
+ if (isDir && c.dirs) {
522
+ c.dirs.push({ layer, path: e.path })
523
+ if (e.opaque) c.stop = true
524
+ } else {
525
+ c.stop = true
526
+ }
527
+ }
528
+ }
529
+ // A layer's whiteouts hide only what is below it.
530
+ for (const w of wh) hidden.add(w)
531
+ }
532
+ const kids = new Map()
533
+ for (const [name, c] of pend) kids.set(name, this.#fromEntry(c.layer, c.i, c.e, dir, c.dirs))
534
+ dir.kids = kids
535
+ if (!dir.whiteouts) dir.whiteouts = new Set()
536
+ return kids
537
+ }
538
+
539
+ // #load of a directory from one lower layer (most are): nothing to merge,
540
+ // so no per-name bookkeeping (a million-entry directory makes no garbage
541
+ // beyond the decoded entries).
542
+ #loadOne(dir) {
543
+ const { layer, path } = dir.lowerDirs[0]
544
+ const p = this.layers[layer]
545
+ const kids = new Map()
546
+ for (const i of p.children(path)) {
547
+ const e = p.entry(i)
548
+ if (e.whiteout || e.name.startsWith(WHITEOUT_PREFIX)) continue
549
+ const dirs = (e.mode & S_IFMT) === S_IFDIR ? [{ layer, path: e.path }] : null
550
+ kids.set(e.name, this.#fromEntry(layer, i, e, dir, dirs))
551
+ }
552
+ dir.kids = kids
553
+ if (!dir.whiteouts) dir.whiteouts = new Set()
554
+ return kids
555
+ }
556
+
557
+ // Whether the lower layers show name in dir (as #load merges them: the
558
+ // topmost layer that has the name decides).
559
+ #lowerHas(dir, name) {
560
+ for (const { layer, path } of dir.lowerDirs) {
561
+ const p = this.layers[layer]
562
+ const i = p.index(path === '' ? name : path + '/' + name)
563
+ if (i >= 0) return !p.entry(i).whiteout
564
+ }
565
+ return false
566
+ }
567
+
568
+ // ---- lookups -----------------------------------------------------------
569
+
570
+ /** The entry name in directory dir. */
571
+ lookup(dir, name) {
572
+ if (!dir.isDir()) fail(ENOTDIR)
573
+ if (name.length > NAME_MAX) fail(ENAMETOOLONG)
574
+ if (name.startsWith(WHITEOUT_PREFIX)) fail(ENOENT)
575
+ const n = this.#load(dir).get(name)
576
+ if (!n) fail(ENOENT)
577
+ return n
578
+ }
579
+
580
+ /** Directory dir's entries, [name, inode] sorted by name (byte order). */
581
+ list(dir) {
582
+ const { names, nodes } = this.listing(dir)
583
+ return names.map((name, i) => [name, nodes[i]])
584
+ }
585
+
586
+ /**
587
+ * Directory dir's entries sorted by name (byte order) as two arrays,
588
+ * names and their inodes: what a 9P readdir keeps, cheaper than pairs in
589
+ * a directory of a million.
590
+ */
591
+ listing(dir) {
592
+ if (!dir.isDir()) fail(ENOTDIR)
593
+ const kids = this.#load(dir)
594
+ const names = new Array(kids.size)
595
+ const nodes = new Array(kids.size)
596
+ let i = 0
597
+ let sorted = true
598
+ for (const [name, n] of kids) {
599
+ if (i > 0 && names[i - 1] >= name) sorted = false
600
+ names[i] = name
601
+ nodes[i++] = n
602
+ }
603
+ if (!sorted) {
604
+ names.sort() // code unit order: byte order for byte strings
605
+ for (let k = 0; k < names.length; k++) nodes[k] = kids.get(names[k])
606
+ }
607
+ return { names, nodes }
608
+ }
609
+
610
+ /** The number of entries of directory dir. */
611
+ count(dir) {
612
+ return this.#load(dir).size
613
+ }
614
+
615
+ /**
616
+ * The inode path leads to from the root, following symlinks inside the
617
+ * root as a process chrooted to it would (the final one only when follow
618
+ * is set): ELOOP after 40 links, ENOTDIR through a non-directory.
619
+ */
620
+ resolve(path, follow = true) {
621
+ let links = 0
622
+ let cur = this.root
623
+ let parts = path.split('/').filter((s) => s !== '')
624
+ let i = 0
625
+ while (i < parts.length) {
626
+ const name = parts[i]
627
+ const last = i === parts.length - 1
628
+ if (name === '.') {
629
+ if (!cur.isDir()) fail(ENOTDIR)
630
+ i++
631
+ continue
632
+ }
633
+ if (name === '..') {
634
+ if (!cur.isDir()) fail(ENOTDIR)
635
+ cur = cur.parent || this.root
636
+ i++
637
+ continue
638
+ }
639
+ const c = this.lookup(cur, name)
640
+ if (c.type === S_IFLNK && (!last || follow)) {
641
+ if (++links > MAX_SYMLINKS) fail(ELOOP)
642
+ if (c.target.startsWith('/')) cur = this.root
643
+ parts = c.target.split('/').filter((s) => s !== '').concat(parts.slice(i + 1))
644
+ i = 0
645
+ if (parts.length === 0) return cur
646
+ continue
647
+ }
648
+ if (last) return c
649
+ cur = c
650
+ i++
651
+ }
652
+ return cur
653
+ }
654
+
655
+ /** The directory path's parent resolves to, and its last name. */
656
+ resolveParent(path) {
657
+ const parts = path.split('/').filter((s) => s !== '')
658
+ if (parts.length === 0) fail(EINVAL)
659
+ const dir = this.resolve(parts.slice(0, -1).join('/'), true)
660
+ if (!dir.isDir()) fail(ENOTDIR)
661
+ return [dir, parts.at(-1)]
662
+ }
663
+
664
+ // ---- attributes --------------------------------------------------------
665
+
666
+ size(n) {
667
+ switch (n.type) {
668
+ case S_IFREG:
669
+ return n.data ? n.data.size : n.lowPack ? n.lowSize : 0
670
+ case S_IFLNK:
671
+ return n.target.length
672
+ case S_IFDIR:
673
+ return 4096
674
+ }
675
+ return 0
676
+ }
677
+
678
+ blocks(n) {
679
+ switch (n.type) {
680
+ case S_IFREG: {
681
+ const size = this.size(n)
682
+ if (!n.data) return Math.ceil(size / 512)
683
+ return Math.ceil(Math.min(size, n.data.allocated + n.data.baseLen) / 512)
684
+ }
685
+ case S_IFLNK:
686
+ return Math.ceil(n.target.length / 512)
687
+ case S_IFDIR:
688
+ return 8
689
+ }
690
+ return 0
691
+ }
692
+
693
+ /** stat(2)'s view of n. Times are BigInt ns. */
694
+ getattr(n) {
695
+ return {
696
+ ino: n.ino, qtype: n.qtype, mode: n.mode, uid: n.uid, gid: n.gid, nlink: n.nlink,
697
+ rdev: n.rdev, size: this.size(n), blksize: 4096, blocks: this.blocks(n),
698
+ atime: n.atime, mtime: n.mtime, ctime: n.ctime,
699
+ }
700
+ }
701
+
702
+ #ensureData(n) {
703
+ if (!n.data) n.data = new Content(this, n.low, n.lowSize)
704
+ n.upper = true
705
+ return n.data
706
+ }
707
+
708
+ /**
709
+ * Changes n: {mode (permission bits), uid, gid, size, atime, mtime (BigInt
710
+ * ns)}; undefined fields stay. ctime becomes now.
711
+ */
712
+ setattr(n, s) {
713
+ if (s.size !== undefined) {
714
+ if (n.isDir()) fail(EISDIR)
715
+ if (n.type !== S_IFREG) fail(EINVAL)
716
+ if (s.size < 0) fail(EINVAL)
717
+ if (s.size > MAX_FILE_SIZE) fail(EFBIG)
718
+ }
719
+ const t = this.now()
720
+ n.upper = true
721
+ if (s.mode !== undefined) n.mode = (n.mode & S_IFMT) | (s.mode & 0o7777)
722
+ if (s.uid !== undefined) n.uid = s.uid >>> 0
723
+ if (s.gid !== undefined) n.gid = s.gid >>> 0
724
+ if (s.size !== undefined) {
725
+ this.#ensureData(n).truncate(s.size)
726
+ n.mtime = t
727
+ }
728
+ if (s.atime !== undefined) n.atime = s.atime
729
+ if (s.mtime !== undefined) n.mtime = s.mtime
730
+ n.ctime = t
731
+ }
732
+
733
+ // ---- contents ----------------------------------------------------------
734
+
735
+ /** Opens n: {write, trunc}. Checks the type; truncates. */
736
+ open(n, { write = false, trunc = false } = {}) {
737
+ switch (n.type) {
738
+ case S_IFLNK:
739
+ fail(ELOOP)
740
+ case S_IFDIR:
741
+ if (write || trunc) fail(EISDIR)
742
+ return
743
+ case S_IFREG:
744
+ if (trunc && this.size(n) > 0) {
745
+ this.#ensureData(n).truncate(0)
746
+ n.mtime = n.ctime = this.now()
747
+ }
748
+ return
749
+ }
750
+ // Devices, fifos and sockets are the guest kernel's to run.
751
+ fail(ENXIO)
752
+ }
753
+
754
+ read(n, off, len) {
755
+ if (n.isDir()) fail(EISDIR)
756
+ if (n.type !== S_IFREG) fail(EINVAL)
757
+ if (n.data) return n.data.read(off, len)
758
+ if (n.lowPack) return n.lowPack.read({ off: n.lowOff, size: n.lowSize }, off, len)
759
+ return EMPTY
760
+ }
761
+
762
+ write(n, off, bytes) {
763
+ if (n.isDir()) fail(EISDIR)
764
+ if (n.type !== S_IFREG) fail(EINVAL)
765
+ const w = this.#ensureData(n).write(off, bytes)
766
+ if (w > 0) n.mtime = n.ctime = this.now()
767
+ return w
768
+ }
769
+
770
+ readlink(n) {
771
+ if (n.type !== S_IFLNK) fail(EINVAL)
772
+ return n.target
773
+ }
774
+
775
+ // ---- changes to directories --------------------------------------------
776
+
777
+ #checkNew(dir, name) {
778
+ if (!dir.isDir()) fail(ENOTDIR)
779
+ if (dir.removed) fail(ENOENT)
780
+ checkName(name)
781
+ if (this.#load(dir).has(name)) fail(EEXIST)
782
+ }
783
+
784
+ #touch(n) {
785
+ n.mtime = n.ctime = this.now()
786
+ n.upper = true
787
+ }
788
+
789
+ // Makes name in dir be n. made: n is new here (not a rename), so a
790
+ // directory made over a removed lower name must not merge with it.
791
+ #insert(dir, name, n, made) {
792
+ const kids = this.#load(dir)
793
+ if (made && n.isDir() && this.#lowerHas(dir, name)) n.opaque = true
794
+ dir.whiteouts.delete(name)
795
+ kids.set(name, n)
796
+ if (n.isDir()) {
797
+ n.parent = dir
798
+ if (dir.nlink >= 2) dir.nlink++
799
+ }
800
+ this.#touch(dir)
801
+ }
802
+
803
+ #detach(dir, name, n) {
804
+ dir.kids.delete(name)
805
+ if (this.#lowerHas(dir, name)) dir.whiteouts.add(name)
806
+ if (n.isDir() && dir.nlink > 2) dir.nlink--
807
+ this.#touch(dir)
808
+ }
809
+
810
+ create(dir, name, perm, uid, gid) {
811
+ this.#checkNew(dir, name)
812
+ const n = this.#newInode(S_IFREG | (perm & 0o7777), uid, gid)
813
+ this.#insert(dir, name, n, true)
814
+ return n
815
+ }
816
+
817
+ mkdir(dir, name, perm, uid, gid) {
818
+ this.#checkNew(dir, name)
819
+ const n = this.#newInode(S_IFDIR | (perm & 0o7777), uid, gid)
820
+ this.#insert(dir, name, n, true)
821
+ return n
822
+ }
823
+
824
+ symlink(dir, name, target, uid, gid) {
825
+ this.#checkNew(dir, name)
826
+ if (target.length >= PATH_MAX) fail(ENAMETOOLONG)
827
+ const n = this.#newInode(S_IFLNK | 0o777, uid, gid)
828
+ n.target = target
829
+ this.#insert(dir, name, n, true)
830
+ return n
831
+ }
832
+
833
+ mknod(dir, name, mode, rdev, uid, gid) {
834
+ switch (mode & S_IFMT) {
835
+ case S_IFIFO: case S_IFSOCK: case S_IFCHR: case S_IFBLK: case S_IFREG:
836
+ break
837
+ case 0:
838
+ mode |= S_IFREG
839
+ break
840
+ default:
841
+ fail(EINVAL)
842
+ }
843
+ this.#checkNew(dir, name)
844
+ const n = this.#newInode(mode & (S_IFMT | 0o7777), uid, gid)
845
+ n.rdev = rdev
846
+ this.#insert(dir, name, n, true)
847
+ return n
848
+ }
849
+
850
+ /** Makes name in dir another name of n. */
851
+ link(dir, name, n) {
852
+ if (n.isDir()) fail(EPERM)
853
+ if (n.nlink === 0) fail(ENOENT)
854
+ this.#checkNew(dir, name)
855
+ this.#insert(dir, name, n, true)
856
+ n.nlink++
857
+ n.ctime = this.now()
858
+ n.upper = true
859
+ }
860
+
861
+ /** Removes name from dir: a directory (empty) when rmdir is set, else anything else. */
862
+ unlink(dir, name, rmdir = false) {
863
+ if (dir.removed) fail(ENOENT)
864
+ const n = this.lookup(dir, name)
865
+ if (rmdir) {
866
+ if (!n.isDir()) fail(ENOTDIR)
867
+ if (this.#load(n).size > 0) fail(ENOTEMPTY)
868
+ } else if (n.isDir()) {
869
+ fail(EISDIR)
870
+ }
871
+ this.#detach(dir, name, n)
872
+ if (n.isDir()) {
873
+ n.removed = true
874
+ n.nlink = 0
875
+ } else {
876
+ n.nlink--
877
+ n.ctime = this.now()
878
+ this.#release(n)
879
+ }
880
+ }
881
+
882
+ /** Moves odir/oname to ndir/nname, replacing what is there as rename(2) does. */
883
+ rename(odir, oname, ndir, nname) {
884
+ if (odir.removed) fail(ENOENT)
885
+ const src = this.lookup(odir, oname)
886
+ if (!ndir.isDir()) fail(ENOTDIR)
887
+ if (ndir.removed) fail(ENOENT)
888
+ checkName(nname)
889
+ if (odir === ndir && oname === nname) return
890
+ if (src.isDir()) {
891
+ for (let p = ndir; p; p = p.parent) if (p === src) fail(EINVAL)
892
+ }
893
+ const t = this.#load(ndir).get(nname)
894
+ if (t) {
895
+ if (t === src) return // two names of one file: nothing, as Linux
896
+ if (src.isDir()) {
897
+ if (!t.isDir()) fail(ENOTDIR)
898
+ if (this.#load(t).size > 0) fail(ENOTEMPTY)
899
+ } else if (t.isDir()) {
900
+ fail(EISDIR)
901
+ }
902
+ }
903
+ this.#detach(odir, oname, src)
904
+ if (t) {
905
+ this.#detach(ndir, nname, t)
906
+ if (t.isDir()) {
907
+ t.removed = true
908
+ t.nlink = 0
909
+ } else {
910
+ t.nlink--
911
+ t.ctime = this.now()
912
+ this.#release(t)
913
+ }
914
+ }
915
+ this.#insert(ndir, nname, src, false)
916
+ src.ctime = this.now()
917
+ src.upper = true
918
+ }
919
+
920
+ // ---- extended attributes -----------------------------------------------
921
+
922
+ getxattr(n, name) {
923
+ xattrNamespace(name)
924
+ const v = n.xattrs && n.xattrs.get(name)
925
+ if (!v) fail(ENODATA)
926
+ return v
927
+ }
928
+
929
+ listxattr(n) {
930
+ return n.xattrs ? [...n.xattrs.keys()].sort() : []
931
+ }
932
+
933
+ setxattr(n, name, value, flags = 0) {
934
+ xattrNamespace(name)
935
+ const has = !!(n.xattrs && n.xattrs.has(name))
936
+ if (flags & XATTR_CREATE && has) fail(EEXIST)
937
+ if (flags & XATTR_REPLACE && !has) fail(ENODATA)
938
+ n.xattrs = new Map(n.xattrs || [])
939
+ n.xattrs.set(name, value.slice())
940
+ n.ctime = this.now()
941
+ n.upper = true
942
+ }
943
+
944
+ removexattr(n, name) {
945
+ xattrNamespace(name)
946
+ if (!n.xattrs || !n.xattrs.has(name)) fail(ENODATA)
947
+ n.xattrs = new Map(n.xattrs)
948
+ n.xattrs.delete(name)
949
+ n.ctime = this.now()
950
+ n.upper = true
951
+ }
952
+
953
+ /**
954
+ * statfs(2): with maxUpperBytes the size is the cap and free what is left
955
+ * of it. Without one, wmd's Go server's numbers (8 GiB, 4 GiB free), free
956
+ * shrinking once the upper holds more than the other 4 GiB.
957
+ */
958
+ statfs() {
959
+ const bs = 4096
960
+ const capped = Number.isFinite(this.maxUpperBytes)
961
+ const blocks = capped ? Math.floor(this.maxUpperBytes / bs) : 1 << 21
962
+ const left = Math.max(0, blocks - Math.ceil(this.used / bs))
963
+ const free = capped ? left : Math.min(1 << 20, left)
964
+ return {
965
+ fstype: 0x01021997, bsize: bs, blocks, bfree: free, bavail: free,
966
+ files: 1 << 20, ffree: 1 << 19, fsid: 0, namelen: NAME_MAX,
967
+ }
968
+ }
969
+
970
+ // ---- path helpers for the host (bundle files, tests) -------------------
971
+
972
+ /** mkdir -p (following symlinks); returns the directory. */
973
+ mkdirp(path, perm = 0o755) {
974
+ const parts = path.split('/').filter((s) => s !== '')
975
+ let dir = this.root
976
+ for (let i = 0; i < parts.length; i++) {
977
+ let c
978
+ try {
979
+ c = this.resolve(parts.slice(0, i + 1).join('/'), true)
980
+ } catch (e) {
981
+ if (e.errno !== ENOENT) throw e
982
+ c = this.mkdir(dir, parts[i], perm, 0, 0)
983
+ }
984
+ if (!c.isDir()) fail(ENOTDIR)
985
+ dir = c
986
+ }
987
+ return dir
988
+ }
989
+
990
+ /** Writes a whole file (made or replaced), data a Uint8Array or byte string. */
991
+ writeFile(path, data, { mode = 0o644, uid = 0, gid = 0 } = {}) {
992
+ const bytes = typeof data === 'string' ? bytesOf(data) : data
993
+ const [dir, name] = this.resolveParent(path)
994
+ let n
995
+ try {
996
+ n = this.resolve(path, true)
997
+ } catch (e) {
998
+ if (e.errno !== ENOENT) throw e
999
+ n = this.create(dir, name, mode, uid, gid)
1000
+ }
1001
+ this.open(n, { write: true, trunc: true })
1002
+ this.write(n, 0, bytes)
1003
+ return n
1004
+ }
1005
+
1006
+ /** A whole file's bytes. */
1007
+ readFile(path) {
1008
+ const n = this.resolve(path, true)
1009
+ return this.read(n, 0, this.size(n))
1010
+ }
1011
+
1012
+ /** stat(2) (lstat when follow is false) of path. */
1013
+ stat(path, follow = true) {
1014
+ return this.getattr(this.resolve(path, follow))
1015
+ }
1016
+ }
1017
+
1018
+ export { strOf, bytesOf }