UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

144 lines (143 loc) • 5.46 kB
import IgirException from "../../exceptions/igirException.js"; import IOFile from "../../models/files/ioFile.js"; import Patch from "./patch.js"; const NinjaCommand = { TERMINATE: 0, OPEN: 1, XOR: 2 }; const NinjaFileType = { RAW: 0, NES: 1, FDS: 2, SNES: 3, N64: 4, GB: 5, SMS: 6, MEGA: 7, PCE: 8, LYNX: 9 }; const NinjaFileTypeInverted = Object.fromEntries( Object.entries(NinjaFileType).map(([key, value]) => [value, key]) ); class NinjaPatch extends Patch { static SUPPORTED_EXTENSIONS = [".rup"]; static FILE_SIGNATURE = Buffer.from("NINJA"); /** * Parse a NINJA (.rup) patch file and return a {@link NinjaPatch}. */ static patchFrom(file) { const crcBefore = super.getCrcFromPath(file.getExtractedFilePath()); return new NinjaPatch(file, crcBefore); } /** * Apply this patch to the input ROM file and write the patched result to the output path. */ async createPatchedFile(inputRomFile, outputRomPath, callback) { await this.getFile().extractToTempIOFile("r", async (patchFile) => { const header = await patchFile.readNext(5); if (!header.equals(NinjaPatch.FILE_SIGNATURE)) { throw new IgirException(`NINJA patch header is invalid: ${this.getFile().toString()}`); } const version = Math.trunc(Number((await patchFile.readNext(1)).toString())); if (version !== 2) { throw new IgirException(`NINJA v${version} isn't supported: ${this.getFile().toString()}`); } patchFile.skipNext(1); patchFile.skipNext(84); patchFile.skipNext(11); patchFile.skipNext(256); patchFile.skipNext(48); patchFile.skipNext(48); patchFile.skipNext(8); patchFile.skipNext(512); patchFile.skipNext(1074); await this.writeOutputFile(inputRomFile, outputRomPath, patchFile, callback); }); } async writeOutputFile(inputRomFile, outputRomPath, patchFile, callback) { await inputRomFile.extractToFile(outputRomPath); const targetFile = await IOFile.fileFrom(outputRomPath, "r+"); try { while (!patchFile.isEOF()) { await this.applyCommand(patchFile, targetFile); if (callback !== void 0) { const progressPercentage = patchFile.getPosition() / patchFile.getSize(); callback(Math.floor(progressPercentage * targetFile.getSize())); } } } finally { await targetFile.close(); } } async applyCommand(patchFile, targetFile) { const command = (await patchFile.readNext(1)).readUInt8(); if (command === NinjaCommand.TERMINATE) { } else if (command === NinjaCommand.OPEN) { await this.applyCommandOpen(patchFile, targetFile); } else if (command === NinjaCommand.XOR) { await NinjaPatch.applyCommandXor(patchFile, targetFile); } else { throw new IgirException(`Ninja command ${command} isn't supported`); } } async applyCommandOpen(patchFile, targetFile) { const multiFile = (await patchFile.readNext(1)).readUInt8(); if (multiFile > 0) { throw new IgirException( `Multi-file NINJA patches aren't supported: ${this.getFile().toString()}` ); } const fileNameLength = multiFile > 0 ? (await patchFile.readNext(multiFile)).readUIntLE(0, multiFile) : 0; patchFile.skipNext(fileNameLength); const fileType = (await patchFile.readNext(1)).readUInt8(); if (fileType > 0) { throw new IgirException( `unsupported NINJA file type ${NinjaFileTypeInverted[fileType]}: ${this.getFile().toString()}` ); } const sourceFileSizeLength = (await patchFile.readNext(1)).readUInt8(); const sourceFileSize = (await patchFile.readNext(sourceFileSizeLength)).readUIntLE( 0, sourceFileSizeLength ); const modifiedFileSizeLength = (await patchFile.readNext(1)).readUInt8(); const modifiedFileSize = (await patchFile.readNext(modifiedFileSizeLength)).readUIntLE( 0, modifiedFileSizeLength ); patchFile.skipNext(16); patchFile.skipNext(16); if (sourceFileSize !== modifiedFileSize) { patchFile.skipNext(1); const overflowSizeLength = (await patchFile.readNext(1)).readUInt8(); const overflowSize = overflowSizeLength > 0 ? (await patchFile.readNext(overflowSizeLength)).readUIntLE(0, overflowSizeLength) : 0; const overflow = overflowSize > 0 ? await patchFile.readNext(overflowSize) : Buffer.alloc(overflowSize); for (let i = 0; i < overflow.length; i += 1) { overflow[i] ^= 255; } if (modifiedFileSize > sourceFileSize) { await targetFile.writeAt(overflow, targetFile.getSize()); } } } static async applyCommandXor(patchFile, targetFile) { const offsetLength = (await patchFile.readNext(1)).readUInt8(); const offset = (await patchFile.readNext(offsetLength)).readUIntLE(0, offsetLength); targetFile.seek(offset); const lengthLength = (await patchFile.readNext(1)).readUInt8(); const length = (await patchFile.readNext(lengthLength)).readUIntLE(0, lengthLength); const sourceData = await targetFile.readNext(length); const xorData = await patchFile.readNext(length); const targetData = Buffer.allocUnsafe(length); for (let i = 0; i < length; i += 1) { targetData[i] = (i < sourceData.length ? sourceData[i] : 0) ^ xorData[i]; } await targetFile.writeAt(targetData, offset); } } export { NinjaPatch as default }; //# sourceMappingURL=ninjaPatch.js.map