/** * Vendors the pinned OMP kernel binary and its license files into resources/omp. * * The binary is not committed to Git. `npm run omp:vendor` restores it from the * pinned upstream release (or from a local copy via `--from `) * and verifies every asset against resources/omp/runtime.lock.json. */ import { createHash } from 'node:crypto' import { createReadStream, createWriteStream, promises as fs } from 'node:fs' import path from 'node:path' import { Readable } from 'node:stream' import { pipeline } from 'node:stream/promises' import { fileURLToPath } from 'node:url' const project = fileURLToPath(new URL('../', import.meta.url)) const directory = path.join(project, 'resources', 'omp') const lockPath = path.join(directory, 'runtime.lock.json') const hashPattern = /^[a-f0-9]{64}$/ const namePattern = /^[A-Za-z0-9._-]+$/ const releasePrefix = 'https://github.com/can1357/oh-my-pi/releases/download/' const rawPrefix = 'https://raw.githubusercontent.com/can1357/oh-my-pi/' const maxAssetBytes = 512 * 1024 * 1024 function asset(value, label) { if (!value || typeof value !== 'object' || typeof value.name !== 'string' || !namePattern.test(value.name) || !Number.isSafeInteger(value.bytes) || value.bytes <= 0 || value.bytes > maxAssetBytes || typeof value.sha256 !== 'string' || !hashPattern.test(value.sha256)) { throw new Error(`Invalid ${label} entry in runtime.lock.json`) } return value } async function readLock() { const lock = JSON.parse((await fs.readFile(lockPath, '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))) { throw new Error('Invalid OMP runtime lock') } const binary = asset(lock.binary, 'binary') const license = asset(lock.license, 'license') const thirdPartyNotices = asset(lock.thirdPartyNotices, 'thirdPartyNotices') if (binary.name !== 'omp.exe' || license.name !== 'LICENSE' || thirdPartyNotices.name !== 'THIRD-PARTY-NOTICES.txt') { throw new Error('OMP runtime lock uses unexpected asset names') } return { lock, binary, license, thirdPartyNotices } } async function sha256File(file) { const hash = createHash('sha256') for await (const chunk of createReadStream(file)) hash.update(chunk) return hash.digest('hex') } async function verifyFile(file, spec) { const stat = await fs.stat(file).catch(() => null) if (!stat?.isFile()) throw new Error(`Missing OMP asset: ${path.relative(project, file)}`) if (stat.size !== spec.bytes) throw new Error(`OMP asset size mismatch: ${spec.name} (${stat.size} != ${spec.bytes})`) const digest = await sha256File(file) if (digest !== spec.sha256) throw new Error(`OMP asset digest mismatch: ${spec.name}`) return digest } async function copyVerified(source, target, spec) { if (path.resolve(source) === path.resolve(target)) return verifyFile(target, spec) const stat = await fs.stat(source) const file = stat.isDirectory() ? path.join(source, spec.name) : source const sourceStat = await fs.stat(file) if (!sourceStat.isFile() || sourceStat.size !== spec.bytes) throw new Error(`Local OMP asset has an unexpected size: ${file}`) const digest = await sha256File(file) if (digest !== spec.sha256) throw new Error(`Local OMP asset does not match the pinned release: ${file}`) const temporary = `${target}.part` await fs.copyFile(file, temporary) try { await verifyFile(temporary, spec) await fs.rename(temporary, target) } catch (error) { await fs.rm(temporary, { force: true }) throw error } return digest } async function download(url, target, spec) { const response = await fetch(url, { redirect: 'follow', headers: { 'user-agent': 'YCode OMP vendor' }, signal: AbortSignal.timeout(10 * 60 * 1000) }) if (!response.ok || !response.body) throw new Error(`OMP download failed (${response.status}): ${url}`) const declared = response.headers.get('content-length') if (declared && Number(declared) !== spec.bytes) throw new Error(`OMP download size header mismatch: ${spec.name}`) const temporary = `${target}.part` try { await pipeline(Readable.fromWeb(response.body), createWriteStream(temporary)) const digest = await verifyFile(temporary, spec) await fs.rename(temporary, target) return digest } catch (error) { await fs.rm(temporary, { force: true }) throw error } } function releaseUrl(version, name) { const url = `${releasePrefix}v${version}/${name}` if (!url.startsWith(releasePrefix) || !namePattern.test(name)) throw new Error(`Unsafe OMP release URL: ${url}`) return url } function rawUrl(version, name) { const url = `${rawPrefix}v${version}/${name}` if (!url.startsWith(rawPrefix) || !namePattern.test(name)) throw new Error(`Unsafe OMP raw URL: ${url}`) return url } async function ensureAsset({ lock, spec, source, target, verifyOnly, offline }) { if (await fs.stat(target).catch(() => null)) { const digest = await verifyFile(target, spec) console.log(`[omp] ${spec.name} verified (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`) return } if (verifyOnly) throw new Error(`Missing OMP asset: ${path.relative(project, target)}`) if (source) { if (spec.name !== 'omp.exe') throw new Error(`--from only applies to omp.exe, not ${spec.name}`) const digest = await copyVerified(source, target, spec) console.log(`[omp] ${spec.name} imported from ${source} (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`) return } if (offline) throw new Error(`OMP asset is missing in offline mode: ${path.relative(project, target)}`) try { const digest = await download(releaseUrl(lock.version, spec.name), target, spec) console.log(`[omp] ${spec.name} downloaded (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`) } catch (releaseError) { if (spec.name === 'omp.exe') throw releaseError const digest = await download(rawUrl(lock.version, spec.name), target, spec) console.log(`[omp] ${spec.name} downloaded from raw source (${spec.bytes} bytes, ${digest.slice(0, 12)}…)`) } } async function main() { const args = process.argv.slice(2) const verifyOnly = args.includes('--verify') const offline = args.includes('--offline') const fromIndex = args.indexOf('--from') const source = fromIndex >= 0 ? args[fromIndex + 1] : null if (fromIndex >= 0 && (!source || source.startsWith('--'))) throw new Error('--from requires a file or directory') const { lock, binary, license, thirdPartyNotices } = await readLock() await fs.mkdir(directory, { recursive: true }) console.log(`[omp] pinned ${lock.upstream} v${lock.version} (${lock.platform})`) await ensureAsset({ lock, spec: binary, source, target: path.join(directory, binary.name), verifyOnly, offline }) await ensureAsset({ lock, spec: license, target: path.join(directory, license.name), verifyOnly, offline }) await ensureAsset({ lock, spec: thirdPartyNotices, target: path.join(directory, thirdPartyNotices.name), verifyOnly, offline }) console.log(`[omp] ready: ${path.relative(project, directory)}`) } main().catch((error) => { console.error(`[omp] ${error.message}`) process.exitCode = 1 })