import assert from 'node:assert/strict' import { createHash } from 'node:crypto' import fs from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import { createRequire } from 'node:module' import { pathToFileURL } from 'node:url' const runtimeRoot = path.resolve(process.env.REA_RUNTIME_ROOT || '.tmp/rea-css-runtime') process.env.REA_RUNTIME_ROOT = runtimeRoot const requireFromRuntime = createRequire(path.join(runtimeRoot, 'package.json')) const asarModule = await import(pathToFileURL(requireFromRuntime.resolve('@electron/asar')).href) const { createPackageWithOptions, getRawHeader, statFile } = asarModule const { exportSelectedArtifactText } = await import( pathToFileURL(path.resolve('scripts/rea/patches/SelectedArtifactText.js')).href, ) const digest = (bytes) => createHash('sha256').update(bytes).digest('hex') const archiveSha256 = async (archivePath) => digest(await fs.readFile(archivePath)) const asarPath = (relativePath) => path.join(...relativePath.split('/')) const temporaryRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'rea-selected-text-')) async function makeArchive(label, files, options = {}) { const source = path.join(temporaryRoot, `${label}-source`) const archivePath = path.join(temporaryRoot, `${label}.asar`) for (const [relativePath, contents] of Object.entries(files)) { const filePath = path.join(source, ...relativePath.split('/')) await fs.mkdir(path.dirname(filePath), { recursive: true }) await fs.writeFile(filePath, contents) } await createPackageWithOptions(source, archivePath, options) return archivePath } async function expectRejected(operation, pattern) { await assert.rejects(operation, pattern) } try { const binaryBytes = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10, 255, 0, 128]) const fontBytes = Buffer.from([0, 1, 0, 0, 0, 1, 128, 0, 0, 0, 255, 0]) const binaryArchive = await makeArchive('selected-binary', { 'renderer/robot.png': binaryBytes, 'renderer/codicon.ttf': fontBytes, 'renderer/other.png': Buffer.from('UNSELECTED') }) const binaryRootSha = await archiveSha256(binaryArchive) const binary = await exportSelectedArtifactText(binaryArchive, binaryRootSha, ['renderer/robot.png'], true) assert.equal(binary.operation, 'export_selected_asar_artifact_binary') assert.equal(binary.normalized_result.files.length, 1) assert.deepEqual(Buffer.from(binary.normalized_result.files[0].data_base64, 'base64'), binaryBytes) assert.equal(binary.normalized_result.files[0].sha256, digest(binaryBytes)) const font = await exportSelectedArtifactText(binaryArchive, binaryRootSha, ['renderer/codicon.ttf'], true) assert.deepEqual(Buffer.from(font.normalized_result.files[0].data_base64, 'base64'), fontBytes) assert.equal(font.normalized_result.files[0].sha256, digest(fontBytes)) await expectRejected(() => exportSelectedArtifactText(binaryArchive, binaryRootSha, ['renderer/codicon.ttf']), /must name/) await expectRejected(() => exportSelectedArtifactText(binaryArchive, binaryRootSha, ['renderer/robot.png']), /must name/) await expectRejected(() => exportSelectedArtifactText(binaryArchive, binaryRootSha, ['../renderer/robot.png'], true), /not canonical/) await expectRejected(() => exportSelectedArtifactText(binaryArchive, binaryRootSha, ['renderer/secret.db'], true), /supported image or font/) const selectedPath = 'renderer/assets/selected.js' const selectedText = 'export const selected = "ok";\n' const archivePath = await makeArchive( 'missing-unpacked', { [selectedPath]: selectedText, 'renderer/assets/missing.js': 'export const absent = true;\n', 'renderer/assets/theme.css': '.fixture { color: red; }\n', }, { unpack: '**/missing.js' }, ) const missingMetadata = statFile(archivePath, asarPath('renderer/assets/missing.js')) assert.equal(missingMetadata.unpacked, true, 'fixture member should be marked unpacked') const missingCompanion = path.join( `${archivePath}.unpacked`, 'renderer', 'assets', 'missing.js', ) await fs.rm(missingCompanion) const rootSha256 = await archiveSha256(archivePath) const evidence = await exportSelectedArtifactText(archivePath, rootSha256, [selectedPath]) assert.equal(evidence.authority, 'shipped-artifact') assert.equal(evidence.subject.digest.sha256, rootSha256) assert.equal(evidence.normalized_result.root_sha256, rootSha256) assert.deepEqual(evidence.normalized_result.scope.selected_paths, [selectedPath]) assert.match(evidence.normalized_result.scope.unselected_members, /availability are unknown/u) assert.deepEqual(evidence.normalized_result.files.map((file) => file.path), [selectedPath]) assert.equal(evidence.normalized_result.files[0].text, selectedText) await expectRejected( () => exportSelectedArtifactText(archivePath, '0'.repeat(64), [selectedPath]), /changed before interpretation/u, ) await expectRejected( () => exportSelectedArtifactText(archivePath, rootSha256, ['../renderer/assets/selected.js']), /not canonical/u, ) await expectRejected( () => exportSelectedArtifactText(archivePath, rootSha256, ['renderer\\assets\\selected.js']), /not canonical/u, ) await expectRejected( () => exportSelectedArtifactText(archivePath, rootSha256, ['renderer/assets/missing.db']), /must name a supported UTF-8 text file/u, ) const hashMismatchArchive = path.join(temporaryRoot, 'hash-mismatch.asar') await fs.copyFile(archivePath, hashMismatchArchive) const packedEntry = statFile(hashMismatchArchive, asarPath(selectedPath)) const header = getRawHeader(hashMismatchArchive) const payloadOffset = 8 + header.headerSize + Number(packedEntry.offset) const handle = await fs.open(hashMismatchArchive, 'r+') try { await handle.write(Buffer.from('x'), 0, 1, payloadOffset) } finally { await handle.close() } await expectRejected( async () => exportSelectedArtifactText(hashMismatchArchive, await archiveSha256(hashMismatchArchive), [selectedPath]), /SHA-256 mismatch/u, ) const perFileBudgetArchive = await makeArchive('per-file-budget', { 'renderer/assets/large.js': Buffer.alloc(8 * 1024 * 1024 + 1, 0x61), }) await expectRejected( async () => exportSelectedArtifactText(perFileBudgetArchive, await archiveSha256(perFileBudgetArchive), ['renderer/assets/large.js']), /per-file budget/u, ) const aggregateBudgetArchive = await makeArchive('aggregate-budget', { 'renderer/assets/one.js': Buffer.alloc(8 * 1024 * 1024, 0x61), 'renderer/assets/two.js': Buffer.alloc(8 * 1024 * 1024, 0x62), 'renderer/assets/three.js': Buffer.from('x'), }) await expectRejected( async () => exportSelectedArtifactText( aggregateBudgetArchive, await archiveSha256(aggregateBudgetArchive), ['renderer/assets/one.js', 'renderer/assets/two.js', 'renderer/assets/three.js'], ), /aggregate budget/u, ) const invalidUtf8Archive = await makeArchive('invalid-utf8', { 'renderer/assets/invalid.json': Buffer.from([0xc3, 0x28]), }) await expectRejected( async () => exportSelectedArtifactText(invalidUtf8Archive, await archiveSha256(invalidUtf8Archive), ['renderer/assets/invalid.json']), /strict UTF-8/u, ) console.log('Selected artifact text regression passed: root pinning, exact canonical selection, bounded strict UTF-8 reads, declared SHA verification, and ignored missing unselected unpacked members.') } finally { await fs.rm(temporaryRoot, { recursive: true, force: true }) }