import { parentPort } from 'worker_threads'; // This worker is unpacked from app.asar so Node can execute it directly. // Use direct CommonJS imports to keep TypeScript from emitting tslib helpers, // which cannot be resolved across the app.asar/app.asar.unpacked boundary. const fs = require('fs') as typeof import('fs'); const nodeCrypto = require('crypto') as typeof import('crypto'); const path = require('path') as typeof import('path'); export type ReticulumResourceWorkerTask = | { id: number; kind: 'finalize_resource'; sourcePath: string; destinationPath: string; expectedHash: string; expectedSize: number; moveSource?: boolean; } | { id: number; kind: 'delete_paths'; paths: string[]; } | { id: number; kind: 'hash_file'; path: string; } | { id: number; kind: 'read_and_hash_file'; path: string; } | { id: number; kind: 'write_range_file'; sourcePath: string; destinationPath: string; startByte: number; endByteExclusive: number; } | { id: number; kind: 'inspect_paths'; entries: Array<{ fileHash: string; assembledPath: string; partialPath: string; expectedSize: number; expectedHash: string; expectComplete: boolean; }>; }; export type ReticulumResourceWorkerTaskInput = | Omit, 'id'> | Omit, 'id'> | Omit, 'id'> | Omit, 'id'> | Omit, 'id'> | Omit, 'id'>; export type ReticulumResourceWorkerResult = | { id: number; kind: ReticulumResourceWorkerTask['kind']; ok: true; durationMs: number; hash?: string; deleted?: number; bytes?: Uint8Array; sizeBytes?: number; inspections?: Array<{ fileHash: string; assembledValid: boolean; partialExists: boolean; }>; } | { id: number; kind: ReticulumResourceWorkerTask['kind']; ok: false; durationMs: number; error: string; }; export function hashReticulumResourceFile(filePath: string): string { const hash = nodeCrypto.createHash('sha256'); const fd = fs.openSync(filePath, 'r'); const buffer = Buffer.allocUnsafe(1024 * 1024); try { for (;;) { const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null); if (bytesRead <= 0) break; hash.update(buffer.subarray(0, bytesRead)); } } finally { fs.closeSync(fd); } return hash.digest('hex'); } export function finalizeReticulumResource( task: Extract ): string { const stat = fs.statSync(task.sourcePath); if (!stat.isFile() || stat.size !== task.expectedSize) { throw new Error('Resource source size mismatch'); } const hash = hashReticulumResourceFile(task.sourcePath); if (hash !== task.expectedHash.toLowerCase()) { throw new Error('Assembled file hash mismatch'); } fs.mkdirSync(path.dirname(task.destinationPath), { recursive: true }); if (path.resolve(task.sourcePath) !== path.resolve(task.destinationPath)) { if (task.moveSource) { try { fs.renameSync(task.sourcePath, task.destinationPath); } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code !== 'EEXIST' && code !== 'EPERM') throw error; fs.rmSync(task.destinationPath, { force: true }); fs.renameSync(task.sourcePath, task.destinationPath); } return hash; } const temporaryPath = `${task.destinationPath}.${process.pid}.${Date.now()}.tmp`; try { fs.copyFileSync(task.sourcePath, temporaryPath); fs.renameSync(temporaryPath, task.destinationPath); } finally { fs.rmSync(temporaryPath, { force: true }); } } return hash; } export function deleteReticulumResourcePaths(paths: string[]): number { let deleted = 0; for (const candidate of new Set(paths.filter(Boolean))) { if (!fs.existsSync(candidate)) continue; fs.rmSync(candidate, { recursive: true, force: true }); deleted += 1; } return deleted; } export function writeReticulumResourceRange( task: Extract ): { hash: string; sizeBytes: number } { const startByte = Math.floor(task.startByte); const endByteExclusive = Math.floor(task.endByteExclusive); if (startByte < 0 || endByteExclusive <= startByte) { throw new Error('Invalid resource byte range'); } const sourceStat = fs.statSync(task.sourcePath); if (!sourceStat.isFile() || endByteExclusive > sourceStat.size) { throw new Error('Resource byte range exceeds source'); } fs.mkdirSync(path.dirname(task.destinationPath), { recursive: true }); const source = fs.openSync(task.sourcePath, 'r'); const output = fs.openSync(task.destinationPath, 'w'); const sizeBytes = endByteExclusive - startByte; const buffer = Buffer.allocUnsafe(Math.min(1024 * 1024, sizeBytes)); const hash = nodeCrypto.createHash('sha256'); let remaining = sizeBytes; let offset = startByte; try { while (remaining > 0) { const readSize = Math.min(buffer.length, remaining); const bytesRead = fs.readSync(source, buffer, 0, readSize, offset); if (bytesRead <= 0) throw new Error('Unexpected EOF while reading resource range'); const slice = buffer.subarray(0, bytesRead); fs.writeSync(output, slice); hash.update(slice); remaining -= bytesRead; offset += bytesRead; } } catch (error) { fs.rmSync(task.destinationPath, { force: true }); throw error; } finally { fs.closeSync(source); fs.closeSync(output); } return { hash: hash.digest('hex'), sizeBytes }; } parentPort?.on('message', (task: ReticulumResourceWorkerTask) => { const startedAt = Date.now(); try { if (task.kind === 'hash_file') { parentPort?.postMessage({ id: task.id, kind: task.kind, ok: true, hash: hashReticulumResourceFile(task.path), durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult); return; } if (task.kind === 'read_and_hash_file') { const bytes = fs.readFileSync(task.path); const result = { id: task.id, kind: task.kind, ok: true, bytes, hash: nodeCrypto.createHash('sha256').update(bytes).digest('hex'), durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult; parentPort?.postMessage(result, [bytes.buffer as ArrayBuffer]); return; } if (task.kind === 'write_range_file') { const result = writeReticulumResourceRange(task); parentPort?.postMessage({ id: task.id, kind: task.kind, ok: true, hash: result.hash, sizeBytes: result.sizeBytes, durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult); return; } if (task.kind === 'delete_paths') { parentPort?.postMessage({ id: task.id, kind: task.kind, ok: true, deleted: deleteReticulumResourcePaths(task.paths), durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult); return; } if (task.kind === 'inspect_paths') { const inspections = task.entries.map((entry) => { let assembledValid = false; let partialExists = false; try { const stat = fs.statSync(entry.assembledPath); assembledValid = stat.isFile() && stat.size === entry.expectedSize && (entry.expectComplete || hashReticulumResourceFile(entry.assembledPath) === entry.expectedHash); } catch { assembledValid = false; } try { partialExists = fs.statSync(entry.partialPath).isFile(); } catch { partialExists = false; } return { fileHash: entry.fileHash, assembledValid, partialExists }; }); parentPort?.postMessage({ id: task.id, kind: task.kind, ok: true, inspections, durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult); return; } parentPort?.postMessage({ id: task.id, kind: task.kind, ok: true, hash: finalizeReticulumResource(task), durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult); } catch (error) { parentPort?.postMessage({ id: task.id, kind: task.kind, ok: false, error: error instanceof Error ? error.message : String(error), durationMs: Date.now() - startedAt, } satisfies ReticulumResourceWorkerResult); } });