@threadedstack/wmd-web 0.1.17 → 0.1.18
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 +44 -0
- package/RELEASE.json +39 -54
- package/guest/arm64/Image +2 -2
- package/guest/arm64/Image-4k +1 -1
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +4 -4
- package/relay/hosted.ts +10 -8
- package/relay/main.ts +2 -2
- package/relay/relay.ts +1 -0
- package/relay/token.ts +34 -5
- package/relay/wisp.ts +37 -5
- package/src/boot.js +25 -0
- package/src/build/builder.js +791 -0
- package/src/build/context.js +349 -0
- package/src/build/dockerfile.js +1055 -0
- package/src/build/dockerignore.js +270 -0
- package/src/build/fast.js +1187 -0
- package/src/build/inspect.js +78 -0
- package/src/build/pin.js +6 -0
- package/src/build/tar.js +304 -0
- package/src/host/ctl.js +17 -0
- package/src/host/disk-map.js +330 -0
- package/src/host/disk-worker.js +125 -0
- package/src/host/disk.js +219 -0
- package/src/host/wmd-imports.js +3 -3
- package/src/image-files.js +2 -1
- package/src/registry/hub.js +113 -44
- package/src/registry/load-tar.js +473 -0
- package/src/registry/pull.js +5 -3
- package/src/worker.js +2 -0
- package/wmdpack.wasm +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Reading built layers back, for the fast-path parity gate (docs/plans/web-buildkit-impl.md 5.5): the entries of a
|
|
2
|
+
// layer tar (path, type, mode, owner, size, sha256, link target, xattrs, mtime) and the layers of a Docker tar
|
|
3
|
+
// (`docker save` layout or an OCI layout, layers plain or gzip).
|
|
4
|
+
import { scanTarBytes } from './tar.js'
|
|
5
|
+
|
|
6
|
+
const dec = new TextDecoder()
|
|
7
|
+
const TYPES = { 0: 'file', 7: 'file', 1: 'hardlink', 2: 'symlink', 3: 'char', 4: 'block', 5: 'dir', 6: 'fifo' }
|
|
8
|
+
|
|
9
|
+
const hex = (buf) => [...new Uint8Array(buf)].map((x) => x.toString(16).padStart(2, '0')).join('')
|
|
10
|
+
const sha256Hex = async (u8) => hex(await crypto.subtle.digest('SHA-256', u8))
|
|
11
|
+
|
|
12
|
+
/** gunzip when the bytes start with the gzip magic; else the bytes themselves. */
|
|
13
|
+
export async function maybeGunzip(u8) {
|
|
14
|
+
if (!(u8.length > 2 && u8[0] === 0x1f && u8[1] === 0x8b)) return u8
|
|
15
|
+
const s = new Blob([u8]).stream().pipeThrough(new DecompressionStream('gzip'))
|
|
16
|
+
return new Uint8Array(await new Response(s).arrayBuffer())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** './a/', '/a' and 'a' are the same path: no leading './' or '/', no trailing '/'. */
|
|
20
|
+
export function cleanEntryPath(p) {
|
|
21
|
+
return p.replace(/^(\.\/|\/)+/, '').replace(/\/+$/, '') || '.'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Every entry of a layer tar (plain or gzip) as {path, type, mode, uid, gid, size, sha256, linkname, xattrs, mtime}
|
|
26
|
+
* in archive order. `sha256` is the file's content for regular files, else null; `mode` the permission bits (with
|
|
27
|
+
* setuid/setgid/sticky).
|
|
28
|
+
*/
|
|
29
|
+
export async function layerEntries(bytes) {
|
|
30
|
+
const u8 = await maybeGunzip(bytes)
|
|
31
|
+
const out = []
|
|
32
|
+
for await (const e of scanTarBytes(u8)) {
|
|
33
|
+
const type = TYPES[e.type] || `type-${e.type}`
|
|
34
|
+
out.push({
|
|
35
|
+
path: cleanEntryPath(e.name),
|
|
36
|
+
type,
|
|
37
|
+
mode: e.mode & 0o7777,
|
|
38
|
+
uid: e.uid,
|
|
39
|
+
gid: e.gid,
|
|
40
|
+
size: type === 'file' ? e.size : 0,
|
|
41
|
+
sha256: type === 'file' ? await sha256Hex(u8.subarray(e.offset, e.offset + e.size)) : null,
|
|
42
|
+
linkname: e.linkname,
|
|
43
|
+
xattrs: e.xattrs || {},
|
|
44
|
+
mtime: e.mtime,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A Docker tar (what BuildKit's type=docker output writes: a `docker save` layout with manifest.json, or an OCI
|
|
52
|
+
* layout with index.json) as {config, layers}: `config` the image config JSON, `layers` the layer tars' bytes
|
|
53
|
+
* bottom first, as stored (plain or gzip).
|
|
54
|
+
*/
|
|
55
|
+
export async function dockerTarLayers(bytes) {
|
|
56
|
+
const files = new Map()
|
|
57
|
+
for await (const e of scanTarBytes(bytes)) {
|
|
58
|
+
if (e.type === '0' || e.type === '7') files.set(cleanEntryPath(e.name), bytes.subarray(e.offset, e.offset + e.size))
|
|
59
|
+
}
|
|
60
|
+
const get = (p) => {
|
|
61
|
+
const f = files.get(cleanEntryPath(p))
|
|
62
|
+
if (!f) throw new Error(`the Docker tar has no ${p}`)
|
|
63
|
+
return f
|
|
64
|
+
}
|
|
65
|
+
const json = (p) => JSON.parse(dec.decode(get(p)))
|
|
66
|
+
const blob = (digest) => get(`blobs/${digest.replace(':', '/')}`)
|
|
67
|
+
if (files.has('manifest.json')) {
|
|
68
|
+
const m = json('manifest.json')[0]
|
|
69
|
+
return { config: json(m.Config), layers: m.Layers.map((l) => get(l)) }
|
|
70
|
+
}
|
|
71
|
+
if (files.has('index.json')) {
|
|
72
|
+
let m = json('index.json').manifests[0]
|
|
73
|
+
let man = JSON.parse(dec.decode(blob(m.digest)))
|
|
74
|
+
if (man.manifests) man = JSON.parse(dec.decode(blob(man.manifests[0].digest)))
|
|
75
|
+
return { config: JSON.parse(dec.decode(blob(man.config.digest))), layers: man.layers.map((l) => blob(l.digest)) }
|
|
76
|
+
}
|
|
77
|
+
throw new Error('the tar has neither manifest.json nor index.json')
|
|
78
|
+
}
|
package/src/build/pin.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The wmd-web builder images (scripts/web-builder.sh writes this file): the sha256 and size of each
|
|
2
|
+
// `docker save` tar in web/dist/builder/. createBuilder refuses a tar that does not match.
|
|
3
|
+
export const BUILDER_PINS = {
|
|
4
|
+
arm64: { file: 'builder-arm64.tar', sha256: '905d0735a2fb7aa12f00bb21f50e82651d1f65db0aa030aa38ab4033047072ee', bytes: 107119104 },
|
|
5
|
+
amd64: { file: 'builder-amd64.tar', sha256: 'be2199d5eb05d1bcc2814999749de17d4e4ce206726f31fe8a495cf602fd3ccd', bytes: 113018368 },
|
|
6
|
+
}
|
package/src/build/tar.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// A small deterministic tar writer and header scanner (docs/plans/web-buildkit-impl.md 5.6).
|
|
2
|
+
// Output is ustar; a pax extended header (typeflag 'x') carries what ustar cannot hold: names over the
|
|
3
|
+
// name+prefix limits, non-ASCII names, link targets over 100 bytes, sizes of 8 GiB or more, and uid/gid/mtime
|
|
4
|
+
// that overflow their octal fields. Nothing reads the host clock: mtime is what the caller gives, else 0.
|
|
5
|
+
|
|
6
|
+
const enc = new TextEncoder()
|
|
7
|
+
const dec = new TextDecoder()
|
|
8
|
+
const BLOCK = 512
|
|
9
|
+
|
|
10
|
+
/** TextDecoder.decode that also takes a view on a SharedArrayBuffer (wasm memory), which browsers' decoders refuse. */
|
|
11
|
+
export function decodeText(u8, d = dec) {
|
|
12
|
+
return typeof SharedArrayBuffer !== 'undefined' && u8.buffer instanceof SharedArrayBuffer ? d.decode(u8.slice()) : d.decode(u8)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Throws on a path that must not enter an archive: absolute, '..', NUL, empty components. */
|
|
16
|
+
export function checkTarPath(path) {
|
|
17
|
+
if (path === './') return // the archive's root folder entry
|
|
18
|
+
if (typeof path !== 'string' || path === '') throw new Error('tar: empty path')
|
|
19
|
+
if (path.includes('\0')) throw new Error(`tar: NUL in path ${JSON.stringify(path)}`)
|
|
20
|
+
if (path.startsWith('/')) throw new Error(`tar: absolute path ${path}`)
|
|
21
|
+
const parts = path.split('/')
|
|
22
|
+
if (parts[parts.length - 1] === '') parts.pop() // one trailing '/' is a directory spelling
|
|
23
|
+
for (const p of parts) {
|
|
24
|
+
if (p === '' || p === '.' || p === '..') throw new Error(`tar: unsafe path ${path}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isAscii(s) {
|
|
29
|
+
for (let i = 0; i < s.length; i++) if (s.charCodeAt(i) > 0x7f) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toSeconds(t) {
|
|
34
|
+
if (t === undefined || t === null) return 0
|
|
35
|
+
if (t instanceof Date) return Math.floor(t.getTime() / 1000)
|
|
36
|
+
return Math.max(0, Math.floor(Number(t) || 0))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function octal(buf, off, len, value) {
|
|
40
|
+
const s = value.toString(8).padStart(len - 1, '0')
|
|
41
|
+
for (let i = 0; i < len - 1; i++) buf[off + i] = s.charCodeAt(i)
|
|
42
|
+
buf[off + len - 1] = 0
|
|
43
|
+
}
|
|
44
|
+
const octMax = (len) => 8 ** (len - 1) - 1
|
|
45
|
+
|
|
46
|
+
function putStr(buf, off, str) {
|
|
47
|
+
for (let i = 0; i < str.length; i++) buf[off + i] = str.charCodeAt(i)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One pax record: "<len> <key>=<value>\n" where len counts itself. */
|
|
51
|
+
function paxRecord(key, value) {
|
|
52
|
+
const body = enc.encode(` ${key}=${value}\n`)
|
|
53
|
+
let n = body.length + 1
|
|
54
|
+
while (String(n).length + body.length !== n) n = String(n).length + body.length
|
|
55
|
+
const head = enc.encode(String(n))
|
|
56
|
+
const out = new Uint8Array(n)
|
|
57
|
+
out.set(head, 0)
|
|
58
|
+
out.set(body, head.length)
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function concat(parts, total) {
|
|
63
|
+
const out = new Uint8Array(total ?? parts.reduce((a, p) => a + p.length, 0))
|
|
64
|
+
let o = 0
|
|
65
|
+
for (const p of parts) {
|
|
66
|
+
out.set(p, o)
|
|
67
|
+
o += p.length
|
|
68
|
+
}
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function rawHeader(name, prefix, typeflag, size, mode, mtime, uid, gid, linkname) {
|
|
73
|
+
const h = new Uint8Array(BLOCK)
|
|
74
|
+
putStr(h, 0, name)
|
|
75
|
+
octal(h, 100, 8, mode)
|
|
76
|
+
octal(h, 108, 8, uid)
|
|
77
|
+
octal(h, 116, 8, gid)
|
|
78
|
+
octal(h, 124, 12, size)
|
|
79
|
+
octal(h, 136, 12, mtime)
|
|
80
|
+
h.fill(0x20, 148, 156)
|
|
81
|
+
h[156] = typeflag.charCodeAt(0)
|
|
82
|
+
putStr(h, 157, linkname)
|
|
83
|
+
putStr(h, 257, 'ustar')
|
|
84
|
+
h[262] = 0
|
|
85
|
+
h[263] = 0x30
|
|
86
|
+
h[264] = 0x30
|
|
87
|
+
putStr(h, 345, prefix)
|
|
88
|
+
let sum = 0
|
|
89
|
+
for (let i = 0; i < BLOCK; i++) sum += h[i]
|
|
90
|
+
const cs = sum.toString(8).padStart(6, '0')
|
|
91
|
+
putStr(h, 148, cs)
|
|
92
|
+
h[154] = 0
|
|
93
|
+
h[155] = 0x20
|
|
94
|
+
return h
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The header block(s) of one entry: a pax header first when needed, then the ustar header.
|
|
99
|
+
* Exported so the over-8-GiB size path can be tested without 8 GiB of data.
|
|
100
|
+
* @returns {Uint8Array}
|
|
101
|
+
*/
|
|
102
|
+
export function tarEntryHeader(path, typeflag, size, { mode, mtime, uid = 0, gid = 0, linkname = '' } = {}) {
|
|
103
|
+
checkTarPath(path)
|
|
104
|
+
const isDir = typeflag === '5'
|
|
105
|
+
const full = isDir && !path.endsWith('/') ? `${path}/` : path
|
|
106
|
+
const pax = []
|
|
107
|
+
let name = full
|
|
108
|
+
let prefix = ''
|
|
109
|
+
const nameBytes = enc.encode(full)
|
|
110
|
+
if (isAscii(full) && nameBytes.length <= 100) {
|
|
111
|
+
// plain
|
|
112
|
+
} else if (isAscii(full) && nameBytes.length <= 255 + 1 + 100) {
|
|
113
|
+
// ustar prefix/name split at a '/'
|
|
114
|
+
let at = -1
|
|
115
|
+
for (let i = full.length - 2; i > 0; i--) {
|
|
116
|
+
if (full[i] !== '/') continue
|
|
117
|
+
if (full.length - i - 1 <= 100 && i <= 155) {
|
|
118
|
+
at = i
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (at > 0) {
|
|
123
|
+
prefix = full.slice(0, at)
|
|
124
|
+
name = full.slice(at + 1)
|
|
125
|
+
} else pax.push(['path', full])
|
|
126
|
+
} else pax.push(['path', full])
|
|
127
|
+
if (pax.length) {
|
|
128
|
+
// ustar fallback name: ASCII-only tail, never empty
|
|
129
|
+
name = full.replace(/[^\x20-\x7e]/g, '_').slice(-100)
|
|
130
|
+
prefix = ''
|
|
131
|
+
}
|
|
132
|
+
let link = linkname
|
|
133
|
+
if (link && (!isAscii(link) || enc.encode(link).length > 100)) {
|
|
134
|
+
pax.push(['linkpath', link])
|
|
135
|
+
link = link.replace(/[^\x20-\x7e]/g, '_').slice(0, 100)
|
|
136
|
+
}
|
|
137
|
+
const m = (mode ?? (isDir ? 0o755 : 0o644)) & 0o7777
|
|
138
|
+
let t = toSeconds(mtime)
|
|
139
|
+
if (t > octMax(12)) {
|
|
140
|
+
pax.push(['mtime', String(t)])
|
|
141
|
+
t = octMax(12)
|
|
142
|
+
}
|
|
143
|
+
let u = uid
|
|
144
|
+
if (u > octMax(8)) {
|
|
145
|
+
pax.push(['uid', String(u)])
|
|
146
|
+
u = octMax(8)
|
|
147
|
+
}
|
|
148
|
+
let g = gid
|
|
149
|
+
if (g > octMax(8)) {
|
|
150
|
+
pax.push(['gid', String(g)])
|
|
151
|
+
g = octMax(8)
|
|
152
|
+
}
|
|
153
|
+
let sz = size
|
|
154
|
+
if (sz > octMax(12)) {
|
|
155
|
+
pax.push(['size', String(sz)])
|
|
156
|
+
sz = octMax(12)
|
|
157
|
+
}
|
|
158
|
+
const main = rawHeader(name, prefix, typeflag, sz, m, t, u, g, link)
|
|
159
|
+
if (!pax.length) return main
|
|
160
|
+
const recs = pax.map(([k, v]) => paxRecord(k, v))
|
|
161
|
+
const dataLen = recs.reduce((a, r) => a + r.length, 0)
|
|
162
|
+
const padded = Math.ceil(dataLen / BLOCK) * BLOCK
|
|
163
|
+
const tail = (full.replace(/\/$/, '').split('/').pop() || 'x').replace(/[^\x20-\x7e]/g, '_').slice(-60)
|
|
164
|
+
const ph = rawHeader(`PaxHeaders.0/${tail}`, '', 'x', dataLen, 0o644, 0, 0, 0, '')
|
|
165
|
+
const body = new Uint8Array(padded)
|
|
166
|
+
body.set(concat(recs, dataLen), 0)
|
|
167
|
+
return concat([ph, body, main])
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A tar writer. Paths are relative, '/'-separated, without '..' or a leading '/'. */
|
|
171
|
+
export function tarWriter() {
|
|
172
|
+
const chunks = []
|
|
173
|
+
let total = 0
|
|
174
|
+
let done = false
|
|
175
|
+
const push = (c) => {
|
|
176
|
+
if (done) throw new Error('tar: writer already finished')
|
|
177
|
+
chunks.push(c)
|
|
178
|
+
total += c.length
|
|
179
|
+
}
|
|
180
|
+
const pad = (n) => (BLOCK - (n % BLOCK)) % BLOCK
|
|
181
|
+
return {
|
|
182
|
+
file(path, bytes, { mode, mtime, uid, gid } = {}) {
|
|
183
|
+
const data = typeof bytes === 'string' ? enc.encode(bytes) : bytes
|
|
184
|
+
push(tarEntryHeader(path, '0', data.length, { mode, mtime, uid, gid }))
|
|
185
|
+
if (data.length) {
|
|
186
|
+
push(data)
|
|
187
|
+
const p = pad(data.length)
|
|
188
|
+
if (p) push(new Uint8Array(p))
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
dir(path, { mode, mtime, uid, gid } = {}) {
|
|
192
|
+
push(tarEntryHeader(path, '5', 0, { mode, mtime, uid, gid }))
|
|
193
|
+
},
|
|
194
|
+
symlink(path, target, { mtime, uid, gid } = {}) {
|
|
195
|
+
if (!target) throw new Error('tar: empty symlink target')
|
|
196
|
+
push(tarEntryHeader(path, '2', 0, { mode: 0o777, mtime, uid, gid, linkname: target }))
|
|
197
|
+
},
|
|
198
|
+
/** The archive so far, ended with two zero blocks. */
|
|
199
|
+
finish() {
|
|
200
|
+
if (done) throw new Error('tar: writer already finished')
|
|
201
|
+
push(new Uint8Array(BLOCK * 2))
|
|
202
|
+
done = true
|
|
203
|
+
return concat(chunks, total)
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseNum(buf, off, len) {
|
|
209
|
+
if (buf[off] & 0x80) {
|
|
210
|
+
// GNU base-256
|
|
211
|
+
let v = buf[off] & 0x7f
|
|
212
|
+
for (let i = 1; i < len; i++) v = v * 256 + buf[off + i]
|
|
213
|
+
return v
|
|
214
|
+
}
|
|
215
|
+
let s = ''
|
|
216
|
+
for (let i = 0; i < len && buf[off + i] !== 0 && buf[off + i] !== 0x20; i++) s += String.fromCharCode(buf[off + i])
|
|
217
|
+
return s ? parseInt(s, 8) : 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function cstr(buf, off, len) {
|
|
221
|
+
let e = off
|
|
222
|
+
while (e < off + len && buf[e] !== 0) e++
|
|
223
|
+
return decodeText(buf.subarray(off, e))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parsePax(data) {
|
|
227
|
+
const out = {}
|
|
228
|
+
let p = 0
|
|
229
|
+
while (p < data.length) {
|
|
230
|
+
let sp = p
|
|
231
|
+
while (sp < data.length && data[sp] !== 0x20) sp++
|
|
232
|
+
const n = parseInt(decodeText(data.subarray(p, sp)), 10)
|
|
233
|
+
if (!(n > 0) || p + n > data.length) break
|
|
234
|
+
const rec = decodeText(data.subarray(sp + 1, p + n - 1))
|
|
235
|
+
const eq = rec.indexOf('=')
|
|
236
|
+
if (eq > 0) out[rec.slice(0, eq)] = rec.slice(eq + 1)
|
|
237
|
+
p += n
|
|
238
|
+
}
|
|
239
|
+
return out
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Walks a tar's headers without reading file data. `read(offset, length)` returns a Uint8Array (sync or
|
|
244
|
+
* promise) so a Blob can be walked by slices. Yields real entries only (pax 'x'/'g' and GNU 'L'/'K' are folded
|
|
245
|
+
* into the entry they describe): {name, type, size, mode, mtime, uid, gid, linkname, xattrs, offset} where `offset` is
|
|
246
|
+
* the start of the entry's data. `type` is the typeflag character ('0' for the old NUL flag).
|
|
247
|
+
*/
|
|
248
|
+
export async function* scanTar(read, totalLength) {
|
|
249
|
+
let off = 0
|
|
250
|
+
let pax = null
|
|
251
|
+
let gnuName = null
|
|
252
|
+
let gnuLink = null
|
|
253
|
+
const need = async (o, n) => {
|
|
254
|
+
const b = await read(o, n)
|
|
255
|
+
if (b.length < n) throw new Error('tar: truncated archive')
|
|
256
|
+
return b
|
|
257
|
+
}
|
|
258
|
+
while (off + BLOCK <= totalLength) {
|
|
259
|
+
const h = await need(off, BLOCK)
|
|
260
|
+
if (h.every((b) => b === 0)) return
|
|
261
|
+
let sum = 0
|
|
262
|
+
for (let i = 0; i < BLOCK; i++) sum += i >= 148 && i < 156 ? 0x20 : h[i]
|
|
263
|
+
if (sum !== parseNum(h, 148, 8)) throw new Error(`tar: bad header checksum at ${off}`)
|
|
264
|
+
const type = h[156] === 0 ? '0' : String.fromCharCode(h[156])
|
|
265
|
+
let size = parseNum(h, 124, 12)
|
|
266
|
+
off += BLOCK
|
|
267
|
+
if (type === 'x' || type === 'g' || type === 'L' || type === 'K') {
|
|
268
|
+
const data = await need(off, size)
|
|
269
|
+
off += Math.ceil(size / BLOCK) * BLOCK
|
|
270
|
+
if (type === 'x') pax = { ...(pax || {}), ...parsePax(data) }
|
|
271
|
+
else if (type === 'L') gnuName = cstr(data, 0, data.length)
|
|
272
|
+
else if (type === 'K') gnuLink = cstr(data, 0, data.length)
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
const prefix = cstr(h, 345, 155)
|
|
276
|
+
let name = cstr(h, 0, 100)
|
|
277
|
+
if (prefix) name = `${prefix}/${name}`
|
|
278
|
+
let linkname = cstr(h, 157, 100)
|
|
279
|
+
let mtime = parseNum(h, 136, 12)
|
|
280
|
+
let uid = parseNum(h, 108, 8)
|
|
281
|
+
let gid = parseNum(h, 116, 8)
|
|
282
|
+
if (gnuName !== null) name = gnuName
|
|
283
|
+
if (gnuLink !== null) linkname = gnuLink
|
|
284
|
+
if (pax) {
|
|
285
|
+
if (pax.path !== undefined) name = pax.path
|
|
286
|
+
if (pax.linkpath !== undefined) linkname = pax.linkpath
|
|
287
|
+
if (pax.size !== undefined) size = Number(pax.size)
|
|
288
|
+
if (pax.mtime !== undefined) mtime = Math.floor(Number(pax.mtime))
|
|
289
|
+
if (pax.uid !== undefined) uid = Number(pax.uid)
|
|
290
|
+
if (pax.gid !== undefined) gid = Number(pax.gid)
|
|
291
|
+
}
|
|
292
|
+
const xattrs = {}
|
|
293
|
+
if (pax) for (const [k, v] of Object.entries(pax)) if (k.startsWith('SCHILY.xattr.')) xattrs[k.slice(13)] = v
|
|
294
|
+
pax = gnuName = gnuLink = null
|
|
295
|
+
const hasData = type === '0' || type === '7' || (type !== '1' && type !== '2' && type !== '5' && size > 0)
|
|
296
|
+
yield { name, type, size, mode: parseNum(h, 100, 8), mtime, uid, gid, linkname, xattrs, offset: off }
|
|
297
|
+
if (hasData) off += Math.ceil(size / BLOCK) * BLOCK
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** scanTar over a Uint8Array. */
|
|
302
|
+
export function scanTarBytes(u8) {
|
|
303
|
+
return scanTar((o, n) => u8.subarray(o, o + n), u8.length)
|
|
304
|
+
}
|
package/src/host/ctl.js
CHANGED
|
@@ -719,6 +719,14 @@ export const DEFAULT_CAPS = [
|
|
|
719
719
|
'CAP_CHOWN', 'CAP_DAC_OVERRIDE', 'CAP_FSETID', 'CAP_FOWNER', 'CAP_MKNOD', 'CAP_NET_RAW', 'CAP_SETGID',
|
|
720
720
|
'CAP_SETUID', 'CAP_SETFCAP', 'CAP_SETPCAP', 'CAP_NET_BIND_SERVICE', 'CAP_SYS_CHROOT', 'CAP_KILL', 'CAP_AUDIT_WRITE',
|
|
721
721
|
]
|
|
722
|
+
/** Every capability, as internal/engine/ocispec.go allCaps (Linux 6.1: 0..40); what --privileged grants. */
|
|
723
|
+
export const ALL_CAPS = [
|
|
724
|
+
'CHOWN', 'DAC_OVERRIDE', 'DAC_READ_SEARCH', 'FOWNER', 'FSETID', 'KILL', 'SETGID', 'SETUID', 'SETPCAP', 'LINUX_IMMUTABLE',
|
|
725
|
+
'NET_BIND_SERVICE', 'NET_BROADCAST', 'NET_ADMIN', 'NET_RAW', 'IPC_LOCK', 'IPC_OWNER', 'SYS_MODULE', 'SYS_RAWIO', 'SYS_CHROOT',
|
|
726
|
+
'SYS_PTRACE', 'SYS_PACCT', 'SYS_ADMIN', 'SYS_BOOT', 'SYS_NICE', 'SYS_RESOURCE', 'SYS_TIME', 'SYS_TTY_CONFIG', 'MKNOD', 'LEASE',
|
|
727
|
+
'AUDIT_WRITE', 'AUDIT_CONTROL', 'SETFCAP', 'MAC_OVERRIDE', 'MAC_ADMIN', 'SYSLOG', 'WAKE_ALARM', 'BLOCK_SUSPEND', 'AUDIT_READ',
|
|
728
|
+
'PERFMON', 'BPF', 'CHECKPOINT_RESTORE',
|
|
729
|
+
].map(c => `CAP_${c}`)
|
|
722
730
|
const MASKED = ['/proc/acpi', '/proc/asound', '/proc/kcore', '/proc/keys', '/proc/latency_stats', '/proc/timer_list', '/proc/timer_stats', '/proc/sched_debug', '/proc/scsi', '/sys/firmware']
|
|
723
731
|
const READONLY = ['/proc/bus', '/proc/fs', '/proc/irq', '/proc/sys', '/proc/sysrq-trigger']
|
|
724
732
|
const ETC_FILES = ['hosts', 'resolv.conf', 'hostname']
|
|
@@ -751,6 +759,8 @@ function defaultMounts() {
|
|
|
751
759
|
* @param {{destination:string, files:Record<string,{data:Uint8Array|string, mode?:number}|string>, file?:string, readOnly?:boolean}[]} [o.mounts]
|
|
752
760
|
* each mount is a share vol<i> with these files; `file` binds only that file of the share
|
|
753
761
|
* @param {string} [o.arch] for imageconfig.json (default arm64)
|
|
762
|
+
* @param {boolean} [o.privileged] docker run --privileged as dockerd writes it: all capabilities, no masked or
|
|
763
|
+
* read-only paths, /sys read-write, every device allowed (default false; only builder machines use it)
|
|
754
764
|
* @param {boolean} [o.init] docker run --init: tini at /sbin/docker-init runs the command
|
|
755
765
|
* @param {boolean} [o.keepRunning] wmd's keep-running init at /sbin/docker-init: the container stays up
|
|
756
766
|
* after its command ends; args may be [] (nothing runs until ControlClient.startCommand). Send the
|
|
@@ -785,6 +795,13 @@ export function containerBundle(o) {
|
|
|
785
795
|
resources: { devices: [{ allow: false, access: 'rwm' }] },
|
|
786
796
|
},
|
|
787
797
|
}
|
|
798
|
+
if (o.privileged) {
|
|
799
|
+
spec.process.capabilities = { bounding: ALL_CAPS, effective: ALL_CAPS, inheritable: ALL_CAPS, permitted: ALL_CAPS }
|
|
800
|
+
spec.linux.maskedPaths = []
|
|
801
|
+
spec.linux.readonlyPaths = []
|
|
802
|
+
spec.linux.resources = { devices: [{ allow: true, access: 'rwm' }] }
|
|
803
|
+
for (const m of spec.mounts) if (m.destination === '/sys') m.options = m.options.filter(x => x !== 'ro')
|
|
804
|
+
}
|
|
788
805
|
if (o.init || o.keepRunning) {
|
|
789
806
|
// as dockerd binds its docker-init (internal/engine/ocispec.go): the guest's tini, or its own init for keep-running
|
|
790
807
|
spec.mounts.push({ destination: DOCKER_INIT, type: 'bind', source: o.keepRunning ? '/sbin/init' : '/sbin/tini', options: ['bind', 'ro'] })
|