@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,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 }
|