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
JavaScript
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