@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,165 @@
|
|
|
1
|
+
// W62: a writer for wmd's pack format (internal/pack/pack.go Writer), byte-compatible with
|
|
2
|
+
// the Go writer for the entries it takes: no sparse runs (the JS reader has none either), so
|
|
3
|
+
// a file's contents are all at its entry's off.
|
|
4
|
+
//
|
|
5
|
+
// writePack(entries) -> Uint8Array. An entry is {path (byte string, '' the root), whiteout?,
|
|
6
|
+
// opaque?, mode, uid, gid, rdev, atime, mtime, ctime (BigInt ns), size, data? (Uint8Array, or
|
|
7
|
+
// a function (off, len) -> Uint8Array for a regular file's contents), target, link, xattrs
|
|
8
|
+
// (Map name -> Uint8Array)}. Entries with the same non-zero link share the first one's
|
|
9
|
+
// contents (pass data on the first only). The index is sorted by path as Go sorts it (byte
|
|
10
|
+
// order), and a path added twice is an error, as Go's Close says.
|
|
11
|
+
|
|
12
|
+
import { MAGIC, S_IFMT, S_IFREG, bytesOf } from './pack.js'
|
|
13
|
+
|
|
14
|
+
const FLAG_WHITEOUT = 1
|
|
15
|
+
const FLAG_OPAQUE = 2
|
|
16
|
+
const READ_CHUNK = 1 << 20
|
|
17
|
+
|
|
18
|
+
class Bytes {
|
|
19
|
+
constructor(n = 1 << 16) {
|
|
20
|
+
this.b = new Uint8Array(n)
|
|
21
|
+
this.n = 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
room(k) {
|
|
25
|
+
if (this.n + k <= this.b.length) return
|
|
26
|
+
let len = this.b.length * 2
|
|
27
|
+
while (len < this.n + k) len *= 2
|
|
28
|
+
const g = new Uint8Array(len)
|
|
29
|
+
g.set(this.b.subarray(0, this.n))
|
|
30
|
+
this.b = g
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
byte(x) {
|
|
34
|
+
this.room(1)
|
|
35
|
+
this.b[this.n++] = x
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
raw(u8) {
|
|
39
|
+
this.room(u8.length)
|
|
40
|
+
this.b.set(u8, this.n)
|
|
41
|
+
this.n += u8.length
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Go's binary.AppendUvarint (x a Number up to 2^53 or a BigInt up to 2^64)
|
|
45
|
+
uvarint(x) {
|
|
46
|
+
if (typeof x === 'number' && x < 2 ** 31) {
|
|
47
|
+
if (x < 0) throw new RangeError(`uvarint ${x}`)
|
|
48
|
+
while (x >= 0x80) {
|
|
49
|
+
this.byte((x & 0x7f) | 0x80)
|
|
50
|
+
x >>>= 7
|
|
51
|
+
}
|
|
52
|
+
this.byte(x)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
let v = BigInt(x)
|
|
56
|
+
if (v < 0n || v >= 1n << 64n) throw new RangeError(`uvarint ${x}`)
|
|
57
|
+
while (v >= 0x80n) {
|
|
58
|
+
this.byte(Number(v & 0x7fn) | 0x80)
|
|
59
|
+
v >>= 7n
|
|
60
|
+
}
|
|
61
|
+
this.byte(Number(v))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Go's binary.AppendVarint: zig-zag
|
|
65
|
+
varint(x) {
|
|
66
|
+
const v = BigInt(x)
|
|
67
|
+
if (v < -(1n << 63n) || v >= 1n << 63n) throw new RangeError(`varint ${x}`)
|
|
68
|
+
this.uvarint(v < 0n ? (-v << 1n) - 1n : v << 1n)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
str(s) {
|
|
72
|
+
const u8 = typeof s === 'string' ? bytesOf(s) : s
|
|
73
|
+
this.uvarint(u8.length)
|
|
74
|
+
this.raw(u8)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
done() {
|
|
78
|
+
return this.b.subarray(0, this.n)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// a byte string's order is code unit order (pack.js), the order Go sorts its index in
|
|
83
|
+
const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)
|
|
84
|
+
|
|
85
|
+
/** The pack holding entries, as bytes. */
|
|
86
|
+
export function writePack(entries) {
|
|
87
|
+
const ents = entries.map(e => ({ ...e, path: cleanPath(e.path) }))
|
|
88
|
+
const data = new Bytes(1 << 20)
|
|
89
|
+
data.raw(bytesOf(MAGIC))
|
|
90
|
+
const linked = new Map() // link -> {off, size}
|
|
91
|
+
for (const e of ents) {
|
|
92
|
+
e.off = 0
|
|
93
|
+
if (e.whiteout || ((e.mode >>> 0) & S_IFMT) !== S_IFREG) continue
|
|
94
|
+
const had = e.link ? linked.get(e.link) : undefined
|
|
95
|
+
if (had) {
|
|
96
|
+
e.off = had.off
|
|
97
|
+
e.size = had.size
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
const size = e.size || 0
|
|
101
|
+
e.off = data.n
|
|
102
|
+
if (size > 0) {
|
|
103
|
+
if (typeof e.data === 'function') {
|
|
104
|
+
for (let o = 0; o < size; o += READ_CHUNK) {
|
|
105
|
+
const want = Math.min(READ_CHUNK, size - o)
|
|
106
|
+
const got = e.data(o, want)
|
|
107
|
+
if (got.length !== want) throw new Error(`pack ${JSON.stringify(e.path)}: read ${got.length} of ${want} bytes at ${o}`)
|
|
108
|
+
data.raw(got)
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
if (!e.data || e.data.length !== size) throw new Error(`pack ${JSON.stringify(e.path)}: ${e.data ? e.data.length : 0} bytes of data for size ${size}`)
|
|
112
|
+
data.raw(e.data)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (e.link) linked.set(e.link, { off: e.off, size })
|
|
116
|
+
}
|
|
117
|
+
ents.sort(byPath)
|
|
118
|
+
for (let i = 1; i < ents.length; i++) {
|
|
119
|
+
if (ents[i].path === ents[i - 1].path) throw new Error(`pack: ${JSON.stringify(ents[i].path)} added twice`)
|
|
120
|
+
}
|
|
121
|
+
const start = data.n
|
|
122
|
+
data.uvarint(ents.length)
|
|
123
|
+
for (const e of ents) entry(data, e)
|
|
124
|
+
const len = data.n - start
|
|
125
|
+
const foot = new Uint8Array(16)
|
|
126
|
+
const dv = new DataView(foot.buffer)
|
|
127
|
+
dv.setBigUint64(0, BigInt(start), true)
|
|
128
|
+
dv.setBigUint64(8, BigInt(len), true)
|
|
129
|
+
data.raw(foot)
|
|
130
|
+
data.raw(bytesOf(MAGIC))
|
|
131
|
+
return data.done().slice()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cleanPath(p) {
|
|
135
|
+
const parts = []
|
|
136
|
+
for (const s of p.split('/')) {
|
|
137
|
+
if (s === '' || s === '.') continue
|
|
138
|
+
if (s === '..') parts.pop()
|
|
139
|
+
else parts.push(s)
|
|
140
|
+
}
|
|
141
|
+
return parts.join('/')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function entry(w, e) {
|
|
145
|
+
w.str(e.path)
|
|
146
|
+
w.byte((e.whiteout ? FLAG_WHITEOUT : 0) | (e.opaque ? FLAG_OPAQUE : 0))
|
|
147
|
+
if (e.whiteout) return
|
|
148
|
+
w.uvarint(e.mode >>> 0)
|
|
149
|
+
w.uvarint((e.uid || 0) >>> 0)
|
|
150
|
+
w.uvarint((e.gid || 0) >>> 0)
|
|
151
|
+
w.uvarint(e.rdev || 0)
|
|
152
|
+
w.varint(e.atime || 0n)
|
|
153
|
+
w.varint(e.mtime || 0n)
|
|
154
|
+
w.varint(e.ctime || 0n)
|
|
155
|
+
w.varint(e.size || 0)
|
|
156
|
+
w.varint(e.off || 0)
|
|
157
|
+
w.str(e.target || '')
|
|
158
|
+
w.uvarint(e.link || 0)
|
|
159
|
+
const keys = e.xattrs ? [...e.xattrs.keys()].sort() : []
|
|
160
|
+
w.uvarint(keys.length)
|
|
161
|
+
for (const k of keys) {
|
|
162
|
+
w.str(k)
|
|
163
|
+
w.str(e.xattrs.get(k))
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/fs/pack.js
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// Reader for wmd's pack format (internal/pack/pack.go), byte-compatible with
|
|
2
|
+
// the Go reader.
|
|
3
|
+
//
|
|
4
|
+
// A pack is one image layer in one file:
|
|
5
|
+
//
|
|
6
|
+
// magic "WMDPACK1"
|
|
7
|
+
// data (each regular file's contents at its entry's off)
|
|
8
|
+
// index (uvarint count, then the entries, sorted by path)
|
|
9
|
+
// footer (index offset u64 LE, index length u64 LE, magic)
|
|
10
|
+
//
|
|
11
|
+
// An entry is: path (uvarint length + bytes), flags (1 byte: 1 whiteout,
|
|
12
|
+
// 2 opaque) and, unless it is a whiteout, mode, uid, gid, rdev (uvarints),
|
|
13
|
+
// atime, mtime, ctime, size, off (zig-zag varints), target (string), link
|
|
14
|
+
// (uvarint) and the xattrs (uvarint count, then name and value strings).
|
|
15
|
+
//
|
|
16
|
+
// Names are kept as "byte strings": one UTF-16 code unit per byte (Latin-1),
|
|
17
|
+
// so any byte sequence a Linux name can hold round-trips exactly and string
|
|
18
|
+
// order is byte order, the order Go sorts the index in. bytesOf/strOf convert.
|
|
19
|
+
|
|
20
|
+
export const MAGIC = 'WMDPACK1'
|
|
21
|
+
|
|
22
|
+
// Linux file types (st_mode & S_IFMT).
|
|
23
|
+
export const S_IFMT = 0o170000
|
|
24
|
+
export const S_IFREG = 0o100000
|
|
25
|
+
export const S_IFDIR = 0o040000
|
|
26
|
+
export const S_IFLNK = 0o120000
|
|
27
|
+
export const S_IFIFO = 0o010000
|
|
28
|
+
export const S_IFSOCK = 0o140000
|
|
29
|
+
export const S_IFCHR = 0o020000
|
|
30
|
+
export const S_IFBLK = 0o060000
|
|
31
|
+
|
|
32
|
+
const FLAG_WHITEOUT = 1
|
|
33
|
+
const FLAG_OPAQUE = 2
|
|
34
|
+
|
|
35
|
+
export class PackFormatError extends Error {}
|
|
36
|
+
|
|
37
|
+
/** A byte string (one char per byte) from bytes. */
|
|
38
|
+
export function strOf(u8) {
|
|
39
|
+
let s = ''
|
|
40
|
+
for (let i = 0; i < u8.length; i += 8192) {
|
|
41
|
+
s += String.fromCharCode.apply(null, u8.subarray(i, i + 8192))
|
|
42
|
+
}
|
|
43
|
+
return s
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The bytes of a byte string. Chars above 0xff are not bytes: an error. */
|
|
47
|
+
export function bytesOf(s) {
|
|
48
|
+
const out = new Uint8Array(s.length)
|
|
49
|
+
for (let i = 0; i < s.length; i++) {
|
|
50
|
+
const c = s.charCodeAt(i)
|
|
51
|
+
if (c > 0xff) throw new RangeError('not a byte string')
|
|
52
|
+
out[i] = c
|
|
53
|
+
}
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A UTF-8 JavaScript string as a byte string. */
|
|
58
|
+
export function byteStr(utf8) {
|
|
59
|
+
return strOf(new TextEncoder().encode(utf8))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A random access source over bytes held in memory. */
|
|
63
|
+
export function bytesSource(u8) {
|
|
64
|
+
return {
|
|
65
|
+
size: u8.length,
|
|
66
|
+
read(off, len) {
|
|
67
|
+
return u8.subarray(off, Math.min(off + len, u8.length))
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A source over an OPFS FileSystemSyncAccessHandle (Worker only). */
|
|
73
|
+
export function syncHandleSource(handle) {
|
|
74
|
+
return {
|
|
75
|
+
size: handle.getSize(),
|
|
76
|
+
read(off, len) {
|
|
77
|
+
const b = new Uint8Array(len)
|
|
78
|
+
const n = handle.read(b, { at: off })
|
|
79
|
+
return n === len ? b : b.subarray(0, n)
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Linux's device number for major and minor (new_encode_dev). */
|
|
85
|
+
export function makedev(major, minor) {
|
|
86
|
+
return (major & 0xfff) * 256 + Math.floor(major / 4096) * 2 ** 44 + (minor & 0xff) + Math.floor(minor / 256) * 2 ** 20
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A Linux device number's major. */
|
|
90
|
+
export function major(dev) {
|
|
91
|
+
dev = BigInt(dev)
|
|
92
|
+
return Number(((dev >> 8n) & 0xfffn) | ((dev >> 32n) & ~0xfffn))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A Linux device number's minor. */
|
|
96
|
+
export function minor(dev) {
|
|
97
|
+
dev = BigInt(dev)
|
|
98
|
+
return Number((dev & 0xffn) | ((dev >> 12n) & 0xffffff00n))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER)
|
|
102
|
+
|
|
103
|
+
class Decoder {
|
|
104
|
+
constructor(b) {
|
|
105
|
+
this.b = b
|
|
106
|
+
this.p = 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
bad() {
|
|
110
|
+
throw new PackFormatError('not a pack')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// uvarint is a Number when it fits in 53 bits, else a BigInt.
|
|
114
|
+
uvarint() {
|
|
115
|
+
const b = this.b
|
|
116
|
+
let p = this.p
|
|
117
|
+
let x = 0
|
|
118
|
+
let mul = 1
|
|
119
|
+
for (let i = 0; i < 7; i++) {
|
|
120
|
+
if (p >= b.length) this.bad()
|
|
121
|
+
const c = b[p++]
|
|
122
|
+
x += (c & 0x7f) * mul
|
|
123
|
+
if (c < 0x80) {
|
|
124
|
+
this.p = p
|
|
125
|
+
return x
|
|
126
|
+
}
|
|
127
|
+
mul *= 128
|
|
128
|
+
}
|
|
129
|
+
// Beyond 49 bits: finish in BigInt.
|
|
130
|
+
let big = BigInt(x)
|
|
131
|
+
let shift = 49n
|
|
132
|
+
for (let i = 7; i < 10; i++) {
|
|
133
|
+
if (p >= b.length) this.bad()
|
|
134
|
+
const c = b[p++]
|
|
135
|
+
if (i === 9 && c > 1) this.bad() // overflows 64 bits, as Go's Uvarint says
|
|
136
|
+
big |= BigInt(c & 0x7f) << shift
|
|
137
|
+
if (c < 0x80) {
|
|
138
|
+
this.p = p
|
|
139
|
+
return big <= MAX_SAFE ? Number(big) : big
|
|
140
|
+
}
|
|
141
|
+
shift += 7n
|
|
142
|
+
}
|
|
143
|
+
this.bad()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// varint is zig-zag: a Number when safe, else a BigInt.
|
|
147
|
+
varint() {
|
|
148
|
+
const u = this.uvarint()
|
|
149
|
+
if (typeof u === 'number') {
|
|
150
|
+
const h = Math.floor(u / 2)
|
|
151
|
+
return u % 2 ? -h - 1 : h
|
|
152
|
+
}
|
|
153
|
+
let x = u >> 1n
|
|
154
|
+
if (u & 1n) x = ~x
|
|
155
|
+
return x >= -MAX_SAFE && x <= MAX_SAFE ? Number(x) : x
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
bytes() {
|
|
159
|
+
const n = this.uvarint()
|
|
160
|
+
if (typeof n !== 'number' || n > this.b.length - this.p) this.bad()
|
|
161
|
+
const s = this.b.subarray(this.p, this.p + n)
|
|
162
|
+
this.p += n
|
|
163
|
+
return s
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
str() {
|
|
167
|
+
return strOf(this.bytes())
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const ROOT = -1 // the parent of a top-level entry
|
|
172
|
+
const NONE = -1 // no child, no sibling
|
|
173
|
+
// Distinct times kept as one BigInt each (images share few mtimes).
|
|
174
|
+
const MAX_INTERNED_TIMES = 1 << 16
|
|
175
|
+
// Names kept as one string each: the first this many distinct, this short.
|
|
176
|
+
const MAX_INTERNED_NAMES = 1 << 16
|
|
177
|
+
const MAX_INTERNED_NAME = 32
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* An open pack. Its index stays in memory as the pack's bytes (one copy),
|
|
181
|
+
* with three 32-bit numbers per entry (where it starts, its first child, its
|
|
182
|
+
* next sibling); an entry is decoded when asked for (entry(i), lookup()),
|
|
183
|
+
* never kept. Opening still decodes and checks every entry once, so a bad
|
|
184
|
+
* pack is refused at open, as Go's reader does.
|
|
185
|
+
*
|
|
186
|
+
* A decoded entry is {path, name, dir, whiteout, opaque, mode, uid, gid,
|
|
187
|
+
* rdev, atime, mtime, ctime (BigInt ns), size, off, target, link, xattrs
|
|
188
|
+
* (Map|null)}, a new object each time.
|
|
189
|
+
*/
|
|
190
|
+
export class Pack {
|
|
191
|
+
constructor(source, name = 'pack') {
|
|
192
|
+
this.source = source
|
|
193
|
+
this.name = name
|
|
194
|
+
const size = source.size
|
|
195
|
+
const foot = 16 + MAGIC.length
|
|
196
|
+
if (size < 2 * MAGIC.length + 16) throw new PackFormatError(`${name}: not a pack`)
|
|
197
|
+
const f = source.read(size - foot, foot)
|
|
198
|
+
if (strOf(f.subarray(16)) !== MAGIC) throw new PackFormatError(`${name}: not a pack`)
|
|
199
|
+
const dv = new DataView(f.buffer, f.byteOffset, f.byteLength)
|
|
200
|
+
const start = Number(dv.getBigUint64(0, true))
|
|
201
|
+
const n = Number(dv.getBigUint64(8, true))
|
|
202
|
+
if (start < MAGIC.length || n < 0 || start + n !== size - foot) {
|
|
203
|
+
throw new PackFormatError(`${name}: not a pack`)
|
|
204
|
+
}
|
|
205
|
+
this.dataEnd = start
|
|
206
|
+
this.times = new Map()
|
|
207
|
+
this.lastTimeAt = 0
|
|
208
|
+
this.lastTimeLen = 0
|
|
209
|
+
this.lastTime = 0n
|
|
210
|
+
this.names = new Map()
|
|
211
|
+
// A copy: the source's bytes may be a view the caller reuses.
|
|
212
|
+
const d = new Decoder(source.read(start, n).slice())
|
|
213
|
+
this.idx = d.b
|
|
214
|
+
const count = d.uvarint()
|
|
215
|
+
if (typeof count !== 'number' || count > n || count >= 2 ** 31) throw new PackFormatError(`${name}: not a pack`)
|
|
216
|
+
this.count = count
|
|
217
|
+
this.pos = n < 2 ** 32 ? new Uint32Array(count) : new Float64Array(count)
|
|
218
|
+
this.firstChild = new Int32Array(count).fill(NONE)
|
|
219
|
+
this.nextSibling = new Int32Array(count).fill(NONE)
|
|
220
|
+
this.rootFirst = NONE
|
|
221
|
+
this.links = new Map() // hard-link group -> {n, first}
|
|
222
|
+
const lastChild = new Int32Array(count)
|
|
223
|
+
let rootLast = NONE
|
|
224
|
+
const dirs = new Map() // directory path -> index, while opening only
|
|
225
|
+
let prev = null
|
|
226
|
+
for (let i = 0; i < count; i++) {
|
|
227
|
+
this.pos[i] = d.p
|
|
228
|
+
const e = this.#entry(d)
|
|
229
|
+
if (prev !== null && prev >= e.path) {
|
|
230
|
+
throw new PackFormatError(`${name}: index not sorted at ${JSON.stringify(e.path)}`)
|
|
231
|
+
}
|
|
232
|
+
prev = e.path
|
|
233
|
+
if (!e.whiteout && (e.mode & S_IFMT) === S_IFREG && (e.off < 0 || e.size < 0 || e.off + e.size > start)) {
|
|
234
|
+
throw new PackFormatError(`${name}: ${e.path}: contents outside the data`)
|
|
235
|
+
}
|
|
236
|
+
if (e.link) {
|
|
237
|
+
const g = this.links.get(e.link)
|
|
238
|
+
if (g) g.n++
|
|
239
|
+
else this.links.set(e.link, { n: 1, first: i })
|
|
240
|
+
}
|
|
241
|
+
if (e.path === '') continue
|
|
242
|
+
if (!e.whiteout && (e.mode & S_IFMT) === S_IFDIR) dirs.set(e.path, i)
|
|
243
|
+
// Children are linked in index order, which is name order. An entry
|
|
244
|
+
// whose directory is not a directory entry of this pack is reachable
|
|
245
|
+
// by no lookup (as before: only directory entries are listed).
|
|
246
|
+
const parent = e.dir === '' ? ROOT : dirs.get(e.dir)
|
|
247
|
+
if (parent === undefined) continue
|
|
248
|
+
if (parent === ROOT) {
|
|
249
|
+
if (rootLast === NONE) this.rootFirst = i
|
|
250
|
+
else this.nextSibling[rootLast] = i
|
|
251
|
+
rootLast = i
|
|
252
|
+
} else {
|
|
253
|
+
if (this.firstChild[parent] === NONE) this.firstChild[parent] = i
|
|
254
|
+
else this.nextSibling[lastChild[parent]] = i
|
|
255
|
+
lastChild[parent] = i
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// One string per distinct short name (index.js, package.json, ...).
|
|
261
|
+
#name(b) {
|
|
262
|
+
const s = strOf(b)
|
|
263
|
+
if (b.length > MAX_INTERNED_NAME) return s
|
|
264
|
+
const had = this.names.get(s)
|
|
265
|
+
if (had !== undefined) return had
|
|
266
|
+
if (this.names.size < MAX_INTERNED_NAMES) this.names.set(s, s)
|
|
267
|
+
return s
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// A time (zig-zag varint ns) as a BigInt, one per distinct time. The
|
|
271
|
+
// same bytes as the last time decoded (the common case: an image's files
|
|
272
|
+
// share an mtime) are that time, without decoding.
|
|
273
|
+
#time(d) {
|
|
274
|
+
const b = d.b
|
|
275
|
+
const s = d.p
|
|
276
|
+
const ls = this.lastTimeAt
|
|
277
|
+
const ln = this.lastTimeLen
|
|
278
|
+
if (ln > 0 && s + ln <= b.length) {
|
|
279
|
+
let k = 0
|
|
280
|
+
while (k < ln && b[s + k] === b[ls + k]) k++
|
|
281
|
+
if (k === ln) {
|
|
282
|
+
d.p = s + ln
|
|
283
|
+
return this.lastTime
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const v = d.varint()
|
|
287
|
+
let t = typeof v === 'bigint' ? v : BigInt(v)
|
|
288
|
+
const had = this.times.get(t)
|
|
289
|
+
if (had !== undefined) t = had
|
|
290
|
+
else if (this.times.size < MAX_INTERNED_TIMES) this.times.set(t, t)
|
|
291
|
+
if (b === this.idx) {
|
|
292
|
+
this.lastTimeAt = s
|
|
293
|
+
this.lastTimeLen = d.p - s
|
|
294
|
+
this.lastTime = t
|
|
295
|
+
}
|
|
296
|
+
return t
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
#entry(d) {
|
|
300
|
+
const pb = d.bytes()
|
|
301
|
+
const path = strOf(pb)
|
|
302
|
+
if (d.p >= d.b.length) d.bad()
|
|
303
|
+
const fl = d.b[d.p++]
|
|
304
|
+
const slash = pb.lastIndexOf(0x2f)
|
|
305
|
+
const e = {
|
|
306
|
+
path,
|
|
307
|
+
dir: slash < 0 ? '' : path.slice(0, slash),
|
|
308
|
+
// Its own string (not a slice that would keep path), shared by equal names.
|
|
309
|
+
name: this.#name(slash < 0 ? pb : pb.subarray(slash + 1)),
|
|
310
|
+
whiteout: (fl & FLAG_WHITEOUT) !== 0,
|
|
311
|
+
opaque: (fl & FLAG_OPAQUE) !== 0,
|
|
312
|
+
mode: 0, uid: 0, gid: 0, rdev: 0,
|
|
313
|
+
atime: 0n, mtime: 0n, ctime: 0n,
|
|
314
|
+
size: 0, off: 0, target: '', link: 0, xattrs: null,
|
|
315
|
+
}
|
|
316
|
+
if (e.whiteout) return e
|
|
317
|
+
e.mode = Number(d.uvarint()) >>> 0
|
|
318
|
+
e.uid = Number(d.uvarint()) >>> 0
|
|
319
|
+
e.gid = Number(d.uvarint()) >>> 0
|
|
320
|
+
e.rdev = d.uvarint()
|
|
321
|
+
e.atime = this.#time(d)
|
|
322
|
+
e.mtime = this.#time(d)
|
|
323
|
+
e.ctime = this.#time(d)
|
|
324
|
+
e.size = d.varint()
|
|
325
|
+
e.off = d.varint()
|
|
326
|
+
if (typeof e.size !== 'number' || typeof e.off !== 'number') d.bad()
|
|
327
|
+
e.target = d.str()
|
|
328
|
+
e.link = d.uvarint()
|
|
329
|
+
let nx = d.uvarint()
|
|
330
|
+
if (typeof nx !== 'number' || nx > d.b.length - d.p) d.bad()
|
|
331
|
+
for (; nx > 0; nx--) {
|
|
332
|
+
const k = d.str()
|
|
333
|
+
const v = d.bytes().slice()
|
|
334
|
+
if (!e.xattrs) e.xattrs = new Map()
|
|
335
|
+
e.xattrs.set(k, v)
|
|
336
|
+
}
|
|
337
|
+
return e
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
get length() {
|
|
341
|
+
return this.count
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Entry i, decoded. */
|
|
345
|
+
entry(i) {
|
|
346
|
+
const d = new Decoder(this.idx)
|
|
347
|
+
d.p = this.pos[i]
|
|
348
|
+
return this.#entry(d)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Every entry, decoded (for tests and tools: it holds them all). */
|
|
352
|
+
get entries() {
|
|
353
|
+
if (!this.all) {
|
|
354
|
+
this.all = new Array(this.count)
|
|
355
|
+
for (let i = 0; i < this.count; i++) this.all[i] = this.entry(i)
|
|
356
|
+
}
|
|
357
|
+
return this.all
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Compares entry i's path with byte string s: <0, 0, >0.
|
|
361
|
+
#cmp(i, s) {
|
|
362
|
+
const b = this.idx
|
|
363
|
+
let p = this.pos[i]
|
|
364
|
+
// The path's uvarint length (checked at open).
|
|
365
|
+
let len = 0
|
|
366
|
+
let mul = 1
|
|
367
|
+
for (;;) {
|
|
368
|
+
const c = b[p++]
|
|
369
|
+
len += (c & 0x7f) * mul
|
|
370
|
+
if (c < 0x80) break
|
|
371
|
+
mul *= 128
|
|
372
|
+
}
|
|
373
|
+
const m = Math.min(len, s.length)
|
|
374
|
+
for (let k = 0; k < m; k++) {
|
|
375
|
+
const x = b[p + k] - s.charCodeAt(k)
|
|
376
|
+
if (x !== 0) return x
|
|
377
|
+
}
|
|
378
|
+
return len - s.length
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** The index of the entry at path (whiteouts included), or -1. */
|
|
382
|
+
index(path) {
|
|
383
|
+
let lo = 0
|
|
384
|
+
let hi = this.count - 1
|
|
385
|
+
while (lo <= hi) {
|
|
386
|
+
const mid = (lo + hi) >>> 1
|
|
387
|
+
const c = this.#cmp(mid, path)
|
|
388
|
+
if (c === 0) return mid
|
|
389
|
+
if (c < 0) lo = mid + 1
|
|
390
|
+
else hi = mid - 1
|
|
391
|
+
}
|
|
392
|
+
return -1
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The entry at path (whiteouts included), or undefined. */
|
|
396
|
+
lookup(path) {
|
|
397
|
+
const i = this.index(path)
|
|
398
|
+
return i < 0 ? undefined : this.entry(i)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Directory path's entries' indexes (whiteouts included), in name order. */
|
|
402
|
+
children(path) {
|
|
403
|
+
let i
|
|
404
|
+
if (path === '') {
|
|
405
|
+
i = this.rootFirst
|
|
406
|
+
} else {
|
|
407
|
+
const d = this.index(path)
|
|
408
|
+
if (d < 0) return []
|
|
409
|
+
i = this.firstChild[d]
|
|
410
|
+
}
|
|
411
|
+
const out = []
|
|
412
|
+
for (; i !== NONE; i = this.nextSibling[i]) out.push(i)
|
|
413
|
+
return out
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Hard-link group link's size and its first entry's index (Go's LinkGroup). */
|
|
417
|
+
linkGroup(link) {
|
|
418
|
+
return this.links.get(link) || { n: 0, first: 0 }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Up to len bytes of regular file entry e's contents at off. */
|
|
422
|
+
read(e, off, len) {
|
|
423
|
+
if (off >= e.size || len <= 0) return new Uint8Array(0)
|
|
424
|
+
const n = Math.min(len, e.size - off)
|
|
425
|
+
return this.source.read(e.off + off, n)
|
|
426
|
+
}
|
|
427
|
+
}
|
package/src/fs/seed.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// What a 9P service's `files` option puts in its tree at boot (bootWmd channels.services[].files).
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One entry of a 9P service's files: its bytes or text, or {data, mode?, uid?, gid?} (P-B11: the agent
|
|
5
|
+
* binary, 0755), or {dir: true, mode?, uid?, gid?} (W65: a directory, such as the image's WORKDIR).
|
|
6
|
+
* Missing parents are made root's, 0755. An entry that names a path the layers have takes its contents
|
|
7
|
+
* (a file) and the mode and owner it gives; the rest of the layer's attributes stay.
|
|
8
|
+
*/
|
|
9
|
+
export function seedEntry(fs, path, f) {
|
|
10
|
+
const o = f && typeof f === 'object' && !ArrayBuffer.isView(f) && !(f instanceof ArrayBuffer) ? f : { data: f }
|
|
11
|
+
const attrs = { mode: o.mode, uid: o.uid, gid: o.gid }
|
|
12
|
+
let n
|
|
13
|
+
const slash = path.replace(/\/+$/, '').lastIndexOf('/')
|
|
14
|
+
if (slash > 0) fs.mkdirp(path.slice(0, slash))
|
|
15
|
+
if (o.dir) {
|
|
16
|
+
n = fs.mkdirp(path, o.mode ?? 0o755)
|
|
17
|
+
} else {
|
|
18
|
+
const bytes = typeof o.data === 'string' ? new TextEncoder().encode(o.data) : new Uint8Array(o.data)
|
|
19
|
+
n = fs.writeFile(path, bytes, { mode: o.mode ?? 0o644, uid: o.uid ?? 0, gid: o.gid ?? 0 })
|
|
20
|
+
}
|
|
21
|
+
if (attrs.mode !== undefined || attrs.uid !== undefined || attrs.gid !== undefined) fs.setattr(n, attrs)
|
|
22
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// P-B9: the file server's state as bytes, and back. What a checkpoint needs of the 9P
|
|
2
|
+
// service: the Overlay's tree (its upper, and every lower inode that has been read, with
|
|
3
|
+
// their identity: inode numbers, link counts, who is linked to whom) and the live
|
|
4
|
+
// connection's fid table (the guest kernel's v9fs holds those fids across a restore; a
|
|
5
|
+
// new server with none would answer every request on an open file with EBADF).
|
|
6
|
+
//
|
|
7
|
+
// The lower layers are not written: they are the image's packs, which the restoring page
|
|
8
|
+
// gets the way the first boot did. Inodes and Contents that read from a pack are written
|
|
9
|
+
// with a token for the pack (its index in the overlay's layer list).
|
|
10
|
+
//
|
|
11
|
+
// The tree goes through graph.js as one graph, so an inode shared by a name and a fid stays
|
|
12
|
+
// one inode.
|
|
13
|
+
|
|
14
|
+
import { Content, DirInode, Inode, Overlay } from './overlay.js'
|
|
15
|
+
import { Fid } from './9p/server.js'
|
|
16
|
+
import { Pack } from './pack.js'
|
|
17
|
+
import { decodeGraph, encodeGraph } from './graph.js'
|
|
18
|
+
|
|
19
|
+
/** Overlay fields the constructor of the restoring process sets (live objects, config). */
|
|
20
|
+
const OVERLAY_SKIP = ['layers', 'now', 'onFull', 'maxUpperBytes']
|
|
21
|
+
|
|
22
|
+
function classesFor(target) {
|
|
23
|
+
return {
|
|
24
|
+
Overlay: { ctor: Overlay, make: () => target, skip: OVERLAY_SKIP },
|
|
25
|
+
Inode: { ctor: Inode, make: () => new Inode(0n, 0) },
|
|
26
|
+
DirInode: { ctor: DirInode, make: () => new DirInode(0n, 0) },
|
|
27
|
+
// Content has private methods: it must come from its constructor.
|
|
28
|
+
Content: { ctor: Content, make: () => new Content(null, null, 0) },
|
|
29
|
+
Fid: { ctor: Fid, make: () => new Fid(null, null, '', 0) },
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The state of overlay (and of the 9P server over it, or null) as graph segments.
|
|
35
|
+
* @returns {{segments: Uint8Array[], bytes: number, inodes: number, fids: number}}
|
|
36
|
+
*/
|
|
37
|
+
export function dumpService(overlay, server = null) {
|
|
38
|
+
const special = { pack: v => (v instanceof Pack ? overlay.layers.indexOf(v) : undefined) }
|
|
39
|
+
const root = { overlay, msize: server ? server.msize : 0, fids: server ? server.fids : new Map() }
|
|
40
|
+
const out = encodeGraph(root, { classes: classesFor(overlay), special })
|
|
41
|
+
return { ...out, fids: root.fids.size }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Replaces the state of `overlay` (built from the same layers by the restoring process) with
|
|
46
|
+
* the dumped one. Returns {msize, fids}: the server state to put on the connection's server.
|
|
47
|
+
*/
|
|
48
|
+
export function loadService(bytes, overlay) {
|
|
49
|
+
const special = {
|
|
50
|
+
revive: i => {
|
|
51
|
+
const p = overlay.layers[i]
|
|
52
|
+
if (!p) throw new Error(`service snapshot: it refers to layer ${i}, and this boot has ${overlay.layers.length}`)
|
|
53
|
+
return p
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
const root = decodeGraph(bytes, { classes: classesFor(overlay), special })
|
|
57
|
+
if (root.overlay !== overlay) throw new Error('service snapshot: the overlay is not the first object')
|
|
58
|
+
return { msize: root.msize, fids: root.fids }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Puts a loaded fid table on a NinePServer. */
|
|
62
|
+
export function adoptServer(server, state) {
|
|
63
|
+
server.msize = state.msize
|
|
64
|
+
server.fids = state.fids
|
|
65
|
+
}
|