diff --git a/lib/routes/bilibili/manga-update.ts b/lib/routes/bilibili/manga-update.ts index cae76c3ba7..62957f5930 100644 --- a/lib/routes/bilibili/manga-update.ts +++ b/lib/routes/bilibili/manga-update.ts @@ -1,5 +1,6 @@ +import { createDecipheriv } from 'node:crypto'; + import type { Route } from '@/types'; -import cache from '@/utils/cache'; import got from '@/utils/got'; export const route: Route = { @@ -25,56 +26,175 @@ export const route: Route = { handler, }; -// Based on https://github.com/SocialSisterYi/bilibili-API-collect/issues/1168#issuecomment-2620749895 -async function genReqSign(query, body) { - // Don't import on top-level to avoid a cyclic dependency - wasm-exec.js generated via `pnpm build`, which in turn needs wasm-exec.js to import routes correctly - const { Go } = await import('./wasm-exec'); +const ALPHA62 = 'br5NH9CxpvJzmEt3OqoF7jgWMIZ2UnASfc8K6XaLYylT1eVBGh4sdQkDwRP0ui'; +const B64_TABLES = ['UalVqQk8T7+5oJxC6MzIniWDrc3XERGN/wdLyp90mf2tHuvsgeSjZbBF1KAOPhY4', 'zI3XZb8T7+5opF1KJxUaBqQk90mf6MlVCEceSjAOPhY42RGN/wdLytHuvsgniWDr']; +const rotateLeft = (x: number, n: number) => ((x << n) | (x >>> (32 - n))) >>> 0; - // Cache the wasm binary as it's quite large (~2MB) - // Here the binary is saved as base64 as the cache stores strings - const wasmBufferBase64 = await cache.tryGet('bilibili-manga-wasm-20250208', async () => { - const wasmResp = await got('https://s1.hdslb.com/bfs/manga-static/manga-pc/6732b1bf426cfc634293.wasm', { - responseType: 'arrayBuffer', - }); - return Buffer.from(wasmResp.data).toString('base64'); - }); - const wasmBuffer = Buffer.from(wasmBufferBase64, 'base64'); - - const go = new Go(); - const { instance } = await WebAssembly.instantiate(wasmBuffer, go.importObject); - go.run(instance); - if (void 0 === globalThis.genReqSign) { - throw new Error('WASM function not available'); +/** + * MD5-shaped block mixer with its own round function and rotation constants + */ +const processBlock = (block: Uint8Array, [a, b, c, d]: number[]) => { + for (let i = 0; i < 16; i++) { + const w = block[i * 4] | (block[i * 4 + 1] << 8) | (block[i * 4 + 2] << 16) | (block[i * 4 + 3] << 24); + a = (a + ((b & c) | (~b & d)) + w) >>> 0; + b = (b + (c ^ a ^ d) + w) >>> 0; + c = (c + ((b & a) | (~b & d)) + w) >>> 0; + d = (d + (c ^ a ^ b) + w) >>> 0; + a = rotateLeft(a, 5); + b = rotateLeft(b, 7); + c = rotateLeft(c, 3); + d = rotateLeft(d, 11); } + return [a, b, c, d]; +}; - const signature = globalThis.genReqSign(query, body, Date.now()); +/** + * Same as MD5 padding except a length of 55 mod 64 gets a full extra block + */ +const md5v2 = (data: Uint8Array) => { + const zeros = 64 - ((data.length + 9) % 64); + const buf = Buffer.alloc(data.length + 1 + zeros + 8); + buf.set(data); + buf[data.length] = 0x80; + buf.writeBigUInt64LE(BigInt(data.length) * 8n, buf.length - 8); + let state = [0xab_cd_ef_01, 0x12_34_56_78, 0xde_ad_be_ef, 0xfe_ed_ca_fe]; + for (let i = 0; i < buf.length; i += 64) { + state = processBlock(buf.subarray(i, i + 64), state); + } + const hex = state.map((w) => w.toString(16).padStart(8, '0')).join(''); + return Buffer.from([...hex].map((ch) => ALPHA62.codePointAt(ch.codePointAt(0)! % 62)!)); +}; - return signature.sign; -} +/** + * RC4 with `S[i] ^= i` after each KSA swap and a +1 offset on the keystream index + */ +const xrc4 = (key: number[], data: Uint8Array) => { + const S = Uint8Array.from({ length: 256 }, (_, i) => i); + for (let i = 0, j = 0; i < 256; i++) { + j = (j + S[i] + key[i % key.length]) & 255; + [S[i], S[j]] = [S[j], S[i]]; + S[i] ^= i; + } + const out = Buffer.alloc(data.length); + for (let n = 0, i = 0, j = 0; n < data.length; n++) { + i = (i + 1) & 255; + j = (j + S[i]) & 255; + [S[i], S[j]] = [S[j], S[i]]; + out[n] = data[n] ^ S[(S[i] + S[j] + 1) & 255]; + } + return out; +}; + +const b64V2Enc = (data: Uint8Array, variant: number) => { + const table = B64_TABLES[variant]; + let out = ''; + for (let i = 0; i < data.length; i += 3) { + const v = (data[i] << 16) | ((data[i + 1] ?? 0) << 8) | (data[i + 2] ?? 0); + for (let k = 0; k < 4; k++) { + out += table[(v >>> (18 - 6 * k)) & 63]; + } + } + const rem = data.length % 3; + return rem ? out + '='.repeat(3 - rem) : out; +}; + +// Based on https://github.com/SocialSisterYi/bilibili-API-collect/issues/1168#issuecomment-2620749895 +// https://s1.hdslb.com/bfs/manga-static/manga-pc/6732b1bf426cfc634293.wasm +export const genReqSign = (query: string, body: string, ts: number) => { + const params = new URLSearchParams(query); + const keys = [...new Set(params.keys())].filter((k) => k !== 'ultra_sign').toSorted((a, b) => Number(a > b) - Number(a < b)); + const acc = keys.map((k) => k + params.getAll(k).join('')).join(''); + const bts = BigInt(ts); + const h1 = md5v2(Buffer.from(acc + bts.toString(16))); + const h2 = md5v2(Buffer.from(body + (bts / 128n).toString(2))); + const encBody = b64V2Enc(xrc4([1, 3, 7], Buffer.from(body)), Number(bts % 2n)); + const h3 = md5v2(Buffer.from(encBody)); + const t = Number(bts & 0xff_ff_ff_ffn); + const buf = Buffer.from([ + 1, + Number((bts >> 23n) & 0xffn), + Number((bts >> 31n) & 0xffn), + Number((bts >> 15n) & 0xffn), + 7, + h1[13], + h1[23], + h2[11], + h2[7], + h3[23], + h3[19], + (t >>> 8) & 255, + (t >>> 24) & 255, + (t >>> 16) & 255, + 7, + 0, + 5, + 0, + 4, + 1, + 5, + 0, + 2, + 0, + 1, + 0, + 6, + 2, + 0, + 0, + ]); + // oxlint-disable-next-line unicorn/no-array-reduce + buf[29] = buf.subarray(0, 28).reduce((x, y) => x ^ y, 0); + const out = Buffer.from([buf[0], buf[1], 0, 0, 0, ...[22, 7, 17, 21, 16, 28, 15, 12, 4, 3, 14, 27, 10, 6, 11, 19, 24, 25, 26, 20, 23, 18, 8, 2].map((i) => buf[i])]); + const hA = md5v2(Buffer.concat([out, Buffer.from([h1[7], h2[29]])])); + const hB = md5v2(Buffer.concat([out, Buffer.from([h3[31]])])); + const sign = Buffer.concat([hA.subarray(0, 24), hB.subarray(0, 24)]); + // The 11 hex digits of the timestamp are spliced into fixed positions of the digest + const hex = bts.toString(16); + for (const [pos, i] of [41, 37, 23, 29, 13, 19, 5, 47, 7, 11, 17].entries()) { + sign[i] = hex.codePointAt(pos)!; + } + return sign.toString('latin1'); +}; + +const decryptBytesData = (bytesData: string, buvid3: string, comicId: string) => { + const key = Buffer.from( + [...buvid3] + .filter((_, i) => i % 2 === 0) + .join('') + .slice(0, 18) + + 'web' + + comicId.slice(-3) + ); + const decipher = createDecipheriv('aes-192-cbc', key, key.subarray(0, 16)); + const plain = Buffer.concat([decipher.update(Buffer.from(bytesData, 'base64')), decipher.final()]); + return JSON.parse(plain.toString()); +}; async function handler(ctx) { const comic_id = ctx.req.param('comicid').startsWith('mc') ? ctx.req.param('comicid').replace('mc', '') : ctx.req.param('comicid'); const link = `https://manga.bilibili.com/detail/mc${comic_id}`; const spi_response = await got('https://api.bilibili.com/x/frontend/finger/spi'); + const buvid3 = spi_response.data.data.b_3; - const query = 'device=pc&platform=web&nov=25'; + const query = 'device=pc&platform=web&nov=27&a=810'; const body = JSON.stringify({ comic_id: Number(comic_id), }); - const ultraSign = await genReqSign(query, body); + const ultraSign = genReqSign(query, body, Date.now()); const response = await got({ method: 'POST', - url: `https://manga.bilibili.com/twirp/comic.v2.Comic/ComicDetail?${query}&ultra_sign=${ultraSign}`, + url: `https://manga.bilibili.com/twirp/comic.v1.Comic/ComicDetail?${query}&ultra_sign=${ultraSign}`, body, headers: { Referer: link, - Cookie: `buvid3=${spi_response.data.data.b_3}; buvid4=${spi_response.data.data.b_4}`, + Cookie: `buvid3=${buvid3}; buvid4=${spi_response.data.data.b_4}`, + 'x-bili-data-sn': '1E74C20E5720FBF3BB351965D7A9DFC1', }, }); - const data = response.data.data; + const data = decryptBytesData(response.data.bytesData, buvid3, comic_id); const author = data.author_name.join(', '); return { diff --git a/lib/routes/bilibili/wasm-exec.ts b/lib/routes/bilibili/wasm-exec.ts deleted file mode 100644 index a14b739bba..0000000000 --- a/lib/routes/bilibili/wasm-exec.ts +++ /dev/null @@ -1,661 +0,0 @@ -// @ts-nocheck https://github.com/golang/go/blob/master/lib/wasm/wasm_exec.js -// oxlint-disable unicorn/prefer-math-trunc -// oxlint-disable unicorn-js/no-this-outside-of-class -// oxlint-disable unicorn-js/no-array-from-fill -// oxlint-disable unicorn-js/no-global-object-property-assignment -// oxlint-disable unicorn-js/no-unnecessary-global-this -// oxlint-disable unicorn-js/no-undeclared-class-members -// oxlint-disable unicorn-js/prefer-array-from-map -// oxlint-disable unicorn-js/require-array-sort-compare -// oxlint-disable unicorn-js/prefer-short-arrow-method -// oxlint-disable unicorn-js/prefer-block-statement-over-iife -// oxlint-disable no-unused-vars -// oxlint-disable unicorn/consistent-function-scoping -/* eslint-disable prefer-rest-params */ -/* eslint-disable default-case */ -/* eslint-disable no-console */ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -'use strict'; - -(() => { - const enosys = () => { - const err = new Error('not implemented'); - err.code = 'ENOSYS'; - return err; - }; - - if (!globalThis.fs) { - let outputBuf = ''; - globalThis.fs = { - constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused - writeSync(fd, buf) { - outputBuf += decoder.decode(buf); - const nl = outputBuf.lastIndexOf('\n'); - if (nl !== -1) { - console.log(outputBuf.slice(0, nl)); - outputBuf = outputBuf.slice(nl + 1); - } - return buf.length; - }, - write(fd, buf, offset, length, position, callback) { - if (offset !== 0 || length !== buf.length || position !== null) { - callback(enosys()); - return; - } - const n = this.writeSync(fd, buf); - callback(null, n); - }, - chmod(path, mode, callback) { - callback(enosys()); - }, - chown(path, uid, gid, callback) { - callback(enosys()); - }, - close(fd, callback) { - callback(enosys()); - }, - fchmod(fd, mode, callback) { - callback(enosys()); - }, - fchown(fd, uid, gid, callback) { - callback(enosys()); - }, - fstat(fd, callback) { - callback(enosys()); - }, - fsync(fd, callback) { - callback(null); - }, - ftruncate(fd, length, callback) { - callback(enosys()); - }, - lchown(path, uid, gid, callback) { - callback(enosys()); - }, - link(path, link, callback) { - callback(enosys()); - }, - lstat(path, callback) { - callback(enosys()); - }, - mkdir(path, perm, callback) { - callback(enosys()); - }, - open(path, flags, mode, callback) { - callback(enosys()); - }, - read(fd, buffer, offset, length, position, callback) { - callback(enosys()); - }, - readdir(path, callback) { - callback(enosys()); - }, - readlink(path, callback) { - callback(enosys()); - }, - rename(from, to, callback) { - callback(enosys()); - }, - rmdir(path, callback) { - callback(enosys()); - }, - stat(path, callback) { - callback(enosys()); - }, - symlink(path, link, callback) { - callback(enosys()); - }, - truncate(path, length, callback) { - callback(enosys()); - }, - unlink(path, callback) { - callback(enosys()); - }, - utimes(path, atime, mtime, callback) { - callback(enosys()); - }, - }; - } - - if (!globalThis.process) { - globalThis.process = { - getuid() { - return -1; - }, - getgid() { - return -1; - }, - geteuid() { - return -1; - }, - getegid() { - return -1; - }, - getgroups() { - throw enosys(); - }, - pid: -1, - ppid: -1, - umask() { - throw enosys(); - }, - cwd() { - throw enosys(); - }, - chdir() { - throw enosys(); - }, - }; - } - - if (!globalThis.path) { - globalThis.path = { - resolve(...pathSegments) { - return pathSegments.join('/'); - }, - }; - } - - if (!globalThis.crypto) { - throw new Error('globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)'); - } - - if (!globalThis.performance) { - throw new Error('globalThis.performance is not available, polyfill required (performance.now only)'); - } - - if (!globalThis.TextEncoder) { - throw new Error('globalThis.TextEncoder is not available, polyfill required'); - } - - if (!globalThis.TextDecoder) { - throw new Error('globalThis.TextDecoder is not available, polyfill required'); - } - - const encoder = new TextEncoder(); - const decoder = new TextDecoder('utf-8'); - - globalThis.Go = class { - constructor() { - this.argv = ['js']; - this.env = {}; - this.exit = (code) => { - if (code !== 0) { - console.warn('exit code:', code); - } - }; - this._exitPromise = new Promise((resolve) => { - this._resolveExitPromise = resolve; - }); - this._pendingEvent = null; - this._scheduledTimeouts = new Map(); - this._nextCallbackTimeoutID = 1; - - const setInt64 = (addr, v) => { - this.mem.setUint32(addr + 0, v, true); - this.mem.setUint32(addr + 4, Math.floor(v / 4_294_967_296), true); - }; - - const getInt64 = (addr) => { - const low = this.mem.getUint32(addr + 0, true); - const high = this.mem.getInt32(addr + 4, true); - return low + high * 4_294_967_296; - }; - - const loadValue = (addr) => { - const f = this.mem.getFloat64(addr, true); - if (f === 0) { - return; - } - if (!Number.isNaN(f)) { - return f; - } - - const id = this.mem.getUint32(addr, true); - return this._values[id]; - }; - - const storeValue = (addr, v) => { - const nanHead = 0x7f_f8_00_00; - - if (typeof v === 'number' && v !== 0) { - if (Number.isNaN(v)) { - this.mem.setUint32(addr + 4, nanHead, true); - this.mem.setUint32(addr, 0, true); - return; - } - this.mem.setFloat64(addr, v, true); - return; - } - - if (v === undefined) { - this.mem.setFloat64(addr, 0, true); - return; - } - - let id = this._ids.get(v); - if (id === undefined) { - id = this._idPool.pop(); - if (id === undefined) { - id = this._values.length; - } - this._values[id] = v; - this._goRefCounts[id] = 0; - this._ids.set(v, id); - } - this._goRefCounts[id]++; - let typeFlag = 0; - switch (typeof v) { - case 'object': - if (v !== null) { - typeFlag = 1; - } - break; - case 'string': - typeFlag = 2; - break; - case 'symbol': - typeFlag = 3; - break; - case 'function': - typeFlag = 4; - break; - } - this.mem.setUint32(addr + 4, nanHead | typeFlag, true); - this.mem.setUint32(addr, id, true); - }; - - const loadSlice = (addr) => { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - return new Uint8Array(this._inst.exports.mem.buffer, array, len); - }; - - const loadSliceOfValues = (addr) => { - const array = getInt64(addr + 0); - const len = getInt64(addr + 8); - const a = Array.from({ length: len }); - for (let i = 0; i < len; i++) { - a[i] = loadValue(array + i * 8); - } - return a; - }; - - const loadString = (addr) => { - const saddr = getInt64(addr + 0); - const len = getInt64(addr + 8); - return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); - }; - - const testCallExport = (a, b) => { - this._inst.exports.testExport0(); - return this._inst.exports.testExport(a, b); - }; - - const timeOrigin = Date.now() - performance.now(); - this.importObject = { - _gotest: { - add: (a, b) => a + b, - callExport: testCallExport, - }, - gojs: { - // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) - // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported - // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). - // This changes the SP, thus we have to update the SP used by the imported function. - - // func wasmExit(code int32) - 'runtime.wasmExit': (sp) => { - sp >>>= 0; - const code = this.mem.getInt32(sp + 8, true); - this.exited = true; - delete this._inst; - delete this._values; - delete this._goRefCounts; - delete this._ids; - delete this._idPool; - this.exit(code); - }, - - // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) - 'runtime.wasmWrite': (sp) => { - sp >>>= 0; - const fd = getInt64(sp + 8); - const p = getInt64(sp + 16); - const n = this.mem.getInt32(sp + 24, true); - globalThis.fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); - }, - - // func resetMemoryDataView() - 'runtime.resetMemoryDataView': (sp) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-useless-assignment - sp >>>= 0; - this.mem = new DataView(this._inst.exports.mem.buffer); - }, - - // func nanotime1() int64 - 'runtime.nanotime1': (sp) => { - sp >>>= 0; - setInt64(sp + 8, (timeOrigin + performance.now()) * 1_000_000); - }, - - // func walltime() (sec int64, nsec int32) - 'runtime.walltime': (sp) => { - sp >>>= 0; - const msec = Date.now(); - setInt64(sp + 8, msec / 1000); - this.mem.setInt32(sp + 16, (msec % 1000) * 1_000_000, true); - }, - - // func scheduleTimeoutEvent(delay int64) int32 - 'runtime.scheduleTimeoutEvent': (sp) => { - sp >>>= 0; - const id = this._nextCallbackTimeoutID; - this._nextCallbackTimeoutID++; - this._scheduledTimeouts.set( - id, - setTimeout( - () => { - this._resume(); - while (this._scheduledTimeouts.has(id)) { - // for some reason Go failed to register the timeout event, log and try again - // (temporary workaround for https://github.com/golang/go/issues/28975) - console.warn('scheduleTimeoutEvent: missed timeout event'); - this._resume(); - } - }, - getInt64(sp + 8) - ) - ); - this.mem.setInt32(sp + 16, id, true); - }, - - // func clearTimeoutEvent(id int32) - 'runtime.clearTimeoutEvent': (sp) => { - sp >>>= 0; - const id = this.mem.getInt32(sp + 8, true); - clearTimeout(this._scheduledTimeouts.get(id)); - this._scheduledTimeouts.delete(id); - }, - - // func getRandomData(r []byte) - 'runtime.getRandomData': (sp) => { - sp >>>= 0; - crypto.getRandomValues(loadSlice(sp + 8)); - }, - - // func finalizeRef(v ref) - 'syscall/js.finalizeRef': (sp) => { - sp >>>= 0; - const id = this.mem.getUint32(sp + 8, true); - this._goRefCounts[id]--; - if (this._goRefCounts[id] !== 0) { - return; - } - - const v = this._values[id]; - this._values[id] = null; - this._ids.delete(v); - this._idPool.push(id); - }, - - // func stringVal(value string) ref - 'syscall/js.stringVal': (sp) => { - sp >>>= 0; - storeValue(sp + 24, loadString(sp + 8)); - }, - - // func valueGet(v ref, p string) ref - 'syscall/js.valueGet': (sp) => { - sp >>>= 0; - const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 32, result); - }, - - // func valueSet(v ref, p string, x ref) - 'syscall/js.valueSet': (sp) => { - sp >>>= 0; - Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); - }, - - // func valueDelete(v ref, p string) - 'syscall/js.valueDelete': (sp) => { - sp >>>= 0; - Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); - }, - - // func valueIndex(v ref, i int) ref - 'syscall/js.valueIndex': (sp) => { - sp >>>= 0; - storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); - }, - - // valueSetIndex(v ref, i int, x ref) - 'syscall/js.valueSetIndex': (sp) => { - sp >>>= 0; - Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); - }, - - // func valueCall(v ref, m string, args []ref) (ref, bool) - 'syscall/js.valueCall': (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const m = Reflect.get(v, loadString(sp + 16)); - const args = loadSliceOfValues(sp + 32); - const result = Reflect.apply(m, v, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 56, result); - this.mem.setUint8(sp + 64, 1); - } catch (error) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 56, error); - this.mem.setUint8(sp + 64, 0); - } - }, - - // func valueInvoke(v ref, args []ref) (ref, bool) - 'syscall/js.valueInvoke': (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const args = loadSliceOfValues(sp + 16); - const result = Reflect.apply(v, undefined, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, result); - this.mem.setUint8(sp + 48, 1); - } catch (error) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, error); - this.mem.setUint8(sp + 48, 0); - } - }, - - // func valueNew(v ref, args []ref) (ref, bool) - 'syscall/js.valueNew': (sp) => { - sp >>>= 0; - try { - const v = loadValue(sp + 8); - const args = loadSliceOfValues(sp + 16); - const result = Reflect.construct(v, args); - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, result); - this.mem.setUint8(sp + 48, 1); - } catch (error) { - sp = this._inst.exports.getsp() >>> 0; // see comment above - storeValue(sp + 40, error); - this.mem.setUint8(sp + 48, 0); - } - }, - - // func valueLength(v ref) int - 'syscall/js.valueLength': (sp) => { - sp >>>= 0; - setInt64(sp + 16, Number.parseInt(loadValue(sp + 8).length)); - }, - - // valuePrepareString(v ref) (ref, int) - 'syscall/js.valuePrepareString': (sp) => { - sp >>>= 0; - const str = encoder.encode(String(loadValue(sp + 8))); - storeValue(sp + 16, str); - setInt64(sp + 24, str.length); - }, - - // valueLoadString(v ref, b []byte) - 'syscall/js.valueLoadString': (sp) => { - sp >>>= 0; - const str = loadValue(sp + 8); - loadSlice(sp + 16).set(str); - }, - - // func valueInstanceOf(v ref, t ref) bool - 'syscall/js.valueInstanceOf': (sp) => { - sp >>>= 0; - this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0); - }, - - // func copyBytesToGo(dst []byte, src ref) (int, bool) - 'syscall/js.copyBytesToGo': (sp) => { - sp >>>= 0; - const dst = loadSlice(sp + 8); - const src = loadValue(sp + 32); - if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { - this.mem.setUint8(sp + 48, 0); - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(sp + 40, toCopy.length); - this.mem.setUint8(sp + 48, 1); - }, - - // func copyBytesToJS(dst ref, src []byte) (int, bool) - 'syscall/js.copyBytesToJS': (sp) => { - sp >>>= 0; - const dst = loadValue(sp + 8); - const src = loadSlice(sp + 16); - if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { - this.mem.setUint8(sp + 48, 0); - return; - } - const toCopy = src.subarray(0, dst.length); - dst.set(toCopy); - setInt64(sp + 40, toCopy.length); - this.mem.setUint8(sp + 48, 1); - }, - - debug: (value) => { - console.log(value); - }, - }, - }; - } - - async run(instance) { - if (!(instance instanceof WebAssembly.Instance)) { - throw new TypeError('Go.run: WebAssembly.Instance expected'); - } - this._inst = instance; - this.mem = new DataView(this._inst.exports.mem.buffer); - this._values = [ - // JS values that Go currently has references to, indexed by reference id - NaN, - 0, - null, - true, - false, - globalThis, - this, - ]; - this._goRefCounts = Array.from({ length: this._values.length }).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id - this._ids = new Map([ - // mapping from JS values to reference ids - [0, 1], - [null, 2], - [true, 3], - [false, 4], - [globalThis, 5], - [this, 6], - ]); - this._idPool = []; // unused ids that have been garbage collected - this.exited = false; // whether the Go program has exited - - // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. - let offset = 4096; - - const strPtr = (str) => { - const ptr = offset; - const bytes = encoder.encode(str + '\0'); - new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); - offset += bytes.length; - if (offset % 8 !== 0) { - offset += 8 - (offset % 8); - } - return ptr; - }; - - const argc = this.argv.length; - - const argvPtrs: number[] = []; - for (const arg of this.argv) { - argvPtrs.push(strPtr(arg)); - } - argvPtrs.push(0); - - const keys = Object.keys(this.env).toSorted(); - for (const key of keys) { - argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); - } - argvPtrs.push(0); - - const argv = offset; - for (const ptr of argvPtrs) { - this.mem.setUint32(offset, ptr, true); - this.mem.setUint32(offset + 4, 0, true); - offset += 8; - } - - // The linker guarantees global data starts from at least wasmMinDataAddr. - // Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr. - const wasmMinDataAddr = 4096 + 8192; - if (offset >= wasmMinDataAddr) { - throw new Error('total length of command line and environment variables exceeds limit'); - } - - this._inst.exports.run(argc, argv); - if (this.exited) { - this._resolveExitPromise(); - } - await this._exitPromise; - } - - _resume() { - if (this.exited) { - throw new Error('Go program has already exited'); - } - this._inst.exports.resume(); - if (this.exited) { - this._resolveExitPromise(); - } - } - - _makeFuncWrapper(id) { - // somehow avoiding aliasing this with an arrow function doesn't work - // oxlint-disable-next-line unicorn/no-this-assignment typescript/no-this-alias - const go = this; - return function () { - const event = { id, this: this, args: arguments }; - go._pendingEvent = event; - go._resume(); - return event.result; - }; - } - }; -})(); - -export const Go = globalThis.Go;