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
JavaScript
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);
}
}