diff --git a/.dockerignore b/.dockerignore index be6ab71..7ee9628 100644 --- a/.dockerignore +++ b/.dockerignore @@ -26,3 +26,5 @@ test-hashes.json test_updater.txt test_handlers_hashes.json +.taskfile.env +.task \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0313be4..6fb7fb7 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ test_handlers_hashes.json .DS_Store Thumbs.db +.taskfile.env +.task \ No newline at end of file diff --git a/README.md b/README.md index 00be85b..6090c48 100644 --- a/README.md +++ b/README.md @@ -62,18 +62,14 @@ A software distribution platform for assets built and hosted on Gitea. Built wit ### Installation -1. **Build the Frontend**: +1. **Build Everything (WASM, Frontend, Backend)**: ```bash - cd frontend - pnpm install - pnpm build - cd .. + go-task all ``` -2. **Build and Run the Backend**: +2. **Run the Application**: ```bash - go build -o software-station . - ./software-station -t YOUR_TOKEN -s https://your-gitea-instance.com -ua-blocklist ua-blocklist.txt + go-task run ``` ### Docker (Recommended) @@ -107,23 +103,18 @@ The frontend uses Tailwind CSS. You can customize the look and feel in `frontend ## Development -Run the backend and frontend separately for a better development experience: +Run the backend and frontend simultaneously with live reload (uses parallel tasks): ```bash -# Backend (with live reload using Air or just go run) -go run main.go - -# Frontend (Vite dev server) -cd frontend -pnpm dev +go-task dev ``` ## Testing -We maintain a high test coverage (>60%). Run the test suite: +Run the full test suite (including WASM tests): ```bash -go test -v -coverpkg=./... ./... +go-task test ``` ## License diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..df985d2 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,159 @@ +version: '3' + +vars: + BINARY_NAME: software-station + FRONTEND_DIR: frontend + BUILD_DIR: build + VERIFIER_DIR: software-verifier + WASM_OUT: frontend/static/verifier + VERSION: + sh: grep '"version":' frontend/package.json | cut -d'"' -f4 + BUILD_DATE: + sh: date -u +'%Y-%m-%dT%H:%M:%SZ' + VCS_REF: + sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown" + +tasks: + default: + desc: Build everything + cmds: + - task: all + + all: + desc: Build everything + deps: [build-go, build-frontend] + + dev: + desc: Start development environment (parallel) + deps: [build-wasm] + cmds: + - task: dev-frontend + - task: dev-backend + parallel: true + + dev-frontend: + internal: true + dir: "{{.FRONTEND_DIR}}" + cmds: + - pnpm dev + + dev-backend: + internal: true + cmds: + - go run main.go + + preview: + desc: Preview the production build + dir: "{{.FRONTEND_DIR}}" + cmds: + - pnpm preview + + build-wasm: + desc: Build WASM verifier + sources: + - "{{.VERIFIER_DIR}}/**/*.go" + generates: + - "{{.WASM_OUT}}/verifier.wasm" + - "{{.WASM_OUT}}/wasm_exec.js" + cmds: + - mkdir -p {{.WASM_OUT}} + - cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" {{.WASM_OUT}}/wasm_exec.js + - cd {{.VERIFIER_DIR}} && GOOS=js GOARCH=wasm go build -o ../{{.WASM_OUT}}/verifier.wasm . + - go run scripts/sri-gen/main.go frontend/src/lib/verifier.ts + silent: true + + build-frontend: + desc: Build Svelte frontend + deps: [build-wasm] + sources: + - "{{.FRONTEND_DIR}}/**/*" + - exclude: "{{.FRONTEND_DIR}}/node_modules/**/*" + - exclude: "{{.FRONTEND_DIR}}/build/**/*" + generates: + - "{{.FRONTEND_DIR}}/build/**/*" + cmds: + - cd {{.FRONTEND_DIR}} && pnpm install && pnpm build + - go run scripts/sri-gen/main.go + + build-go: + desc: Build main Go application + sources: + - "**/*.go" + - exclude: "{{.VERIFIER_DIR}}/**/*" + - exclude: "scripts/**/*" + generates: + - "{{.BINARY_NAME}}" + cmds: + - go build -o {{.BINARY_NAME}} main.go + + release: + desc: Build release binary + deps: [build-frontend] + cmds: + - CGO_ENABLED=0 go build -ldflags="-s -w" -o {{.BINARY_NAME}} main.go + + run: + desc: Run the application + deps: [all] + cmds: + - ./{{.BINARY_NAME}} + + format: + desc: Format code + cmds: + - go fmt ./... + - cd {{.FRONTEND_DIR}} && pnpm run format + + lint: + desc: Lint code + cmds: + - go vet ./... + - cd {{.FRONTEND_DIR}} && pnpm run lint + + scan: + desc: Security scan + cmds: + - gosec ./... + + check: + desc: Type check frontend + cmds: + - cd {{.FRONTEND_DIR}} && pnpm run check + + tidy: + desc: Run format, lint, and check + cmds: + - task: format + - task: lint + - task: check + + test: + desc: Run tests + deps: [test-wasm] + cmds: + - go test -v -coverpkg=./... ./... + + test-wasm: + desc: Run WASM tests + dir: "{{.VERIFIER_DIR}}" + cmds: + - go test -v ./... + + clean: + desc: Clean build artifacts + cmds: + - rm -rf {{.FRONTEND_DIR}}/build + - rm -rf {{.WASM_OUT}} + - rm -f {{.BINARY_NAME}} + - rm -f coverage.out + + docker-build: + desc: Build Docker image + cmds: + - | + docker build \ + --build-arg VERSION={{.VERSION}} \ + --build-arg BUILD_DATE={{.BUILD_DATE}} \ + --build-arg VCS_REF={{.VCS_REF}} \ + -t {{.BINARY_NAME}}:latest \ + -t {{.BINARY_NAME}}:{{.VERSION}} . diff --git a/frontend/.gitignore b/frontend/.gitignore index 3b462cb..0524b21 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,3 +21,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +.taskfile.env +.task \ No newline at end of file diff --git a/frontend/src/lib/verifier.ts b/frontend/src/lib/verifier.ts index 78b2647..d5b006c 100644 --- a/frontend/src/lib/verifier.ts +++ b/frontend/src/lib/verifier.ts @@ -19,7 +19,10 @@ export async function loadVerifier() { const go = new (window as any).Go(); const result = await WebAssembly.instantiateStreaming( - fetch('/verifier/verifier.wasm'), + fetch('/verifier/verifier.wasm', { + integrity: 'sha384-fDQVhNAuumlwh5lh1AT6LiSLer1EQYa1G8TEJLCZvKXeUxYi2gn3QoI5YdNFtKW0', + crossOrigin: 'anonymous' + }), go.importObject ); go.run(result.instance); diff --git a/frontend/static/verifier/verifier.wasm b/frontend/static/verifier/verifier.wasm index 0f3421a..61bbbea 100755 Binary files a/frontend/static/verifier/verifier.wasm and b/frontend/static/verifier/verifier.wasm differ diff --git a/frontend/static/verifier/wasm_exec.js b/frontend/static/verifier/wasm_exec.js index ec6a143..d71af9e 100644 --- a/frontend/static/verifier/wasm_exec.js +++ b/frontend/static/verifier/wasm_exec.js @@ -2,30 +2,22 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -'use strict'; +"use strict"; (() => { const enosys = () => { - const err = new Error('not implemented'); - err.code = 'ENOSYS'; + const err = new Error("not implemented"); + err.code = "ENOSYS"; return err; }; if (!globalThis.fs) { - let outputBuf = ''; + 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 + 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'); + const nl = outputBuf.lastIndexOf("\n"); if (nl != -1) { console.log(outputBuf.substring(0, nl)); outputBuf = outputBuf.substring(nl + 1); @@ -40,147 +32,81 @@ 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()); - }, + 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(); - }, + 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(); - }, - }; + umask() { throw enosys(); }, + cwd() { throw enosys(); }, + chdir() { throw enosys(); }, + } } if (!globalThis.path) { globalThis.path = { resolve(...pathSegments) { - return pathSegments.join('/'); - }, - }; + return pathSegments.join("/"); + } + } } if (!globalThis.crypto) { - throw new Error( - 'globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)' - ); + 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)' - ); + 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'); + throw new Error("globalThis.TextEncoder is not available, polyfill required"); } if (!globalThis.TextDecoder) { - throw new Error('globalThis.TextDecoder is not available, polyfill required'); + throw new Error("globalThis.TextDecoder is not available, polyfill required"); } - const encoder = new TextEncoder('utf-8'); - const decoder = new TextDecoder('utf-8'); + const encoder = new TextEncoder("utf-8"); + const decoder = new TextDecoder("utf-8"); globalThis.Go = class { constructor() { - this.argv = ['js']; + this.argv = ["js"]; this.env = {}; this.exit = (code) => { if (code !== 0) { - console.warn('exit code:', code); + console.warn("exit code:", code); } }; this._exitPromise = new Promise((resolve) => { @@ -193,17 +119,17 @@ const setInt64 = (addr, v) => { this.mem.setUint32(addr + 0, v, true); this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); - }; + } const setInt32 = (addr, v) => { this.mem.setUint32(addr + 0, v, true); - }; + } const getInt64 = (addr) => { const low = this.mem.getUint32(addr + 0, true); const high = this.mem.getInt32(addr + 4, true); return low + high * 4294967296; - }; + } const loadValue = (addr) => { const f = this.mem.getFloat64(addr, true); @@ -216,12 +142,12 @@ const id = this.mem.getUint32(addr, true); return this._values[id]; - }; + } const storeValue = (addr, v) => { - const nanHead = 0x7ff80000; + const nanHead = 0x7FF80000; - if (typeof v === 'number' && v !== 0) { + if (typeof v === "number" && v !== 0) { if (isNaN(v)) { this.mem.setUint32(addr + 4, nanHead, true); this.mem.setUint32(addr, 0, true); @@ -249,30 +175,30 @@ this._goRefCounts[id]++; let typeFlag = 0; switch (typeof v) { - case 'object': + case "object": if (v !== null) { typeFlag = 1; } break; - case 'string': + case "string": typeFlag = 2; break; - case 'symbol': + case "symbol": typeFlag = 3; break; - case 'function': + 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); @@ -282,18 +208,18 @@ 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 = { @@ -308,7 +234,7 @@ // This changes the SP, thus we have to update the SP used by the imported function. // func wasmExit(code int32) - 'runtime.wasmExit': (sp) => { + "runtime.wasmExit": (sp) => { sp >>>= 0; const code = this.mem.getInt32(sp + 8, true); this.exited = true; @@ -321,7 +247,7 @@ }, // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) - 'runtime.wasmWrite': (sp) => { + "runtime.wasmWrite": (sp) => { sp >>>= 0; const fd = getInt64(sp + 8); const p = getInt64(sp + 16); @@ -330,50 +256,47 @@ }, // func resetMemoryDataView() - 'runtime.resetMemoryDataView': (sp) => { + "runtime.resetMemoryDataView": (sp) => { sp >>>= 0; this.mem = new DataView(this._inst.exports.mem.buffer); }, // func nanotime1() int64 - 'runtime.nanotime1': (sp) => { + "runtime.nanotime1": (sp) => { sp >>>= 0; setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); }, // func walltime() (sec int64, nsec int32) - 'runtime.walltime': (sp) => { + "runtime.walltime": (sp) => { sp >>>= 0; - const msec = new Date().getTime(); + const msec = (new Date).getTime(); setInt64(sp + 8, msec / 1000); this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); }, // func scheduleTimeoutEvent(delay int64) int32 - 'runtime.scheduleTimeoutEvent': (sp) => { + "runtime.scheduleTimeoutEvent": (sp) => { sp >>>= 0; const id = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++; - this._scheduledTimeouts.set( - id, - setTimeout( - () => { + 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(); - 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) - ) - ); + } + }, + getInt64(sp + 8), + )); this.mem.setInt32(sp + 16, id, true); }, // func clearTimeoutEvent(id int32) - 'runtime.clearTimeoutEvent': (sp) => { + "runtime.clearTimeoutEvent": (sp) => { sp >>>= 0; const id = this.mem.getInt32(sp + 8, true); clearTimeout(this._scheduledTimeouts.get(id)); @@ -381,13 +304,13 @@ }, // func getRandomData(r []byte) - 'runtime.getRandomData': (sp) => { + "runtime.getRandomData": (sp) => { sp >>>= 0; crypto.getRandomValues(loadSlice(sp + 8)); }, // func finalizeRef(v ref) - 'syscall/js.finalizeRef': (sp) => { + "syscall/js.finalizeRef": (sp) => { sp >>>= 0; const id = this.mem.getUint32(sp + 8, true); this._goRefCounts[id]--; @@ -400,13 +323,13 @@ }, // func stringVal(value string) ref - 'syscall/js.stringVal': (sp) => { + "syscall/js.stringVal": (sp) => { sp >>>= 0; storeValue(sp + 24, loadString(sp + 8)); }, // func valueGet(v ref, p string) ref - 'syscall/js.valueGet': (sp) => { + "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 @@ -414,31 +337,31 @@ }, // func valueSet(v ref, p string, x ref) - 'syscall/js.valueSet': (sp) => { + "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) => { + "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) => { + "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) => { + "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) => { + "syscall/js.valueCall": (sp) => { sp >>>= 0; try { const v = loadValue(sp + 8); @@ -456,7 +379,7 @@ }, // func valueInvoke(v ref, args []ref) (ref, bool) - 'syscall/js.valueInvoke': (sp) => { + "syscall/js.valueInvoke": (sp) => { sp >>>= 0; try { const v = loadValue(sp + 8); @@ -473,7 +396,7 @@ }, // func valueNew(v ref, args []ref) (ref, bool) - 'syscall/js.valueNew': (sp) => { + "syscall/js.valueNew": (sp) => { sp >>>= 0; try { const v = loadValue(sp + 8); @@ -490,13 +413,13 @@ }, // func valueLength(v ref) int - 'syscall/js.valueLength': (sp) => { + "syscall/js.valueLength": (sp) => { sp >>>= 0; setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); }, // valuePrepareString(v ref) (ref, int) - 'syscall/js.valuePrepareString': (sp) => { + "syscall/js.valuePrepareString": (sp) => { sp >>>= 0; const str = encoder.encode(String(loadValue(sp + 8))); storeValue(sp + 16, str); @@ -504,20 +427,20 @@ }, // valueLoadString(v ref, b []byte) - 'syscall/js.valueLoadString': (sp) => { + "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) => { + "syscall/js.valueInstanceOf": (sp) => { sp >>>= 0; - this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 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) => { + "syscall/js.copyBytesToGo": (sp) => { sp >>>= 0; const dst = loadSlice(sp + 8); const src = loadValue(sp + 32); @@ -532,7 +455,7 @@ }, // func copyBytesToJS(dst ref, src []byte) (int, bool) - 'syscall/js.copyBytesToJS': (sp) => { + "syscall/js.copyBytesToJS": (sp) => { sp >>>= 0; const dst = loadValue(sp + 8); const src = loadSlice(sp + 16); @@ -546,21 +469,20 @@ this.mem.setUint8(sp + 48, 1); }, - debug: (value) => { + "debug": (value) => { console.log(value); }, - }, + } }; } async run(instance) { if (!(instance instanceof WebAssembly.Instance)) { - throw new Error('Go.run: WebAssembly.Instance expected'); + throw new Error("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 + this._values = [ // JS values that Go currently has references to, indexed by reference id NaN, 0, null, @@ -570,8 +492,7 @@ this, ]; this._goRefCounts = new Array(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 + this._ids = new Map([ // mapping from JS values to reference ids [0, 1], [null, 2], [true, 3], @@ -579,7 +500,7 @@ [globalThis, 5], [this, 6], ]); - this._idPool = []; // unused ids that have been garbage collected + 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. @@ -587,7 +508,7 @@ const strPtr = (str) => { const ptr = offset; - const bytes = encoder.encode(str + '\0'); + const bytes = encoder.encode(str + "\0"); new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); offset += bytes.length; if (offset % 8 !== 0) { @@ -621,7 +542,7 @@ // 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'); + throw new Error("total length of command line and environment variables exceeds limit"); } this._inst.exports.run(argc, argv); @@ -633,7 +554,7 @@ _resume() { if (this.exited) { - throw new Error('Go program has already exited'); + throw new Error("Go program has already exited"); } this._inst.exports.resume(); if (this.exited) { @@ -650,5 +571,5 @@ return event.result; }; } - }; + } })(); diff --git a/scripts/sri-gen/main.go b/scripts/sri-gen/main.go index 8c1df5f..0c9c929 100644 --- a/scripts/sri-gen/main.go +++ b/scripts/sri-gen/main.go @@ -12,23 +12,34 @@ import ( ) func main() { - buildDir := "frontend/build" + // Default behavior: process HTML in frontend/build + target := "frontend/build" if len(os.Args) > 1 { - buildDir = os.Args[1] + target = os.Args[1] } - fmt.Printf("Generating SRI hashes for assets in %s...\n", buildDir) + info, err := os.Stat(target) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } - err := filepath.Walk(buildDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if !info.IsDir() && strings.HasSuffix(path, ".html") { - return processHTML(path, buildDir) - } - return nil - }) + if info.IsDir() { + fmt.Printf("Generating SRI hashes for assets in %s...\n", target) + err = filepath.Walk(target, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() && strings.HasSuffix(path, ".html") { + return processHTML(path, target) + } + return nil + }) + } else { + // If a single file is provided (like verifier.ts), process it specially + fmt.Printf("Updating SRI hashes in %s...\n", target) + err = processSourceFile(target) + } if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -37,16 +48,12 @@ func main() { } func processHTML(htmlPath, buildDir string) error { - content, err := os.ReadFile(filepath.Clean(htmlPath)) // #nosec G304 + content, err := os.ReadFile(filepath.Clean(htmlPath)) if err != nil { return err } updated := string(content) - - // Regex to find script and link tags that don't have integrity yet - // Matches: