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.

128 lines (127 loc) • 5.72 kB
import IOFile from '../../polyfill/ioFile.js'; import IgirException from '../exceptions/igirException.js'; import Patch from './patch.js'; const NinjaCommand = { TERMINATE: 0x00, OPEN: 0x01, XOR: 0x02, }; 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])); /** * @see https://www.romhacking.net/utilities/329/ */ export default class NinjaPatch extends Patch { static SUPPORTED_EXTENSIONS = ['.rup']; static FILE_SIGNATURE = Buffer.from('NINJA'); static patchFrom(file) { const crcBefore = Patch.getCrcFromPath(file.getExtractedFilePath()); return new NinjaPatch(file, crcBefore); } async createPatchedFile(inputRomFile, outputRomPath) { return 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 = Number.parseInt((await patchFile.readNext(1)).toString(), 10); if (version !== 2) { throw new IgirException(`NINJA v${version} isn't supported: ${this.getFile().toString()}`); } patchFile.skipNext(1); // encoding patchFile.skipNext(84); // author patchFile.skipNext(11); // version patchFile.skipNext(256); // title patchFile.skipNext(48); // genre patchFile.skipNext(48); // language patchFile.skipNext(8); // date patchFile.skipNext(512); // website patchFile.skipNext(1074); // info return this.writeOutputFile(inputRomFile, outputRomPath, patchFile); }); } async writeOutputFile(inputRomFile, outputRomPath, patchFile) { await inputRomFile.extractToFile(outputRomPath); const targetFile = await IOFile.fileFrom(outputRomPath, 'r+'); try { while (!patchFile.isEOF()) { await this.applyCommand(patchFile, targetFile); } } finally { await targetFile.close(); } } async applyCommand(patchFile, targetFile) { const command = (await patchFile.readNext(1)).readUInt8(); if (command === NinjaCommand.TERMINATE) { // Nothing } 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); // file name 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); // source MD5 patchFile.skipNext(16); // modified MD5 if (sourceFileSize !== modifiedFileSize) { patchFile.skipNext(1); // "M" or "A" 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; // NOTE(cemmer): this isn't documented anywhere } 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] : 0x00) ^ xorData[i]; } await targetFile.writeAt(targetData, offset); } }