| c26267b3 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
"use strict"; |
| 11 |
|
| 12 |
const E = { SUCCESS: 0, ACCES: 2, BADF: 8, EXIST: 20, INVAL: 28, ISDIR: 31, NOENT: 44, NOSYS: 52, NOTDIR: 54, NOTEMPTY: 55, NOTSUP: 58 }; |
| 13 |
const FT = { UNKNOWN: 0, CHAR: 2, DIR: 3, FILE: 4 }; |
| 14 |
const O = { CREAT: 1, DIRECTORY: 2, EXCL: 4, TRUNC: 8 }; |
| 15 |
const FD_APPEND = 1; |
| 16 |
const RIGHTS = 0x1fffffffn; |
| 17 |
|
| 18 |
class Exit { |
| 19 |
constructor(code) { this.code = code; } |
| 20 |
} |
| 21 |
|
| 22 |
|
| 23 |
|
| 24 |
|
| 25 |
|
| 26 |
|
| 27 |
async function unzip(buf) { |
| 28 |
const b = new DataView(buf); |
| 29 |
const bytes = new Uint8Array(buf); |
| 30 |
let end = -1; |
| 31 |
for (let i = buf.byteLength - 22; i >= Math.max(0, buf.byteLength - 65557); i--) { |
| 32 |
if (b.getUint32(i, true) === 0x06054b50) { end = i; break; } |
| 33 |
} |
| 34 |
if (end < 0) throw new Error("the archive is not a zip file"); |
| 35 |
const count = b.getUint16(end + 10, true); |
| 36 |
let at = b.getUint32(end + 16, true); |
| 37 |
if (at === 0xffffffff) throw new Error("the archive is too large for this page (zip64)"); |
| 38 |
const files = new Map(); |
| 39 |
const dec = new TextDecoder(); |
| 40 |
for (let n = 0; n < count; n++) { |
| 41 |
if (b.getUint32(at, true) !== 0x02014b50) throw new Error("the archive's directory is damaged"); |
| 42 |
const method = b.getUint16(at + 10, true); |
| 43 |
const csize = b.getUint32(at + 20, true); |
| 44 |
const nlen = b.getUint16(at + 28, true), xlen = b.getUint16(at + 30, true), clen = b.getUint16(at + 32, true); |
| 45 |
const mode = b.getUint32(at + 38, true) >>> 16; |
| 46 |
const local = b.getUint32(at + 42, true); |
| 47 |
const name = dec.decode(bytes.subarray(at + 46, at + 46 + nlen)); |
| 48 |
at += 46 + nlen + xlen + clen; |
| 49 |
if (name.endsWith("/") || (mode & 0o170000) === 0o120000) continue; |
| 50 |
const data = local + 30 + b.getUint16(local + 26, true) + b.getUint16(local + 28, true); |
| 51 |
const raw = bytes.subarray(data, data + csize); |
| 52 |
let content; |
| 53 |
if (method === 0) content = raw.slice(); |
| 54 |
else if (method === 8) content = new Uint8Array(await new Response(new Blob([raw]).stream().pipeThrough(new DecompressionStream("deflate-raw"))).arrayBuffer()); |
| 55 |
else throw new Error(`${name}: compression method ${method} is not read here`); |
| 56 |
const slash = name.indexOf("/"); |
| 57 |
files.set(slash < 0 ? name : name.slice(slash + 1), content); |
| 58 |
} |
| 59 |
return files; |
| 60 |
} |
| 61 |
|
| 62 |
|
| 63 |
|
| 64 |
class Fs { |
| 65 |
constructor(files) { |
| 66 |
this.files = files; |
| 67 |
this.dirs = new Set([""]); |
| 68 |
for (const p of files.keys()) this.addParents(p); |
| 69 |
} |
| 70 |
addParents(p) { |
| 71 |
const parts = p.split("/"); |
| 72 |
for (let i = 1; i < parts.length; i++) this.dirs.add(parts.slice(0, i).join("/")); |
| 73 |
} |
| 74 |
|
| 75 |
join(dir, rel) { |
| 76 |
const out = rel.startsWith("/") ? [] : dir ? dir.split("/") : []; |
| 77 |
for (const part of rel.split("/")) { |
| 78 |
if (part === "" || part === ".") continue; |
| 79 |
if (part === "..") { if (!out.length) return null; out.pop(); } else out.push(part); |
| 80 |
} |
| 81 |
return out.join("/"); |
| 82 |
} |
| 83 |
kind(p) { |
| 84 |
if (this.files.has(p)) return FT.FILE; |
| 85 |
if (this.dirs.has(p)) return FT.DIR; |
| 86 |
return null; |
| 87 |
} |
| 88 |
list(dir) { |
| 89 |
const prefix = dir ? dir + "/" : ""; |
| 90 |
const names = new Set(); |
| 91 |
for (const p of [...this.files.keys(), ...this.dirs]) { |
| 92 |
if (p !== dir && p.startsWith(prefix)) names.add(p.slice(prefix.length).split("/")[0]); |
| 93 |
} |
| 94 |
return [...names].sort(); |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
|
| 99 |
|
| 100 |
function modules(fs, program) { |
| 101 |
const rel = program.replace(/^\/src\//, "").replace(/^\.\//, ""); |
| 102 |
if (!rel || rel.startsWith("/") || rel.split("/").includes("..")) throw new Error(`module ${program}: give a path inside the job's files`); |
| 103 |
const cut = rel.lastIndexOf("/"); |
| 104 |
const dir = cut < 0 ? "" : rel.slice(0, cut), last = rel.slice(cut + 1); |
| 105 |
if (dir.includes("*")) throw new Error(`module ${program}: only the file name may hold *`); |
| 106 |
if (!last.includes("*")) { |
| 107 |
if (!fs.files.has(rel)) throw new Error(`module ${rel} is not among the repository's files (a build makes it?)`); |
| 108 |
return [rel]; |
| 109 |
} |
| 110 |
const re = new RegExp("^" + last.split("*").map((s) => s.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*") + "$"); |
| 111 |
const found = fs.list(dir).filter((n) => re.test(n) && fs.files.has(dir ? `${dir}/${n}` : n)).map((n) => (dir ? `${dir}/${n}` : n)); |
| 112 |
if (!found.length) throw new Error(`no module matches ${program}`); |
| 113 |
return found; |
| 114 |
} |
| 115 |
|
| 116 |
|
| 117 |
|
| 118 |
function wasi(fs, argv, env, say) { |
| 119 |
let memory; |
| 120 |
const view = () => new DataView(memory.buffer); |
| 121 |
const u8 = () => new Uint8Array(memory.buffer); |
| 122 |
const enc = new TextEncoder(), dec = new TextDecoder(); |
| 123 |
const str = (ptr, len) => dec.decode(u8().subarray(ptr, ptr + len)); |
| 124 |
|
| 125 |
const fds = new Map([ |
| 126 |
[0, { stream: "in" }], |
| 127 |
[1, { stream: "out", partial: "" }], |
| 128 |
[2, { stream: "err", partial: "" }], |
| 129 |
[3, { dir: "", preopen: "/src" }], |
| 130 |
[4, { dir: "", preopen: "." }], |
| 131 |
]); |
| 132 |
let next = 5; |
| 133 |
const emit = (fd, text) => { |
| 134 |
const f = fds.get(fd); |
| 135 |
f.partial += text; |
| 136 |
let nl; |
| 137 |
while ((nl = f.partial.indexOf("\n")) >= 0) { |
| 138 |
say(f.stream, f.partial.slice(0, nl)); |
| 139 |
f.partial = f.partial.slice(nl + 1); |
| 140 |
} |
| 141 |
}; |
| 142 |
const flush = () => { for (const fd of [1, 2]) { const f = fds.get(fd); if (f.partial) { say(f.stream, f.partial); f.partial = ""; } } }; |
| 143 |
const filestat = (ptr, kind, size) => { |
| 144 |
const v = view(); |
| 145 |
for (let i = 0; i < 64; i += 8) v.setBigUint64(ptr + i, 0n, true); |
| 146 |
v.setUint8(ptr + 16, kind); |
| 147 |
v.setBigUint64(ptr + 24, 1n, true); |
| 148 |
v.setBigUint64(ptr + 32, BigInt(size), true); |
| 149 |
}; |
| 150 |
const iovs = (ptr, n) => { |
| 151 |
const v = view(), out = []; |
| 152 |
for (let i = 0; i < n; i++) out.push([v.getUint32(ptr + i * 8, true), v.getUint32(ptr + i * 8 + 4, true)]); |
| 153 |
return out; |
| 154 |
}; |
| 155 |
const strings = (list) => list.map((s) => enc.encode(s + "\0")); |
| 156 |
const sizes = (list, countPtr, sizePtr) => { |
| 157 |
const v = view(), all = strings(list); |
| 158 |
v.setUint32(countPtr, all.length, true); |
| 159 |
v.setUint32(sizePtr, all.reduce((n, s) => n + s.length, 0), true); |
| 160 |
return E.SUCCESS; |
| 161 |
}; |
| 162 |
const fill = (list, ptrs, buf) => { |
| 163 |
const v = view(), m = u8(); |
| 164 |
for (const s of strings(list)) { v.setUint32(ptrs, buf, true); ptrs += 4; m.set(s, buf); buf += s.length; } |
| 165 |
return E.SUCCESS; |
| 166 |
}; |
| 167 |
const envList = Object.entries(env).map(([k, v]) => `${k}=${v}`); |
| 168 |
const imports = { |
| 169 |
args_sizes_get: (c, s) => sizes(argv, c, s), |
| 170 |
args_get: (p, b) => fill(argv, p, b), |
| 171 |
environ_sizes_get: (c, s) => sizes(envList, c, s), |
| 172 |
environ_get: (p, b) => fill(envList, p, b), |
| 173 |
clock_time_get: (_id, _precision, out) => { view().setBigUint64(out, BigInt(Math.round(performance.timeOrigin + performance.now())) * 1000000n, true); return E.SUCCESS; }, |
| 174 |
clock_res_get: (_id, out) => { view().setBigUint64(out, 1000n, true); return E.SUCCESS; }, |
| 175 |
random_get: (ptr, len) => { crypto.getRandomValues(u8().subarray(ptr, ptr + len)); return E.SUCCESS; }, |
| 176 |
sched_yield: () => E.SUCCESS, |
| 177 |
proc_exit: (code) => { throw new Exit(code); }, |
| 178 |
fd_prestat_get: (fd, ptr) => { |
| 179 |
const f = fds.get(fd); |
| 180 |
if (!f || !f.preopen) return E.BADF; |
| 181 |
const v = view(); |
| 182 |
v.setUint8(ptr, 0); |
| 183 |
v.setUint32(ptr + 4, enc.encode(f.preopen).length, true); |
| 184 |
return E.SUCCESS; |
| 185 |
}, |
| 186 |
fd_prestat_dir_name: (fd, ptr, len) => { |
| 187 |
const f = fds.get(fd); |
| 188 |
if (!f || !f.preopen) return E.BADF; |
| 189 |
u8().set(enc.encode(f.preopen).subarray(0, len), ptr); |
| 190 |
return E.SUCCESS; |
| 191 |
}, |
| 192 |
fd_fdstat_get: (fd, ptr) => { |
| 193 |
const f = fds.get(fd); |
| 194 |
if (!f) return E.BADF; |
| 195 |
const v = view(); |
| 196 |
v.setUint8(ptr, f.stream ? FT.CHAR : f.dir !== undefined ? FT.DIR : FT.FILE); |
| 197 |
v.setUint16(ptr + 2, f.append ? FD_APPEND : 0, true); |
| 198 |
v.setBigUint64(ptr + 8, RIGHTS, true); |
| 199 |
v.setBigUint64(ptr + 16, RIGHTS, true); |
| 200 |
return E.SUCCESS; |
| 201 |
}, |
| 202 |
fd_fdstat_set_flags: () => E.SUCCESS, |
| 203 |
fd_filestat_get: (fd, ptr) => { |
| 204 |
const f = fds.get(fd); |
| 205 |
if (!f) return E.BADF; |
| 206 |
if (f.stream) filestat(ptr, FT.CHAR, 0); |
| 207 |
else if (f.dir !== undefined) filestat(ptr, FT.DIR, 0); |
| 208 |
else filestat(ptr, FT.FILE, fs.files.get(f.path).length); |
| 209 |
return E.SUCCESS; |
| 210 |
}, |
| 211 |
path_filestat_get: (fd, _flags, p, len, ptr) => { |
| 212 |
const d = fds.get(fd); |
| 213 |
if (!d || d.dir === undefined) return E.BADF; |
| 214 |
const path = fs.join(d.dir, str(p, len)); |
| 215 |
if (path === null) return E.ACCES; |
| 216 |
const kind = fs.kind(path); |
| 217 |
if (kind === null) return E.NOENT; |
| 218 |
filestat(ptr, kind, kind === FT.FILE ? fs.files.get(path).length : 0); |
| 219 |
return E.SUCCESS; |
| 220 |
}, |
| 221 |
path_open: (fd, _dirflags, p, len, oflags, _rb, _ri, fdflags, out) => { |
| 222 |
const d = fds.get(fd); |
| 223 |
if (!d || d.dir === undefined) return E.BADF; |
| 224 |
const path = fs.join(d.dir, str(p, len)); |
| 225 |
if (path === null) return E.ACCES; |
| 226 |
let kind = fs.kind(path); |
| 227 |
if (oflags & O.DIRECTORY) { |
| 228 |
if (kind === null) return E.NOENT; |
| 229 |
if (kind !== FT.DIR) return E.NOTDIR; |
| 230 |
} |
| 231 |
if (kind === null) { |
| 232 |
if (!(oflags & O.CREAT)) return E.NOENT; |
| 233 |
const parent = path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : ""; |
| 234 |
if (fs.kind(parent) !== FT.DIR) return E.NOENT; |
| 235 |
fs.files.set(path, new Uint8Array(0)); |
| 236 |
kind = FT.FILE; |
| 237 |
} else if (oflags & O.EXCL) return E.EXIST; |
| 238 |
if (kind === FT.DIR) fds.set(next, { dir: path, entries: null }); |
| 239 |
else { |
| 240 |
if (oflags & O.TRUNC) fs.files.set(path, new Uint8Array(0)); |
| 241 |
fds.set(next, { path, pos: 0, append: !!(fdflags & FD_APPEND) }); |
| 242 |
} |
| 243 |
view().setUint32(out, next, true); |
| 244 |
next += 1; |
| 245 |
return E.SUCCESS; |
| 246 |
}, |
| 247 |
path_create_directory: (fd, p, len) => { |
| 248 |
const d = fds.get(fd); |
| 249 |
if (!d || d.dir === undefined) return E.BADF; |
| 250 |
const path = fs.join(d.dir, str(p, len)); |
| 251 |
if (path === null) return E.ACCES; |
| 252 |
if (fs.kind(path) !== null) return E.EXIST; |
| 253 |
fs.dirs.add(path); |
| 254 |
return E.SUCCESS; |
| 255 |
}, |
| 256 |
path_unlink_file: (fd, p, len) => { |
| 257 |
const d = fds.get(fd); |
| 258 |
if (!d || d.dir === undefined) return E.BADF; |
| 259 |
const path = fs.join(d.dir, str(p, len)); |
| 260 |
if (path === null || !fs.files.has(path)) return E.NOENT; |
| 261 |
fs.files.delete(path); |
| 262 |
return E.SUCCESS; |
| 263 |
}, |
| 264 |
fd_close: (fd) => { |
| 265 |
if (!fds.has(fd) || fd < 3) return fds.has(fd) ? E.SUCCESS : E.BADF; |
| 266 |
fds.delete(fd); |
| 267 |
return E.SUCCESS; |
| 268 |
}, |
| 269 |
fd_read: (fd, ptr, n, out) => { |
| 270 |
const f = fds.get(fd); |
| 271 |
if (!f) return E.BADF; |
| 272 |
if (f.dir !== undefined) return E.ISDIR; |
| 273 |
let total = 0; |
| 274 |
if (!f.stream) { |
| 275 |
const data = fs.files.get(f.path), m = u8(); |
| 276 |
for (const [buf, len] of iovs(ptr, n)) { |
| 277 |
const k = Math.min(len, data.length - f.pos); |
| 278 |
if (k <= 0) break; |
| 279 |
m.set(data.subarray(f.pos, f.pos + k), buf); |
| 280 |
f.pos += k; |
| 281 |
total += k; |
| 282 |
} |
| 283 |
} |
| 284 |
view().setUint32(out, total, true); |
| 285 |
return E.SUCCESS; |
| 286 |
}, |
| 287 |
fd_write: (fd, ptr, n, out) => { |
| 288 |
const f = fds.get(fd); |
| 289 |
if (!f) return E.BADF; |
| 290 |
if (f.dir !== undefined) return E.ISDIR; |
| 291 |
let total = 0; |
| 292 |
const m = u8(); |
| 293 |
for (const [buf, len] of iovs(ptr, n)) { |
| 294 |
const chunk = m.slice(buf, buf + len); |
| 295 |
if (f.stream === "out" || f.stream === "err") emit(fd, dec.decode(chunk, { stream: true })); |
| 296 |
else if (!f.stream) { |
| 297 |
const old = fs.files.get(f.path); |
| 298 |
const at = f.append ? old.length : f.pos; |
| 299 |
const grown = new Uint8Array(Math.max(old.length, at + len)); |
| 300 |
grown.set(old); |
| 301 |
grown.set(chunk, at); |
| 302 |
fs.files.set(f.path, grown); |
| 303 |
f.pos = at + len; |
| 304 |
} |
| 305 |
total += len; |
| 306 |
} |
| 307 |
view().setUint32(out, total, true); |
| 308 |
return E.SUCCESS; |
| 309 |
}, |
| 310 |
fd_seek: (fd, offset, whence, out) => { |
| 311 |
const f = fds.get(fd); |
| 312 |
if (!f || f.stream || f.dir !== undefined) return E.BADF; |
| 313 |
const size = fs.files.get(f.path).length; |
| 314 |
const base = whence === 0 ? 0 : whence === 1 ? f.pos : size; |
| 315 |
const pos = base + Number(offset); |
| 316 |
if (pos < 0) return E.INVAL; |
| 317 |
f.pos = pos; |
| 318 |
view().setBigUint64(out, BigInt(pos), true); |
| 319 |
return E.SUCCESS; |
| 320 |
}, |
| 321 |
fd_tell: (fd, out) => { |
| 322 |
const f = fds.get(fd); |
| 323 |
if (!f || f.stream || f.dir !== undefined) return E.BADF; |
| 324 |
view().setBigUint64(out, BigInt(f.pos), true); |
| 325 |
return E.SUCCESS; |
| 326 |
}, |
| 327 |
fd_readdir: (fd, buf, len, cookie, out) => { |
| 328 |
const d = fds.get(fd); |
| 329 |
if (!d || d.dir === undefined) return E.BADF; |
| 330 |
if (!d.entries) d.entries = [".", "..", ...fs.list(d.dir)]; |
| 331 |
const v = view(), m = u8(); |
| 332 |
let used = 0; |
| 333 |
for (let i = Number(cookie); i < d.entries.length; i++) { |
| 334 |
const name = d.entries[i]; |
| 335 |
const bytes = enc.encode(name); |
| 336 |
const child = name === "." || name === ".." ? d.dir : fs.join(d.dir, name); |
| 337 |
const head = new DataView(new ArrayBuffer(24)); |
| 338 |
head.setBigUint64(0, BigInt(i + 1), true); |
| 339 |
head.setBigUint64(8, BigInt(i + 1), true); |
| 340 |
head.setUint32(16, bytes.length, true); |
| 341 |
head.setUint8(20, name === "." || name === ".." ? FT.DIR : fs.kind(child) ?? FT.UNKNOWN); |
| 342 |
const entry = new Uint8Array(24 + bytes.length); |
| 343 |
entry.set(new Uint8Array(head.buffer)); |
| 344 |
entry.set(bytes, 24); |
| 345 |
const room = len - used; |
| 346 |
m.set(entry.subarray(0, Math.min(room, entry.length)), buf + used); |
| 347 |
used += Math.min(room, entry.length); |
| 348 |
if (used >= len) break; |
| 349 |
} |
| 350 |
v.setUint32(out, used, true); |
| 351 |
return E.SUCCESS; |
| 352 |
}, |
| 353 |
}; |
| 354 |
return { imports, flush, bind: (mem) => { memory = mem; } }; |
| 355 |
} |
| 356 |
|
| 357 |
|
| 358 |
|
| 359 |
async function run({ archive, exec, env }) { |
| 360 |
const say = (kind, text) => postMessage({ kind, text }); |
| 361 |
const started = performance.now(); |
| 362 |
const fs = new Fs(await unzip(archive)); |
| 363 |
say("note", `${fs.files.size} files unpacked in ${Math.round(performance.now() - started)} ms`); |
| 364 |
const list = modules(fs, exec[0]); |
| 365 |
let first = 0; |
| 366 |
for (const path of list) { |
| 367 |
say("note", `running ${path}`); |
| 368 |
const name = path.slice(path.lastIndexOf("/") + 1); |
| 369 |
const w = wasi(fs, [name, ...exec.slice(1)], { ...env, PWD: "/src" }, say); |
| 370 |
const module = await WebAssembly.compile(fs.files.get(path)); |
| 371 |
const imports = { wasi_snapshot_preview1: {} }; |
| 372 |
|
| 373 |
for (const imp of WebAssembly.Module.imports(module)) { |
| 374 |
if (imp.kind !== "function") continue; |
| 375 |
imports[imp.module] ??= {}; |
| 376 |
if (imp.module === "wasi_snapshot_preview1" && w.imports[imp.name]) imports[imp.module][imp.name] = w.imports[imp.name]; |
| 377 |
else imports[imp.module][imp.name] ??= () => E.NOSYS; |
| 378 |
} |
| 379 |
const instance = await WebAssembly.instantiate(module, imports); |
| 380 |
w.bind(instance.exports.memory); |
| 381 |
let code = 0; |
| 382 |
try { |
| 383 |
instance.exports._start(); |
| 384 |
} catch (e) { |
| 385 |
if (e instanceof Exit) code = e.code; |
| 386 |
else { |
| 387 |
w.flush(); |
| 388 |
say("err", `${path} trapped: ${e.message}`); |
| 389 |
code = 134; |
| 390 |
} |
| 391 |
} |
| 392 |
w.flush(); |
| 393 |
if (code !== 0 && first === 0) first = code; |
| 394 |
} |
| 395 |
postMessage({ kind: "done", code: first, ms: Math.round(performance.now() - started) }); |
| 396 |
} |
| 397 |
|
| 398 |
onmessage = (m) => { |
| 399 |
run(m.data).catch((e) => postMessage({ kind: "failed", text: e.message })); |
| 400 |
}; |