import { promises as fs } from "node:fs"; import path from "node:path"; import { Unpacker } from "@suaveplan/packer"; import { SERVER_KEYS_CODES, ServerKeysError, } from "../../error-codes/error-codes.js"; import { checkSignal, loadFile } from "../keys/keys-io.js"; import type { FileCommandOptions, FilesLogger } from "./files.types.js"; import { deserializePacket, hexToBytes, readManifest } from "./packet-io.js"; const SILENT: () => void = () => {}; const SILENT_LOGGER: FilesLogger = { info: SILENT, error: SILENT }; function pickLogger(options: FileCommandOptions | undefined): FilesLogger { return options?.logger ?? SILENT_LOGGER; } /** Result envelope for {@link serveFileCommand}. */ export interface ServeFileResult { readonly outputPath: string; readonly bytesWritten: number; readonly packetsRead: number; readonly fileId: string; } /** * Reconstruct the original file from the prepared packet directory. * Verifies each packet's signature + checksum against the manifest's * `signerPublicKey` before writing to disk. Output path defaults to * `/`. */ export async function serveFileCommand( packetDir: string, options: FileCommandOptions = {} ): Promise { if (!packetDir) { throw new ServerKeysError( SERVER_KEYS_CODES.INVALID_INPUT, "packetDir is required" ); } checkSignal(options.signal, "serve-file"); const manifest = await readManifest(packetDir); const verifyKey = hexToBytes(manifest.signerPublicKey); const reconstructed = new Uint8Array(manifest.originalSize); let writeOffset = 0; let packetsRead = 0; const sorted = [...manifest.packets].sort((a, b) => a.index - b.index); if (sorted.length !== manifest.totalPackets) { throw new ServerKeysError( SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED, `Manifest declares ${manifest.totalPackets} packets but only ${sorted.length} entries present` ); } for (let i = 0; i < sorted.length; i++) { const record = sorted[i]; /* istanbul ignore start -- defensive: `sorted` is derived from manifest.packets via Array.sort, which preserves length and never introduces sparse holes. */ if (!record) continue; /* istanbul ignore stop */ if (record.index !== i) { throw new ServerKeysError( SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED, `Packet sequence broken at index ${i}: manifest entry has index=${record.index}` ); } checkSignal(options.signal, "serve-file"); const buf = await loadFile(path.join(packetDir, record.filename)); const { packed } = deserializePacket(buf); const unpacker = new Unpacker(); let result: Awaited>; try { result = await unpacker.unpack(packed, { verifySignature: true, verifyKey, verifyChecksum: true, }); } catch (err) { let extras: { cause?: Error; context: { index: number }; } = { context: { index: record.index } }; /* istanbul ignore start -- defensive: Unpacker.unpack rejects with Error subclasses on every documented failure mode, so the else branch is unreachable. */ if (err instanceof Error) { extras = { cause: err, context: { index: record.index } }; } /* istanbul ignore stop */ throw new ServerKeysError( SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED, `Packet ${record.index} (${record.filename}) failed verification`, extras ); } if (result.data.byteLength !== record.originalSize) { throw new ServerKeysError( SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED, `Packet ${record.index} size mismatch: expected ${record.originalSize} got ${result.data.byteLength}` ); } reconstructed.set(result.data, writeOffset); writeOffset += result.data.byteLength; packetsRead++; } if (writeOffset !== manifest.originalSize) { throw new ServerKeysError( SERVER_KEYS_CODES.PACKET_INTEGRITY_FAILED, `Reassembled size ${writeOffset} does not match manifest.originalSize ${manifest.originalSize}` ); } const outputPath = options.output ?? path.join(packetDir, manifest.originalFilename); await fs.mkdir(path.dirname(outputPath), { recursive: true }); await fs.writeFile(outputPath, reconstructed); pickLogger(options).info( `serve-file: fileId=${manifest.fileId} packets=${packetsRead} bytes=${writeOffset} out=${outputPath}` ); return { outputPath, bytesWritten: writeOffset, packetsRead, fileId: manifest.fileId, }; }