Files
ycode/scripts/rea/selected-artifact-text-regression.mjs
T

155 lines
7.3 KiB
JavaScript

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 })
}