@threadedstack/wmd-web 0.1.17 → 0.1.19

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.
@@ -0,0 +1,473 @@
1
+ // An image tar -> packs, in the page: the other half of pull.js. It reads what `buildctl build --output
2
+ // type=oci` or `type=docker` writes (an OCI image layout: oci-layout, index.json, blobs/sha256/..., and for
3
+ // type=docker also manifest.json) or a `docker save` tar (manifest.json, <hex>.json, layer tars), and returns
4
+ // what pullImage returns: { index, packs } with an IMAGE_SCHEMA index (layers top first) and the packs in that
5
+ // order, converted with wmdpack.wasm exactly as pull.js converts a downloaded layer. Also `name`: the
6
+ // io.containerd.image.name annotation of the index, or null.
7
+ //
8
+ // const { index, packs, name } = await loadImageTar(bytes, { converter: wmdpackWasmUrl, platform: 'linux/arm64' })
9
+ //
10
+ // The tar is read in place: a Uint8Array is indexed (entry offsets) and each blob is a view of it (the one
11
+ // copy is the layer handed to the converter Worker); a Blob is read one entry at a time, so the whole tar is
12
+ // never in memory at once.
13
+
14
+ import { createConverter } from './converter.js'
15
+ import { RegistryError, sha256 } from './hub.js'
16
+ import { CONVERTER } from './pull.js'
17
+ import { isAttestation, isIndexType, isLayerType, isManifestType, isZstdLayer, MAX_LAYERS, pickPlatform } from './oci.js'
18
+ import { IMAGE_SCHEMA, isDigest, parsePlatform, platformMatches, platformString } from './ref.js'
19
+
20
+ const MAX_JSON = 4 << 20
21
+ const NAME_ANNOTATION = 'io.containerd.image.name'
22
+ const dec = new TextDecoder()
23
+ const unshared = b => (typeof SharedArrayBuffer !== 'undefined' && b.buffer instanceof SharedArrayBuffer ? b.slice() : b)
24
+ const fail = msg => {
25
+ throw new RegistryError(`image tar: ${msg}`, 400)
26
+ }
27
+
28
+
29
+ const K256 = Uint32Array.from(
30
+ '428a2f98 71374491 b5c0fbcf e9b5dba5 3956c25b 59f111f1 923f82a4 ab1c5ed5 d807aa98 12835b01 243185be 550c7dc3 72be5d74 80deb1fe 9bdc06a7 c19bf174 e49b69c1 efbe4786 0fc19dc6 240ca1cc 2de92c6f 4a7484aa 5cb0a9dc 76f988da 983e5152 a831c66d b00327c8 bf597fc7 c6e00bf3 d5a79147 06ca6351 14292967 27b70a85 2e1b2138 4d2c6dfc 53380d13 650a7354 766a0abb 81c2c92e 92722c85 a2bfe8a1 a81a664b c24b8b70 c76c51a3 d192e819 d6990624 f40e3585 106aa070 19a4c116 1e376c08 2748774c 34b0bcb5 391c0cb3 4ed8aa4a 5b9cca4f 682e6ff3 748f82ee 78a5636f 84c87814 8cc70208 90befffa a4506ceb bef9a3f7 c67178f2'
31
+ .split(' ')
32
+ .map(x => parseInt(x, 16)),
33
+ )
34
+
35
+ /** An incremental SHA-256 (WebCrypto has none): update(chunk) as the bytes arrive, digest() -> 'sha256:<hex>'. */
36
+ class Sha256 {
37
+ constructor() {
38
+ this.h = Uint32Array.of(0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19)
39
+ this.buf = new Uint8Array(64)
40
+ this.n = 0 // bytes in buf
41
+ this.len = 0
42
+ this.w = new Uint32Array(64)
43
+ }
44
+
45
+ block(b, o) {
46
+ const { h, w } = this
47
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n))
48
+ for (let i = 0; i < 16; i++) w[i] = ((b[o + i * 4] << 24) | (b[o + i * 4 + 1] << 16) | (b[o + i * 4 + 2] << 8) | b[o + i * 4 + 3]) | 0
49
+ for (let i = 16; i < 64; i++) {
50
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3)
51
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10)
52
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0
53
+ }
54
+ let [a, b2, c, d, e, f, g, hh] = h
55
+ for (let i = 0; i < 64; i++) {
56
+ const t1 = (hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + ((e & f) ^ (~e & g)) + K256[i] + w[i]) | 0
57
+ const t2 = ((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + ((a & b2) ^ (a & c) ^ (b2 & c))) | 0
58
+ hh = g
59
+ g = f
60
+ f = e
61
+ e = (d + t1) | 0
62
+ d = c
63
+ c = b2
64
+ b2 = a
65
+ a = (t1 + t2) | 0
66
+ }
67
+ h[0] += a
68
+ h[1] += b2
69
+ h[2] += c
70
+ h[3] += d
71
+ h[4] += e
72
+ h[5] += f
73
+ h[6] += g
74
+ h[7] += hh
75
+ }
76
+
77
+ update(u8) {
78
+ this.len += u8.length
79
+ let i = 0
80
+ if (this.n) {
81
+ const take = Math.min(64 - this.n, u8.length)
82
+ this.buf.set(u8.subarray(0, take), this.n)
83
+ this.n += take
84
+ i = take
85
+ if (this.n < 64) return
86
+ this.block(this.buf, 0)
87
+ this.n = 0
88
+ }
89
+ for (; i + 64 <= u8.length; i += 64) this.block(u8, i)
90
+ this.buf.set(u8.subarray(i), 0)
91
+ this.n = u8.length - i
92
+ }
93
+
94
+ digest() {
95
+ const bits = this.len * 8
96
+ const pad = new Uint8Array((this.n < 56 ? 64 : 128) - this.n)
97
+ pad[0] = 0x80
98
+ const dv = new DataView(pad.buffer)
99
+ dv.setUint32(pad.length - 8, Math.floor(bits / 2 ** 32))
100
+ dv.setUint32(pad.length - 4, bits >>> 0)
101
+ const keep = this.len
102
+ this.update(pad)
103
+ this.len = keep
104
+ return `sha256:${[...this.h].map(x => (x >>> 0).toString(16).padStart(8, '0')).join('')}`
105
+ }
106
+ }
107
+
108
+ /** sha256 of the gunzipped bytes, streamed (the decompressed layer is never held whole). */
109
+ async function gunzipDigest(bytes, what) {
110
+ const sha = new Sha256()
111
+ try {
112
+ const rd = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('gzip')).getReader()
113
+ for (;;) {
114
+ const { done, value } = await rd.read()
115
+ if (done) break
116
+ sha.update(value)
117
+ }
118
+ } catch (e) {
119
+ fail(`${what} is not valid gzip (${e.message})`)
120
+ }
121
+ return sha.digest()
122
+ }
123
+
124
+ /** A reader over the tar's bytes: read(off, len) -> Uint8Array (a view for bytes, a fresh copy for a Blob). */
125
+ function source(input) {
126
+ if (input instanceof Uint8Array) return { size: input.length, read: async (off, len) => input.subarray(off, off + len) }
127
+ if (input instanceof ArrayBuffer) return source(new Uint8Array(input))
128
+ if (typeof Blob !== 'undefined' && input instanceof Blob) {
129
+ return { size: input.size, read: async (off, len) => new Uint8Array(await input.slice(off, off + len).arrayBuffer()) }
130
+ }
131
+ throw new TypeError('loadImageTar: the tar as a Uint8Array, ArrayBuffer or Blob')
132
+ }
133
+
134
+ const field = (h, a, l) => dec.decode(h.subarray(a, a + l)).replace(/\0[\s\S]*$/, '')
135
+ function tarSize(h) {
136
+ if (h[124] & 0x80) {
137
+ // base-256 (GNU)
138
+ let n = 0
139
+ for (let i = 125; i < 136; i++) n = n * 256 + h[i]
140
+ return n
141
+ }
142
+ return parseInt(field(h, 124, 12).trim() || '0', 8)
143
+ }
144
+ const paxFields = text => {
145
+ const o = {}
146
+ for (const line of text.split('\n')) {
147
+ const sp = line.indexOf(' ')
148
+ const eq = line.indexOf('=', sp)
149
+ if (sp > 0 && eq > 0) o[line.slice(sp + 1, eq)] = line.slice(eq + 1)
150
+ }
151
+ return o
152
+ }
153
+
154
+ const isUstar = h => field(h, 257, 6) === 'ustar' && h[263] === 0x30 && h[264] === 0x30
155
+ function checksumOk(h) {
156
+ const want = parseInt(field(h, 148, 8).trim() || 'x', 8)
157
+ if (!Number.isInteger(want)) return false
158
+ let u = 0
159
+ let sg = 0
160
+ for (let i = 0; i < 512; i++) {
161
+ const c = i >= 148 && i < 156 ? 0x20 : h[i]
162
+ u += c
163
+ sg += c > 127 ? c - 256 : c
164
+ }
165
+ return want === u || want === sg
166
+ }
167
+
168
+ /**
169
+ * The files of the tar: Map name -> {off, size, type, link}. type '0' is a regular file, '1' a hard link and
170
+ * '2' a symlink (link is its target). Reads the 512-byte headers only (and the metadata records).
171
+ */
172
+ async function indexTar(src) {
173
+ const files = new Map()
174
+ let off = 0
175
+ let longName = null
176
+ let longLink = null
177
+ let pax = null
178
+ while (off + 512 <= src.size) {
179
+ const h = await src.read(off, 512)
180
+ if (h.length < 512 || h.every(x => x === 0)) break
181
+ if (!checksumOk(h)) fail(`bad tar header checksum at offset ${off}`)
182
+ let name = field(h, 0, 100)
183
+ const prefix = field(h, 345, 155)
184
+ if (prefix && isUstar(h)) name = `${prefix}/${name}`
185
+ let size = tarSize(h)
186
+ const type = String.fromCharCode(h[156] || 48)
187
+ const meta = 'LKxgDV'.includes(type)
188
+ if (type === 'x' || type === 'L' || type === 'K') {
189
+ // a record's own size is the header's
190
+ } else if (pax?.size !== undefined && !meta) {
191
+ size = Number(pax.size)
192
+ }
193
+ if (!Number.isSafeInteger(size) || size < 0) fail(`bad entry size for ${name}`)
194
+ const body = off + 512
195
+ if (body + size > src.size) fail(`the tar is cut short inside ${name}`)
196
+ off = body + Math.ceil(size / 512) * 512
197
+ if (meta) {
198
+ if (type === 'g' || type === 'D' || type === 'V') continue
199
+ if (size > 1 << 20) fail('an oversized name record')
200
+ const text = dec.decode(await src.read(body, size))
201
+ if (type === 'L') longName = text.replace(/\0[\s\S]*$/, '')
202
+ else if (type === 'K') longLink = text.replace(/\0[\s\S]*$/, '')
203
+ else pax = paxFields(text)
204
+ continue
205
+ }
206
+ let link = field(h, 157, 100)
207
+ if (pax?.path) name = pax.path
208
+ if (longName) name = longName
209
+ if (pax?.linkpath) link = pax.linkpath
210
+ if (longLink) link = longLink
211
+ longName = null
212
+ longLink = null
213
+ pax = null
214
+ name = name.replace(/^(\.\/)+/, '')
215
+ if (type === '0') files.set(name, { off: body, size, type })
216
+ else if (type === '1' || type === '2') files.set(name, { off: body, size: 0, type, link })
217
+ }
218
+ return files
219
+ }
220
+
221
+ const normPath = p => {
222
+ const out = []
223
+ for (const seg of p.split('/')) {
224
+ if (seg === '' || seg === '.') continue
225
+ if (seg === '..') out.pop()
226
+ else out.push(seg)
227
+ }
228
+ return out.join('/')
229
+ }
230
+ /** The regular file a name leads to, through symlinks and hard links (at most 16 hops), or undefined. */
231
+ function resolveEntry(files, name) {
232
+ let path = normPath(name)
233
+ for (let hops = 0; hops <= 16; hops++) {
234
+ const e = files.get(path)
235
+ if (!e) return undefined
236
+ if (e.type === '0') return e
237
+ // a symlink is relative to its folder (or absolute from the tar's root); a hard link names a path from the root
238
+ path = e.type === '2' && !e.link.startsWith('/') ? normPath(`${path.split('/').slice(0, -1).join('/')}/${e.link}`) : normPath(e.link)
239
+ }
240
+ return fail(`${name}: too many links`)
241
+ }
242
+
243
+ const isGzip = b => b.length > 2 && b[0] === 0x1f && b[1] === 0x8b
244
+ const isZstd = b => b.length >= 4 && b[0] === 0x28 && b[1] === 0xb5 && b[2] === 0x2f && b[3] === 0xfd
245
+
246
+ /**
247
+ * @param {Uint8Array|ArrayBuffer|Blob} input the image tar
248
+ * @param {object} o
249
+ * @param {string|URL|Uint8Array|WebAssembly.Module|{convert: Function, add: Function}} o.converter wmdpack.wasm, or a converter from createConverter
250
+ * @param {string|string[]} [o.platform] default linux/arm64
251
+ * @returns {Promise<{index: object, packs: Uint8Array[], name: string|null}>} packs top first, as index.layers
252
+ */
253
+ export async function loadImageTar(input, o = {}) {
254
+ const t0 = performance.now()
255
+ if (!o.converter) throw new TypeError('loadImageTar: pass converter: wmdpack.wasm (a URL, its bytes or a WebAssembly.Module)')
256
+ const wants = (Array.isArray(o.platform) ? o.platform : [o.platform || 'linux/arm64']).map(p => parsePlatform(p))
257
+ const wantsText = wants.map(platformString).join(' or ')
258
+ const src = source(input)
259
+ const magic = await src.read(0, 4)
260
+ if (isGzip(magic) || isZstd(magic)) fail(`the image tar is compressed (${isGzip(magic) ? 'gzip' : 'zstd'}); give the plain tar`)
261
+ const files = await indexTar(src)
262
+ const entry = path => {
263
+ const e = resolveEntry(files, path)
264
+ if (!e) fail(`it has no ${path}`)
265
+ return e
266
+ }
267
+ const readFile = async path => {
268
+ const e = entry(path)
269
+ return src.read(e.off, e.size)
270
+ }
271
+ const readJson = async (path, what) => {
272
+ const e = entry(path)
273
+ if (e.size > MAX_JSON) fail(`${what} is ${e.size} bytes (at most ${MAX_JSON})`)
274
+ try {
275
+ return JSON.parse(dec.decode(await src.read(e.off, e.size)))
276
+ } catch {
277
+ return fail(`${what} is not JSON`)
278
+ }
279
+ }
280
+ /** A blob by descriptor: size and sha256 checked. */
281
+ const blob = async (d, what) => {
282
+ if (!isDigest(d?.digest)) fail(`bad ${what} descriptor ${JSON.stringify(d).slice(0, 200)}`)
283
+ const path = `blobs/sha256/${d.digest.slice(7)}`
284
+ const e = resolveEntry(files, path)
285
+ if (!e) fail(`it has no blob ${d.digest} (${what})`)
286
+ if (Number.isSafeInteger(d.size) && d.size !== e.size) fail(`${what} ${d.digest.slice(0, 19)}: ${e.size} bytes, the descriptor says ${d.size}`)
287
+ const b = await src.read(e.off, e.size)
288
+ const got = await sha256(unshared(b))
289
+ if (got !== d.digest) fail(`${what} ${d.digest.slice(0, 19)} hashes to ${got.slice(0, 19)}`)
290
+ return b
291
+ }
292
+ const blobJson = async (d, what) => {
293
+ if (d.size > MAX_JSON) fail(`${what} is ${d.size} bytes (at most ${MAX_JSON})`)
294
+ const b = await blob(d, what)
295
+ try {
296
+ return JSON.parse(dec.decode(b))
297
+ } catch {
298
+ return fail(`${what} is not JSON`)
299
+ }
300
+ }
301
+
302
+ let name = null
303
+ let indexDigest = null
304
+ let manifestDigest = null
305
+ let cfg
306
+ let layers // [{digest, mediaType, size, get: () => Promise<Uint8Array>}] bottom first
307
+ let have = []
308
+
309
+ if (resolveEntry(files, 'index.json')) {
310
+ // an OCI layout (also what type=docker writes, beside manifest.json)
311
+ if (resolveEntry(files, 'oci-layout')) {
312
+ const l = await readJson('oci-layout', 'oci-layout')
313
+ if (l.imageLayoutVersion && !/^1\./.test(l.imageLayoutVersion)) fail(`unsupported image layout ${l.imageLayoutVersion}`)
314
+ }
315
+ const top = await readJson('index.json', 'index.json')
316
+ if (!Array.isArray(top.manifests) || top.manifests.length === 0) fail('index.json lists no manifests')
317
+ for (const m of top.manifests) name ??= m?.annotations?.[NAME_ANNOTATION] || null
318
+ let doc = top
319
+ let want = null
320
+ let man = null
321
+ for (let depth = 0; ; depth++) {
322
+ if (depth > 3) fail('indexes nested too deep')
323
+ const pick = pickPlatform(doc, want ? [want] : wants)
324
+ have = [...new Set([...have, ...pick.have || []])]
325
+ let desc = pick.desc
326
+ if (!desc) {
327
+ // BuildKit's index.json names its image manifest without a platform: take it, the config is checked below
328
+ desc = doc.manifests.find(m => m && isDigest(m.digest) && !m.platform && !isAttestation(m) && !isIndexType(m.mediaType || ''))
329
+ if (!desc) break
330
+ } else if (pick.platform) want = pick.platform
331
+ const d = desc
332
+ const sub = await blobJson(d, 'manifest')
333
+ if (isIndexType(d.mediaType || '') || (Array.isArray(sub.manifests) && !sub.layers)) {
334
+ indexDigest ??= d.digest
335
+ doc = sub
336
+ continue
337
+ }
338
+ man = { doc: sub, digest: d.digest, mediaType: d.mediaType || sub.mediaType || '' }
339
+ break
340
+ }
341
+ if (!man) fail(`no ${wantsText} image in it (it has: ${have.length ? have.join(', ') : 'no platforms'})`)
342
+ if (man.doc.schemaVersion === 1 || !(isManifestType(man.mediaType) || Array.isArray(man.doc.layers))) fail(`unsupported manifest type ${JSON.stringify(man.mediaType)}`)
343
+ manifestDigest = man.digest
344
+ if (!man.doc.config || !isDigest(man.doc.config.digest)) fail('the manifest has no config')
345
+ cfg = await blobJson(man.doc.config, 'config')
346
+ const ls = man.doc.layers
347
+ if (!Array.isArray(ls) || ls.length === 0) fail('the manifest has no layers')
348
+ if (ls.length > MAX_LAYERS) fail(`${ls.length} layers (at most ${MAX_LAYERS})`)
349
+ for (const l of ls) {
350
+ if (!isDigest(l?.digest) || !Number.isSafeInteger(l.size) || l.size < 0) fail(`bad layer descriptor ${JSON.stringify(l).slice(0, 200)}`)
351
+ if (isZstdLayer(l.mediaType)) fail(`layer ${l.digest.slice(0, 19)} is zstd; the page's converter takes tar and gzip layers only`)
352
+ if (!isLayerType(l.mediaType || '', { zstd: false })) fail(`layer ${l.digest.slice(0, 19)} has media type ${l.mediaType} (foreign or not a filesystem tar)`)
353
+ }
354
+ layers = ls.map(l => ({ digest: l.digest, size: l.size, get: () => blob(l, 'layer') }))
355
+ } else if (resolveEntry(files, 'manifest.json')) {
356
+ // docker save
357
+ const list = await readJson('manifest.json', 'manifest.json')
358
+ if (!Array.isArray(list) || list.length === 0) fail('manifest.json lists no images')
359
+ const picks = []
360
+ for (const m of list) {
361
+ const cb = await readFile(String(m?.Config ?? ''))
362
+ let c
363
+ try {
364
+ c = JSON.parse(dec.decode(cb))
365
+ } catch {
366
+ fail(`${m.Config} is not JSON`)
367
+ }
368
+ const p = c.os && c.architecture ? { os: c.os, architecture: c.architecture, variant: c.variant || '' } : null
369
+ picks.push({ m, c, cb, p })
370
+ if (p) have.push(platformString(p))
371
+ }
372
+ const hit = picks.find(x => x.p && wants.some(w => platformMatches(w, x.p)))
373
+ if (!hit) fail(`no ${wantsText} image in it (it has: ${have.length ? [...new Set(have)].join(', ') : 'no platforms'})`)
374
+ const { m, c, cb } = hit
375
+ const cm = /^(?:blobs\/sha256\/)?([0-9a-f]{64})(?:\.json)?$/.exec(m.Config)
376
+ if (cm && (await sha256(unshared(cb))) !== `sha256:${cm[1]}`) fail(`${m.Config} does not hash to its name`)
377
+ cfg = c
378
+ name = Array.isArray(m.RepoTags) && m.RepoTags.length ? m.RepoTags[0] : null
379
+ const paths = m.Layers
380
+ if (!Array.isArray(paths) || paths.length === 0) fail('the image has no layers')
381
+ if (paths.length > MAX_LAYERS) fail(`${paths.length} layers (at most ${MAX_LAYERS})`)
382
+ const diffIds = Array.isArray(c.rootfs?.diff_ids) ? c.rootfs.diff_ids : []
383
+ layers = paths.map((p, i) => ({
384
+ digest: null,
385
+ size: resolveEntry(files, p)?.size ?? fail(`it has no layer ${p}`),
386
+ get: async () => {
387
+ const b = await readFile(p)
388
+ if (isZstd(b)) fail(`layer ${p} is zstd; the page's converter takes tar and gzip layers only`)
389
+ const got = await sha256(unshared(b))
390
+ // the config's diff_id is the digest of the uncompressed tar (a gzip layer is gunzipped, streamed, to check it)
391
+ const diff = isGzip(b) ? await gunzipDigest(b, `layer ${p}`) : got
392
+ if (diffIds[i] && diffIds[i] !== diff) fail(`layer ${p} hashes to ${diff.slice(0, 19)}, the config says ${diffIds[i].slice(0, 19)}`)
393
+ return { b, digest: diffIds[i] || diff }
394
+ },
395
+ }))
396
+ } else {
397
+ fail('it is neither an OCI image layout (index.json) nor a docker save tar (manifest.json)')
398
+ }
399
+
400
+ // the platform is checked against the config too: an index entry can lie
401
+ const cfgPlat = cfg.os && cfg.architecture ? { os: cfg.os, architecture: cfg.architecture, variant: cfg.variant || '' } : null
402
+ const want = cfgPlat ? wants.find(w => platformMatches(w, cfgPlat)) : wants[0]
403
+ if (!want || (cfgPlat && !platformMatches(want, cfgPlat))) {
404
+ const had = [...new Set([...have, ...(cfgPlat ? [platformString(cfgPlat)] : [])])]
405
+ fail(`no ${wantsText} image in it (it has: ${had.length ? had.join(', ') : 'no platforms'})`)
406
+ }
407
+
408
+ // layers, bottom first, each converted over the packs below it
409
+ const ownConverter = !(typeof o.converter?.convert === 'function')
410
+ const conv = ownConverter ? await createConverter(o.converter) : o.converter
411
+ const out = []
412
+ let convertMs = 0
413
+ let fileBytes = 0
414
+ let wasmBytes = 0
415
+ let layerBytes = 0
416
+ try {
417
+ for (let i = 0; i < layers.length; i++) {
418
+ const l = layers[i]
419
+ const got = await l.get()
420
+ const b = got.b || got
421
+ if (isZstd(b)) fail(`layer ${l.digest ? l.digest.slice(0, 19) : i} is zstd; the page's converter takes tar and gzip layers only`)
422
+ layerBytes += b.length
423
+ const below = out.map(x => x.pack).reverse()
424
+ const r = await conv.convert(b, below)
425
+ convertMs += r.ms
426
+ fileBytes += r.fileBytes
427
+ wasmBytes = Math.max(wasmBytes, r.wasmBytes || 0)
428
+ out.push({ pack: r.sha, size: r.size, layer: l.digest || got.digest, bytes: r.bytes })
429
+ }
430
+ } finally {
431
+ if (ownConverter) conv.close()
432
+ }
433
+
434
+ const c = cfg.config || {}
435
+ const top = out.slice().reverse()
436
+ const index = {
437
+ schema: IMAGE_SCHEMA,
438
+ ref: name || '',
439
+ platform: platformString(want),
440
+ index: indexDigest,
441
+ manifest: manifestDigest,
442
+ os: cfg.os || null,
443
+ architecture: cfg.architecture || null,
444
+ config: {
445
+ Env: c.Env || null,
446
+ Cmd: c.Cmd || null,
447
+ Entrypoint: c.Entrypoint || null,
448
+ WorkingDir: c.WorkingDir || '',
449
+ User: c.User || '',
450
+ ExposedPorts: c.ExposedPorts || null,
451
+ Labels: c.Labels || null,
452
+ Volumes: c.Volumes || null,
453
+ StopSignal: c.StopSignal || '',
454
+ Healthcheck: c.Healthcheck || null,
455
+ },
456
+ layers: top.map(x => ({ pack: x.pack, size: x.size, layer: x.layer })),
457
+ converter: CONVERTER,
458
+ pulled: new Date().toISOString(),
459
+ stats: {
460
+ totalMs: Math.round(performance.now() - t0),
461
+ layerBytes,
462
+ downloadBytes: 0,
463
+ downloadMs: 0,
464
+ convertMs,
465
+ fileBytes,
466
+ wasmBytes,
467
+ packBytes: out.reduce((a, x) => a + x.size, 0),
468
+ cachedLayers: 0,
469
+ tarFiles: files.size,
470
+ },
471
+ }
472
+ return { index, packs: top.map(x => x.bytes), name, rawConfig: cfg }
473
+ }
@@ -66,7 +66,8 @@ async function fromStore(store, key) {
66
66
  * @param {{get: (key: string) => any, put: (key: string, bytes: Uint8Array) => any}} [o.store]
67
67
  * @param {number} [o.concurrency] downloads at once (default 3)
68
68
  * @param {string[]} [o.local] test registry host:port names allowed besides Docker Hub
69
- * @returns {Promise<{index: object, packs: Uint8Array[]}>} packs top first, as index.layers
69
+ * @param {string} [o.mirror] 'http(s)://host[:port]': a Docker Hub mirror tried first for docker.io references (its host is allowed because it is named here)
70
+ * @returns {Promise<{index: object, packs: Uint8Array[], rawConfig: object}>} packs top first, as index.layers
70
71
  */
71
72
  export async function pullImage(refStr, o = {}) {
72
73
  const t0 = performance.now()
@@ -74,7 +75,7 @@ export async function pullImage(refStr, o = {}) {
74
75
  const wants = (Array.isArray(o.platform) ? o.platform : o.platform ? [o.platform] : DEFAULT_PLATFORMS).map(p => parsePlatform(p))
75
76
  const ev = o.onProgress || (() => {})
76
77
  const sig = o.signal
77
- const hub = new HubRepo(ref, { fetch: o.fetch, local: o.local })
78
+ const hub = new HubRepo(ref, { fetch: o.fetch, local: o.local, mirror: o.mirror })
78
79
  if (!o.converter) throw new TypeError('pullImage: pass converter: wmdpack.wasm (a URL, its bytes or a WebAssembly.Module)')
79
80
 
80
81
  // 1. resolve
@@ -254,5 +255,6 @@ export async function pullImage(refStr, o = {}) {
254
255
  cachedLayers: out.filter(x => x.cached).length,
255
256
  },
256
257
  }
257
- return { index, packs: top.map(x => x.bytes) }
258
+ // rawConfig: the image config blob as parsed JSON (rootfs.diff_ids, history, config.OnBuild, config.Shell...): the build fast path reads it
259
+ return { index, packs: top.map(x => x.bytes), rawConfig: cfg }
258
260
  }
package/src/worker.js CHANGED
@@ -35,6 +35,7 @@ import { WasiExit, createWasi } from './host/wasi.js'
35
35
  import { createJitDoor, poolProbe, TABLE_MIN } from './host/jit.js'
36
36
  import { stdioWriter } from './host/stdio.js'
37
37
  import { createWmdImports } from './host/wmd-imports.js'
38
+ import { createDiskImports } from './host/disk.js'
38
39
  import { createRamImports } from './host/ram-mem.js'
39
40
  import { createChanImports } from './host/chan.js'
40
41
  import { createCkptImports } from './host/ckpt-point.js'
@@ -176,6 +177,7 @@ async function run(m) {
176
177
  chan,
177
178
  ckpt,
178
179
  ram: ramImports,
180
+ blk: m.blk ? createDiskImports({ memory, mailbox: m.blk }) : null, // a disk (host/disk.js)
179
181
  counters: ctl, // stub calls (chan_open, snapshot, jit_* without a door) are counted in ctl, read by the page
180
182
  })
181
183
 
package/wmdpack.wasm CHANGED
Binary file