import { createHash } from 'node:crypto' import path from 'node:path' import { pathToFileURL } from 'node:url' const MAX_SELECTED_FILES = 16 const MAX_FILE_BYTES = 8 * 1024 * 1024 const MAX_TOTAL_BYTES = 16 * 1024 * 1024 const SHA256_PATTERN = /^[a-f0-9]{64}$/u function assertCanonicalSelectedPath(value, binary = false) { if (typeof value !== 'string' || value.length === 0) { throw new TypeError('Selected paths must be non-empty relative strings') } if ( value.startsWith('/') || value.endsWith('/') || value.includes('\\') || value.includes('\0') || /^[a-zA-Z]:/u.test(value) || value.split('/').some((part) => part === '' || part === '.' || part === '..') || path.posix.normalize(value) !== value ) { throw new TypeError(`Selected path is not canonical: ${value}`) } if (binary ? !/\.(?:png|jpe?g|gif|webp|svg|woff2?|ttf)$/u.test(value) : !/\.(?:js|css|json|md|txt|yaml|yml|py)$/u.test(value)) { if (binary) throw new TypeError(`Selected binary path must name a supported image or font: ${value}`) throw new TypeError(`Selected path must name a supported UTF-8 text file (.js/.css/.json/.md/.txt/.yaml/.yml/.py): ${value}`) } } function validateSelection(expectedRootSha256, selectedPaths, binary = false) { if (typeof expectedRootSha256 !== 'string' || !SHA256_PATTERN.test(expectedRootSha256)) { throw new TypeError('Expected ASAR root SHA-256 must be 64 lowercase hexadecimal characters') } if (!Array.isArray(selectedPaths) || selectedPaths.length === 0 || selectedPaths.length > MAX_SELECTED_FILES) { throw new RangeError(`Select between 1 and ${MAX_SELECTED_FILES} exact files`) } const paths = [...selectedPaths] const seen = new Set() for (const selectedPath of paths) { assertCanonicalSelectedPath(selectedPath, binary) if (seen.has(selectedPath)) throw new TypeError(`Duplicate selected path: ${selectedPath}`) seen.add(selectedPath) } return paths } async function loadReaRuntime() { const runtimeRoot = path.resolve(process.env.REA_RUNTIME_ROOT || '.tmp/rea-css-runtime') const runtimeUrl = (relativePath) => pathToFileURL(path.join(runtimeRoot, relativePath)).href const [readerModule, evidenceModule] = await Promise.all([ import(runtimeUrl('dist/artifacts/AsarArtifactReader.js')), import(runtimeUrl('dist/domain/evidence.js')), ]) const { createRequire } = await import('node:module') const requireFromRuntime = createRequire(path.join(runtimeRoot, 'package.json')) const packageMetadata = requireFromRuntime('./package.json') return { AsarArtifactReader: readerModule.AsarArtifactReader, createEvidence: evidenceModule.createEvidence, version: packageMetadata.version, } } async function readSelectedEntry(reader, entry, totalBytes, binary = false) { if (entry.kind !== 'file') throw new TypeError(`Selected path is not a regular file: ${entry.path}`) if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) { throw new Error(`Selected file has no valid declared size: ${entry.path}`) } if (entry.declaredSize > MAX_FILE_BYTES) { throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${entry.path}`) } if (typeof entry.declaredSha256 !== 'string' || !SHA256_PATTERN.test(entry.declaredSha256)) { throw new Error(`Selected file has no valid declared SHA-256: ${entry.path}`) } const chunks = [] const digest = createHash('sha256') let bytes = 0 const stream = await reader.open(entry) for await (const value of stream) { if (!(Buffer.isBuffer(value) || value instanceof Uint8Array)) { throw new TypeError(`Selected file stream returned non-byte data: ${entry.path}`) } const chunk = Buffer.from(value) bytes += chunk.length if (bytes > MAX_FILE_BYTES || bytes > entry.declaredSize || totalBytes + bytes > MAX_TOTAL_BYTES) { throw new RangeError(`Selected text exceeds its declared size or aggregate budget: ${entry.path}`) } digest.update(chunk) chunks.push(chunk) } if (bytes !== entry.declaredSize) { throw new Error(`Selected file size mismatch for ${entry.path}: declared ${entry.declaredSize}, read ${bytes}`) } const actualSha256 = digest.digest('hex') if (actualSha256 !== entry.declaredSha256) { throw new Error(`Selected file SHA-256 mismatch for ${entry.path}`) } if (binary) return { path: entry.path, size_bytes: bytes, sha256: actualSha256, unpacked: entry.unpacked, encoding: 'base64', data_base64: Buffer.concat(chunks, bytes).toString('base64') } let text try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(chunks, bytes)) } catch (cause) { throw new Error(`Selected file is not strict UTF-8: ${entry.path}`, { cause }) } return { path: entry.path, size_bytes: bytes, sha256: actualSha256, unpacked: entry.unpacked, text, } } /** Export bounded UTF-8 text from exact selected files in a root-pinned ASAR. */ export async function exportSelectedArtifactText(inputPath, expectedRootSha256, selectedPaths, binary = false) { if (typeof inputPath !== 'string' || inputPath.length === 0) { throw new TypeError('ASAR input path must be a non-empty string') } const paths = validateSelection(expectedRootSha256, selectedPaths, binary) const { AsarArtifactReader, createEvidence, version } = await loadReaRuntime() const reader = new AsarArtifactReader(inputPath) let primaryFailure let primaryFailed = false let evidence try { await reader.prepareContainer(expectedRootSha256) const wanted = new Set(paths) const entries = new Map() for await (const entry of reader.entries()) { if (!wanted.has(entry.path)) continue if (entries.has(entry.path)) throw new Error(`ASAR inventory contains a duplicate path: ${entry.path}`) entries.set(entry.path, entry) } for (const selectedPath of paths) { if (!entries.has(selectedPath)) throw new Error(`Selected path is absent from the ASAR inventory: ${selectedPath}`) } let declaredTotalBytes = 0 for (const selectedPath of paths) { const entry = entries.get(selectedPath) if (!Number.isSafeInteger(entry.declaredSize) || entry.declaredSize < 0) { throw new Error(`Selected file has no valid declared size: ${selectedPath}`) } if (entry.declaredSize > MAX_FILE_BYTES) { throw new RangeError(`Selected file exceeds the ${MAX_FILE_BYTES}-byte per-file budget: ${selectedPath}`) } declaredTotalBytes += entry.declaredSize if (declaredTotalBytes > MAX_TOTAL_BYTES) { throw new RangeError(`Selected files exceed the ${MAX_TOTAL_BYTES}-byte aggregate budget`) } } const files = [] let actualTotalBytes = 0 for (const selectedPath of paths) { const file = await readSelectedEntry(reader, entries.get(selectedPath), actualTotalBytes, binary) actualTotalBytes += file.size_bytes files.push(file) } const unselectedScope = 'Not read; unpacked payload content and availability are unknown.' evidence = createEvidence( { path: inputPath, sha256: expectedRootSha256, format: 'asar' }, { id: binary ? 'rea-selected-asar-binary' : 'rea-selected-asar-text', name: binary ? 'REA selected ASAR binary adapter' : 'REA selected ASAR text adapter', version, }, { predicateType: binary ? 'rea.artifact.selected-binary' : 'rea.artifact.selected-text', operation: binary ? 'export_selected_asar_artifact_binary' : 'export_selected_asar_artifact_text', parameters: { expected_root_sha256: expectedRootSha256, selected_paths: paths, max_file_bytes: MAX_FILE_BYTES, max_total_bytes: MAX_TOTAL_BYTES, }, result: { root_sha256: expectedRootSha256, scope: { kind: 'selected-files-only', selected_paths: paths, unselected_members: unselectedScope, }, files, }, confidence: 'observed', authority: 'shipped-artifact', limitations: [ 'Only selected file streams were read and validated.', `Unselected ASAR members were not opened; ${unselectedScope}`, ], locations: paths.map((selectedPath) => ({ kind: 'artifact-path', path: selectedPath })), }, ) } catch (error) { primaryFailure = error primaryFailed = true } try { await reader.close() } catch (cleanupFailure) { if (primaryFailed) { throw new AggregateError( [primaryFailure, cleanupFailure], 'Selected ASAR text export failed and reader cleanup also failed', { cause: primaryFailure }, ) } throw cleanupFailure } if (primaryFailed) throw primaryFailure return evidence }