/** * Verifies the staged (or packaged) OMP kernel assets: lock schema, sizes, * SHA-256 digests, and that the binary actually launches with the pinned version. * * Usage: * node scripts/verify-bundled-omp.cjs # resources/omp * node scripts/verify-bundled-omp.cjs --packaged # release/win-unpacked/resources/omp * node scripts/verify-bundled-omp.cjs */ const { createHash } = require('node:crypto') const { createReadStream, existsSync, promises: fs } = require('node:fs') const path = require('node:path') const { spawn } = require('node:child_process') const project = path.resolve(__dirname, '..') const hashPattern = /^[a-f0-9]{64}$/ const lockName = 'runtime.lock.json' const maxAssetBytes = 512 * 1024 * 1024 function fail(message) { throw new Error(message) } function asset(value, label) { if (!value || typeof value !== 'object' || typeof value.name !== 'string' || !/^[A-Za-z0-9._-]+$/.test(value.name) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || value.bytes > maxAssetBytes || typeof value.sha256 !== 'string' || !hashPattern.test(value.sha256)) { fail(`Invalid ${label} entry in ${lockName}`) } return value } async function sha256File(file) { const hash = createHash('sha256') for await (const chunk of createReadStream(file)) hash.update(chunk) return hash.digest('hex') } async function verifyAsset(directory, spec) { const file = path.join(directory, spec.name) const stat = await fs.stat(file).catch(() => null) if (!stat?.isFile()) fail(`Missing asset: ${file}`) if (stat.size !== spec.bytes) fail(`Size mismatch for ${spec.name}: ${stat.size} != ${spec.bytes}`) const digest = await sha256File(file) if (digest !== spec.sha256) fail(`SHA-256 mismatch for ${spec.name}`) console.log(`PASS ${spec.name} ${spec.bytes} bytes ${digest.slice(0, 12)}…`) } function readVersion(exe, timeoutMs = 30_000) { return new Promise((resolve, reject) => { const child = spawn(exe, ['--version'], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) let stdout = '' let stderr = '' const timer = setTimeout(() => { child.kill() reject(new Error(`Timed out launching ${exe}`)) }, timeoutMs) child.stdout.on('data', (chunk) => { stdout += chunk }) child.stderr.on('data', (chunk) => { stderr += chunk }) child.on('error', (error) => { clearTimeout(timer) reject(error) }) child.on('exit', (code) => { clearTimeout(timer) if (code !== 0) reject(new Error(`OMP exited with ${code}: ${stderr.trim()}`)) else resolve(stdout.trim()) }) }) } async function main() { const args = process.argv.slice(2) const packaged = args.includes('--packaged') const positional = args.find((arg) => !arg.startsWith('--')) const directory = positional ? path.resolve(positional) : packaged ? path.join(project, 'release', 'win-unpacked', 'resources', 'omp') : path.join(project, 'resources', 'omp') if (!existsSync(directory)) fail(`OMP directory does not exist: ${directory}`) const lock = JSON.parse((await fs.readFile(path.join(directory, lockName), 'utf8')).replace(/^\uFEFF/, '')) if (lock.format !== 1 || lock.kernel !== 'omp' || lock.upstream !== 'oh-my-pi' || lock.platform !== 'win32-x64' || typeof lock.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(lock.version) || lock.releaseUrl !== `https://github.com/can1357/oh-my-pi/releases/tag/v${lock.version}` || !Number.isFinite(Date.parse(lock.publishedAt))) { fail(`Invalid ${lockName}`) } const binary = asset(lock.binary, 'binary') const license = asset(lock.license, 'license') const notices = asset(lock.thirdPartyNotices, 'thirdPartyNotices') if (binary.name !== 'omp.exe' || license.name !== 'LICENSE' || notices.name !== 'THIRD-PARTY-NOTICES.txt') { fail(`Unexpected asset names in ${lockName}`) } console.log(`Checking OMP ${lock.version} in ${directory}`) await verifyAsset(directory, binary) await verifyAsset(directory, license) await verifyAsset(directory, notices) const output = await readVersion(path.join(directory, binary.name)) if (output !== `omp/${lock.version}`) fail(`Unexpected OMP version output: ${output}`) console.log(`PASS omp.exe --version => ${output}`) console.log(`OMP ${lock.version} bundle is valid.`) } main().catch((error) => { console.error(`FAIL ${error.message}`) process.exitCode = 1 })