@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,521 @@
1
+ // A 9P2000.L file server over an Overlay (../overlay.js), transport-agnostic:
2
+ // handle() takes one request frame and returns its reply frame, synchronously,
3
+ // so it can sit behind any channel (a shared-memory ring, a MessagePort, a
4
+ // test). The behaviour follows wmd's Go server (internal/engine/ninep over
5
+ // internal/memfs, on github.com/hugelgupf/p9); the differences are listed in
6
+ // web/docs/9p.md.
7
+
8
+ import {
9
+ E2BIG, EBADF, EBUSY, EFBIG, EINVAL, EIO, EISDIR, ELOOP, ENOENT, ENOSYS, ENOTDIR,
10
+ FsError,
11
+ } from '../errno.js'
12
+ import {
13
+ S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK, makedev,
14
+ } from '../pack.js'
15
+ import { NAME_MAX } from '../overlay.js'
16
+ import { HEADER, NOFID, NOTAG, ProtoError, T, decode, direntSize, encode, encodeDirents, header } from './codec.js'
17
+
18
+ export const VERSION = '9P2000.L'
19
+ /** The largest msize the server agrees to (wmd's channel pump stops at 1 MiB replies). */
20
+ export const MAX_MSIZE = 1 << 20
21
+ export const MAXWELEM = 16
22
+
23
+ // Tlopen/Tlcreate flags (Linux's open flags, P9_DOTL_*).
24
+ export const O_ACCMODE = 3
25
+ export const O_RDONLY = 0
26
+ export const O_WRONLY = 1
27
+ export const O_RDWR = 2
28
+ export const O_TRUNC = 0o1000
29
+ export const O_APPEND = 0o2000 // P9_DOTL_APPEND
30
+ export const O_DIRECTORY = 0o200000
31
+
32
+ // Tsetattr valid bits.
33
+ export const SETATTR_MODE = 0x1
34
+ export const SETATTR_UID = 0x2
35
+ export const SETATTR_GID = 0x4
36
+ export const SETATTR_SIZE = 0x8
37
+ export const SETATTR_ATIME = 0x10
38
+ export const SETATTR_MTIME = 0x20
39
+ export const SETATTR_CTIME = 0x40
40
+ export const SETATTR_ATIME_SET = 0x80
41
+ export const SETATTR_MTIME_SET = 0x100
42
+
43
+ /** Rgetattr's valid mask: P9_GETATTR_BASIC (mode..blocks). */
44
+ export const GETATTR_BASIC = 0x7ff
45
+ export const AT_REMOVEDIR = 0x200
46
+ export const XATTR_SIZE_MAX = 65536
47
+ const XATTR_REPLACE = 2
48
+ const LOCK_SUCCESS = 0
49
+ const LOCK_TYPE_UNLCK = 2
50
+
51
+ const NS = 1000000000n
52
+
53
+ /** The d_type of a mode (DT_*), as getdents(2) reports it. */
54
+ export function dtype(mode) {
55
+ switch (mode & S_IFMT) {
56
+ case S_IFDIR: return 4
57
+ case S_IFREG: return 8
58
+ case S_IFLNK: return 10
59
+ case S_IFCHR: return 2
60
+ case S_IFBLK: return 6
61
+ case S_IFIFO: return 1
62
+ case S_IFSOCK: return 12
63
+ }
64
+ return 0
65
+ }
66
+
67
+ function qidOf(n) {
68
+ return { type: n.qtype, version: 0, path: n.ino }
69
+ }
70
+
71
+ function checkSafeName(name) {
72
+ if (name === '' || name === '.' || name === '..' || name.includes('/')) throw new FsError(EINVAL)
73
+ }
74
+
75
+ function splitNs(ns) {
76
+ // Floor division, so nsec is in [0, 1e9) as Linux's timespec needs (Go's
77
+ // splitNanos truncates: -1.5 s would give nsec -5e8). A negative sec goes in
78
+ // the u64 field as two's complement.
79
+ let s = ns / NS
80
+ let r = ns % NS
81
+ if (r < 0n) {
82
+ s -= 1n
83
+ r += NS
84
+ }
85
+ return [s, r]
86
+ }
87
+
88
+ function num(v) {
89
+ // A u64 field as a Number, Infinity when it is too large to be one.
90
+ return typeof v === 'bigint' ? Infinity : v
91
+ }
92
+
93
+ export class Fid {
94
+ constructor(node, parent, name, uid) {
95
+ this.node = node
96
+ this.parent = parent // the directory it was walked from (for Tremove/Trename)
97
+ this.name = name
98
+ this.uid = uid
99
+ this.opened = false
100
+ this.omode = 0
101
+ this.append = false // opened with O_APPEND: every Twrite goes to the end of the file
102
+ this.xattr = null // {op: 'walk', buf} | {op: 'create', name, size, flags, buf, len}
103
+ this.snap = null // readdir listing ({names, nodes}), taken at offset 0
104
+ }
105
+ }
106
+
107
+ export class NinePServer {
108
+ /**
109
+ * @param {import('../overlay.js').Overlay} fs
110
+ * @param {{maxMsize?: number}} [o]
111
+ */
112
+ constructor(fs, { maxMsize = MAX_MSIZE } = {}) {
113
+ this.fs = fs
114
+ this.maxMsize = maxMsize
115
+ this.msize = maxMsize
116
+ this.fids = new Map()
117
+ this.counts = {} // message name -> count
118
+ this.internalErrors = [] // unexpected exceptions (bugs), newest last, capped
119
+ }
120
+
121
+ /** Handles one request frame; returns the reply frame. Never throws. */
122
+ handle(req) {
123
+ let tag = NOTAG
124
+ let type = 0
125
+ try {
126
+ const h = header(req)
127
+ tag = h.tag
128
+ type = h.type
129
+ const m = decode(req)
130
+ this.counts[m.op] = (this.counts[m.op] || 0) + 1
131
+ const fn = this[m.op]
132
+ if (typeof fn !== 'function' || m.op[0] !== 'T') return this.#err(tag, ENOSYS)
133
+ const [rname, fields] = fn.call(this, m)
134
+ return encode(rname, tag, fields)
135
+ } catch (e) {
136
+ if (e instanceof FsError) return this.#err(tag, e.errno)
137
+ if (e instanceof ProtoError) {
138
+ if (e.type !== undefined) {
139
+ this.counts[`type${e.type}`] = (this.counts[`type${e.type}`] || 0) + 1
140
+ return this.#err(tag, ENOSYS)
141
+ }
142
+ return this.#err(tag, EINVAL)
143
+ }
144
+ this.internalErrors.push({ type, error: String(e && e.stack || e) })
145
+ if (this.internalErrors.length > 32) this.internalErrors.shift()
146
+ return this.#err(tag, EIO)
147
+ }
148
+ }
149
+
150
+ #err(tag, errno) {
151
+ return encode(T.Rlerror, tag, { ecode: errno })
152
+ }
153
+
154
+ #fid(n) {
155
+ const f = this.fids.get(n)
156
+ if (!f) throw new FsError(EBADF)
157
+ return f
158
+ }
159
+
160
+ // Makes fid id be f. Fid ids are the client's; 9P says a new one must not
161
+ // be in use (walk(5)), so callers check that first. The overlay counts the
162
+ // fids on each inode: an unlinked file's contents go when the last goes.
163
+ #setFid(id, f) {
164
+ const old = this.fids.get(id)
165
+ this.fids.set(id, f)
166
+ this.fs.ref(f.node)
167
+ if (old) this.fs.unref(old.node)
168
+ }
169
+
170
+ #dropFid(id) {
171
+ const f = this.fids.get(id)
172
+ if (!f) return
173
+ this.fids.delete(id)
174
+ this.fs.unref(f.node)
175
+ }
176
+
177
+ #dropAll() {
178
+ for (const id of [...this.fids.keys()]) this.#dropFid(id)
179
+ }
180
+
181
+ // A new fid id must not be in use: EBADF, as Linux's v9fs never reuses a
182
+ // live fid (wmd's Go server replaces it).
183
+ #checkNewFid(id) {
184
+ if (this.fids.has(id)) throw new FsError(EBADF)
185
+ }
186
+
187
+ #dirFid(n) {
188
+ const f = this.#fid(n)
189
+ if (!f.node.isDir()) throw new FsError(ENOTDIR)
190
+ return f
191
+ }
192
+
193
+ // ---- session -----------------------------------------------------------
194
+
195
+ Tversion(m) {
196
+ this.#dropAll()
197
+ if (!m.version.startsWith(VERSION) || m.msize < HEADER + 4) {
198
+ return ['Rversion', { msize: Math.min(m.msize, this.maxMsize), version: 'unknown' }]
199
+ }
200
+ this.msize = Math.min(m.msize, this.maxMsize)
201
+ return ['Rversion', { msize: this.msize, version: VERSION }]
202
+ }
203
+
204
+ Tauth() {
205
+ throw new FsError(ENOSYS)
206
+ }
207
+
208
+ Tattach(m) {
209
+ if (m.afid !== NOFID) throw new FsError(EINVAL)
210
+ this.#checkNewFid(m.fid)
211
+ const uid = m.n_uname === NOFID ? 0 : m.n_uname
212
+ let node = this.fs.root
213
+ let parent = null
214
+ let name = ''
215
+ for (const part of m.aname.split('/').filter((s) => s !== '')) {
216
+ checkSafeName(part)
217
+ if (!node.isDir()) throw new FsError(node.type === S_IFLNK ? ELOOP : ENOTDIR)
218
+ parent = node
219
+ name = part
220
+ node = this.fs.lookup(node, part)
221
+ }
222
+ this.#setFid(m.fid, new Fid(node, parent, name, uid))
223
+ return ['Rattach', { qid: qidOf(node) }]
224
+ }
225
+
226
+ Tflush() {
227
+ // Requests are answered one at a time, in order: none is ever pending.
228
+ return ['Rflush', {}]
229
+ }
230
+
231
+ Tclunk(m) {
232
+ const f = this.#fid(m.fid)
233
+ this.#dropFid(m.fid)
234
+ const x = f.xattr
235
+ if (x && x.op === 'create') {
236
+ if (x.len !== x.size) throw new FsError(EINVAL)
237
+ if (x.flags === XATTR_REPLACE && x.size === 0) this.fs.removexattr(f.node, x.name)
238
+ else this.fs.setxattr(f.node, x.name, x.buf.subarray(0, x.len), x.flags)
239
+ }
240
+ return ['Rclunk', {}]
241
+ }
242
+
243
+ // ---- walking -----------------------------------------------------------
244
+
245
+ Twalk(m) {
246
+ const f = this.#fid(m.fid)
247
+ if (f.opened && m.newfid === m.fid) throw new FsError(EBUSY)
248
+ if (m.newfid !== m.fid) this.#checkNewFid(m.newfid)
249
+ if (m.wnames.length > MAXWELEM) throw new FsError(EINVAL)
250
+ let node = f.node
251
+ let parent = f.parent
252
+ let name = f.name
253
+ const qids = []
254
+ for (const w of m.wnames) {
255
+ checkSafeName(w)
256
+ if (!node.isDir()) throw new FsError(node.type === S_IFLNK ? ELOOP : ENOTDIR)
257
+ if (node.removed) throw new FsError(ENOENT)
258
+ parent = node
259
+ name = w
260
+ node = this.fs.lookup(node, w)
261
+ qids.push(qidOf(node))
262
+ }
263
+ this.#setFid(m.newfid, new Fid(node, parent, name, f.uid))
264
+ return ['Rwalk', { qids }]
265
+ }
266
+
267
+ // ---- attributes --------------------------------------------------------
268
+
269
+ Tgetattr(m) {
270
+ const n = this.#fid(m.fid).node
271
+ const a = this.fs.getattr(n)
272
+ const [as, an] = splitNs(a.atime)
273
+ const [ms, mn] = splitNs(a.mtime)
274
+ const [cs, cn] = splitNs(a.ctime)
275
+ return ['Rgetattr', {
276
+ valid: GETATTR_BASIC, qid: qidOf(n), mode: a.mode, uid: a.uid, gid: a.gid,
277
+ nlink: a.nlink, rdev: a.rdev, size: a.size, blksize: a.blksize, blocks: a.blocks,
278
+ atime_sec: as, atime_nsec: an, mtime_sec: ms, mtime_nsec: mn, ctime_sec: cs, ctime_nsec: cn,
279
+ btime_sec: 0, btime_nsec: 0, gen: 0, data_version: 0,
280
+ }]
281
+ }
282
+
283
+ Tsetattr(m) {
284
+ const n = this.#fid(m.fid).node
285
+ const v = m.valid
286
+ const s = {}
287
+ if (v & SETATTR_MODE) s.mode = m.mode & 0o7777
288
+ if (v & SETATTR_UID && m.uid !== NOFID) s.uid = m.uid
289
+ if (v & SETATTR_GID && m.gid !== NOFID) s.gid = m.gid
290
+ if (v & SETATTR_SIZE) {
291
+ const size = num(m.size)
292
+ if (!Number.isFinite(size)) throw new FsError(EFBIG)
293
+ s.size = size
294
+ }
295
+ if (v & SETATTR_ATIME) {
296
+ s.atime = v & SETATTR_ATIME_SET ? BigInt(m.atime_sec) * NS + BigInt(m.atime_nsec) : this.fs.now()
297
+ }
298
+ if (v & SETATTR_MTIME) {
299
+ s.mtime = v & SETATTR_MTIME_SET ? BigInt(m.mtime_sec) * NS + BigInt(m.mtime_nsec) : this.fs.now()
300
+ }
301
+ this.fs.setattr(n, s)
302
+ return ['Rsetattr', {}]
303
+ }
304
+
305
+ Tstatfs(m) {
306
+ this.#fid(m.fid)
307
+ return ['Rstatfs', this.fs.statfs()]
308
+ }
309
+
310
+ Treadlink(m) {
311
+ return ['Rreadlink', { target: this.fs.readlink(this.#fid(m.fid).node) }]
312
+ }
313
+
314
+ // ---- opening, reading, writing -----------------------------------------
315
+
316
+ Tlopen(m) {
317
+ const f = this.#fid(m.fid)
318
+ if (f.opened || f.xattr) throw new FsError(EINVAL)
319
+ const acc = m.flags & O_ACCMODE
320
+ const n = f.node
321
+ if (m.flags & O_DIRECTORY && !n.isDir()) throw new FsError(ENOTDIR)
322
+ if (n.isDir() && acc !== O_RDONLY) throw new FsError(EISDIR)
323
+ this.fs.open(n, { write: acc !== O_RDONLY, trunc: (m.flags & O_TRUNC) !== 0 })
324
+ f.opened = true
325
+ f.omode = acc
326
+ f.append = (m.flags & O_APPEND) !== 0
327
+ f.snap = null
328
+ return ['Rlopen', { qid: qidOf(n), iounit: 0 }]
329
+ }
330
+
331
+ Tlcreate(m) {
332
+ const f = this.#dirFid(m.fid)
333
+ if (f.opened) throw new FsError(EINVAL)
334
+ checkSafeName(m.name)
335
+ const n = this.fs.create(f.node, m.name, m.mode & 0o7777, f.uid, m.gid)
336
+ const nf = new Fid(n, f.node, m.name, f.uid)
337
+ nf.opened = true
338
+ nf.omode = m.flags & O_ACCMODE
339
+ nf.append = (m.flags & O_APPEND) !== 0
340
+ this.#setFid(m.fid, nf)
341
+ return ['Rlcreate', { qid: qidOf(n), iounit: 0 }]
342
+ }
343
+
344
+ Tread(m) {
345
+ const f = this.#fid(m.fid)
346
+ const max = this.msize - (HEADER + 4)
347
+ const count = Math.min(m.count, max)
348
+ const off = num(m.offset)
349
+ if (f.xattr) {
350
+ if (f.xattr.op !== 'walk') throw new FsError(EINVAL)
351
+ const b = f.xattr.buf
352
+ if (!Number.isFinite(off) || off >= b.length) return ['Rread', { data: new Uint8Array(0) }]
353
+ return ['Rread', { data: b.subarray(off, off + count) }]
354
+ }
355
+ if (!f.opened || f.omode === O_WRONLY) throw new FsError(EBADF)
356
+ if (!Number.isFinite(off)) return ['Rread', { data: new Uint8Array(0) }]
357
+ return ['Rread', { data: this.fs.read(f.node, off, count) }]
358
+ }
359
+
360
+ Twrite(m) {
361
+ const f = this.#fid(m.fid)
362
+ const off = num(m.offset)
363
+ if (f.xattr) {
364
+ const x = f.xattr
365
+ if (x.op !== 'create' || off !== x.len || x.len + m.data.length > x.size) throw new FsError(EINVAL)
366
+ x.buf.set(m.data, x.len)
367
+ x.len += m.data.length
368
+ return ['Rwrite', { count: m.data.length }]
369
+ }
370
+ if (!f.opened || f.omode === O_RDONLY) throw new FsError(EBADF)
371
+ if (!Number.isFinite(off)) throw new FsError(EFBIG)
372
+ // O_APPEND: at the end of the file, whatever offset the client computed (as QEMU's
373
+ // server, whose pwrite on an O_APPEND file appends, and Linux's own pwrite(2)). Without
374
+ // the page cache (cache=mmap/none) v9fs takes the offset from its i_size with no lock
375
+ // (v9fs_file_write_iter), so two processes appending to one file would overwrite each
376
+ // other's records; here each Twrite lands whole after the last.
377
+ const at = f.append ? this.fs.size(f.node) : off
378
+ return ['Rwrite', { count: this.fs.write(f.node, at, m.data) }]
379
+ }
380
+
381
+ Tfsync(m) {
382
+ this.#fid(m.fid)
383
+ return ['Rfsync', {}]
384
+ }
385
+
386
+ Treaddir(m) {
387
+ const f = this.#fid(m.fid)
388
+ if (!f.node.isDir()) throw new FsError(ENOTDIR)
389
+ if (!f.opened) throw new FsError(EBADF)
390
+ const off = num(m.offset)
391
+ if (off === 0 || !f.snap) f.snap = this.fs.listing(f.node)
392
+ const { names, nodes } = f.snap
393
+ const count = Math.min(m.count, this.msize - (HEADER + 4))
394
+ const out = []
395
+ let used = 0
396
+ for (let i = Number.isFinite(off) ? off : names.length; i < names.length; i++) {
397
+ const name = names[i]
398
+ const n = nodes[i]
399
+ const sz = direntSize(name)
400
+ if (used + sz > count) break
401
+ used += sz
402
+ out.push({ qid: qidOf(n), offset: i + 1, type: dtype(n.mode), name })
403
+ }
404
+ return ['Rreaddir', { data: encodeDirents(out) }]
405
+ }
406
+
407
+ // ---- making and removing -----------------------------------------------
408
+
409
+ Tmkdir(m) {
410
+ const d = this.#dirFid(m.dfid)
411
+ checkSafeName(m.name)
412
+ const n = this.fs.mkdir(d.node, m.name, m.mode & 0o7777, d.uid, m.gid)
413
+ return ['Rmkdir', { qid: qidOf(n) }]
414
+ }
415
+
416
+ Tsymlink(m) {
417
+ const d = this.#dirFid(m.fid)
418
+ checkSafeName(m.name)
419
+ const n = this.fs.symlink(d.node, m.name, m.symtgt, d.uid, m.gid)
420
+ return ['Rsymlink', { qid: qidOf(n) }]
421
+ }
422
+
423
+ Tmknod(m) {
424
+ const d = this.#dirFid(m.dfid)
425
+ checkSafeName(m.name)
426
+ const n = this.fs.mknod(d.node, m.name, m.mode, makedev(m.major, m.minor), d.uid, m.gid)
427
+ return ['Rmknod', { qid: qidOf(n) }]
428
+ }
429
+
430
+ Tlink(m) {
431
+ const d = this.#dirFid(m.dfid)
432
+ const t = this.#fid(m.fid)
433
+ checkSafeName(m.name)
434
+ this.fs.link(d.node, m.name, t.node)
435
+ return ['Rlink', {}]
436
+ }
437
+
438
+ Tunlinkat(m) {
439
+ const d = this.#dirFid(m.dirfid)
440
+ checkSafeName(m.name)
441
+ this.fs.unlink(d.node, m.name, (m.flags & AT_REMOVEDIR) !== 0)
442
+ return ['Runlinkat', {}]
443
+ }
444
+
445
+ Trenameat(m) {
446
+ const od = this.#dirFid(m.olddirfid)
447
+ const nd = this.#dirFid(m.newdirfid)
448
+ checkSafeName(m.oldname)
449
+ checkSafeName(m.newname)
450
+ this.fs.rename(od.node, m.oldname, nd.node, m.newname)
451
+ return ['Rrenameat', {}]
452
+ }
453
+
454
+ Trename(m) {
455
+ const f = this.#fid(m.fid)
456
+ const d = this.#dirFid(m.dfid)
457
+ checkSafeName(m.name)
458
+ if (!f.parent) throw new FsError(EINVAL)
459
+ if (f.parent.kids && f.parent.kids.get(f.name) !== f.node) throw new FsError(ENOENT)
460
+ this.fs.rename(f.parent, f.name, d.node, m.name)
461
+ f.parent = d.node
462
+ f.name = m.name
463
+ return ['Rrename', {}]
464
+ }
465
+
466
+ Tremove(m) {
467
+ // Removes the file and clunks the fid, even when the removal fails.
468
+ const f = this.#fid(m.fid)
469
+ try {
470
+ if (!f.parent) throw new FsError(EINVAL)
471
+ if (f.parent.kids && f.parent.kids.get(f.name) !== f.node) throw new FsError(ENOENT)
472
+ this.fs.unlink(f.parent, f.name, f.node.isDir())
473
+ } finally {
474
+ this.#dropFid(m.fid)
475
+ }
476
+ return ['Rremove', {}]
477
+ }
478
+
479
+ // ---- extended attributes -----------------------------------------------
480
+
481
+ Txattrwalk(m) {
482
+ const f = this.#fid(m.fid)
483
+ if (m.newfid !== m.fid) this.#checkNewFid(m.newfid)
484
+ let buf
485
+ if (m.name === '') {
486
+ const names = this.fs.listxattr(f.node)
487
+ let s = ''
488
+ for (const k of names) s += k + '\0'
489
+ buf = new Uint8Array(s.length)
490
+ for (let i = 0; i < s.length; i++) buf[i] = s.charCodeAt(i)
491
+ } else {
492
+ buf = this.fs.getxattr(f.node, m.name)
493
+ }
494
+ const nf = new Fid(f.node, f.parent, f.name, f.uid)
495
+ nf.xattr = { op: 'walk', buf }
496
+ this.#setFid(m.newfid, nf)
497
+ return ['Rxattrwalk', { size: buf.length }]
498
+ }
499
+
500
+ Txattrcreate(m) {
501
+ const f = this.#fid(m.fid)
502
+ if (f.opened) throw new FsError(EINVAL)
503
+ const size = num(m.size)
504
+ if (size > XATTR_SIZE_MAX) throw new FsError(E2BIG)
505
+ if (m.name.length === 0 || m.name.length > NAME_MAX) throw new FsError(EINVAL)
506
+ f.xattr = { op: 'create', name: m.name, size, flags: m.flags, buf: new Uint8Array(size), len: 0 }
507
+ return ['Rxattrcreate', {}]
508
+ }
509
+
510
+ // ---- locks: one guest per container, nothing to contend with -----------
511
+
512
+ Tlock(m) {
513
+ this.#fid(m.fid)
514
+ return ['Rlock', { status: LOCK_SUCCESS }]
515
+ }
516
+
517
+ Tgetlock(m) {
518
+ this.#fid(m.fid)
519
+ return ['Rgetlock', { ltype: LOCK_TYPE_UNLCK, start: m.start, length: m.length, proc_id: m.proc_id, client_id: m.client_id }]
520
+ }
521
+ }
@@ -0,0 +1,43 @@
1
+ // Linux errno values (asm-generic), as 9P2000.L's Rlerror carries them.
2
+
3
+ export const EPERM = 1
4
+ export const ENOENT = 2
5
+ export const EIO = 5
6
+ export const ENXIO = 6
7
+ export const E2BIG = 7
8
+ export const EBADF = 9
9
+ export const EAGAIN = 11
10
+ export const ENOMEM = 12
11
+ export const EACCES = 13
12
+ export const EBUSY = 16
13
+ export const EEXIST = 17
14
+ export const EXDEV = 18
15
+ export const ENOTDIR = 20
16
+ export const EISDIR = 21
17
+ export const EINVAL = 22
18
+ export const EFBIG = 27
19
+ export const ENOSPC = 28
20
+ export const EROFS = 30
21
+ export const EMLINK = 31
22
+ export const ERANGE = 34
23
+ export const ENAMETOOLONG = 36
24
+ export const ENOSYS = 38
25
+ export const ENOTEMPTY = 39
26
+ export const ELOOP = 40
27
+ export const ENODATA = 61
28
+ export const EPROTO = 71
29
+ export const EMSGSIZE = 90
30
+ export const EOPNOTSUPP = 95
31
+
32
+ /** A file system error with a Linux errno. */
33
+ export class FsError extends Error {
34
+ constructor(errno, msg) {
35
+ super(msg || `errno ${errno}`)
36
+ this.errno = errno
37
+ }
38
+ }
39
+
40
+ /** Throws FsError(errno). */
41
+ export function fail(errno, msg) {
42
+ throw new FsError(errno, msg)
43
+ }