@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.
- package/README.md +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- 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
|
+
}
|
package/src/fs/errno.js
ADDED
|
@@ -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
|
+
}
|