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.
119 lines (118 loc) • 4.98 kB
JavaScript
import IgirException from "../../exceptions/igirException.js";
import IOFile from "../../models/files/ioFile.js";
import FsUtil from "../../utils/fsUtil.js";
import FileChecksums, { ChecksumBitmask } from "../files/fileChecksums.js";
import Patch from "./patch.js";
const BPSAction = {
SOURCE_READ: 0,
TARGET_READ: 1,
SOURCE_COPY: 2,
TARGET_COPY: 3
};
class BPSPatch extends Patch {
static SUPPORTED_EXTENSIONS = [".bps"];
static FILE_SIGNATURE = Buffer.from("BPS1");
/**
* Parse a .bps patch file and return a {@link BPSPatch}.
*/
static async patchFrom(file) {
let crcBefore = "";
let crcAfter = "";
let targetSize = 0;
await file.extractToTempIOFile("r", async (patchFile) => {
patchFile.seek(this.FILE_SIGNATURE.length);
await super.readUpsUint(patchFile);
targetSize = await super.readUpsUint(patchFile);
patchFile.seek(patchFile.getSize() - 12);
crcBefore = (await patchFile.readNext(4)).reverse().toString("hex");
crcAfter = (await patchFile.readNext(4)).reverse().toString("hex");
const patchChecksumExpected = (await patchFile.readNext(4)).reverse().toString("hex");
patchFile.seek(0);
const patchData = await patchFile.readNext(patchFile.getSize() - 4);
const patchChecksumsActual = await FileChecksums.hashData(patchData, ChecksumBitmask.CRC32);
if (patchChecksumsActual.crc32 !== patchChecksumExpected) {
throw new IgirException(
`BPS patch is invalid, CRC of contents (${patchChecksumsActual.crc32}) doesn't match expected (${patchChecksumExpected}): ${file.toString()}`
);
}
});
if (crcBefore.length !== 8 || crcAfter.length !== 8) {
throw new IgirException(`couldn't parse base file CRC for patch: ${file.toString()}`);
}
return new BPSPatch(file, crcBefore, crcAfter, targetSize);
}
/**
* 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(4);
if (!header.equals(BPSPatch.FILE_SIGNATURE)) {
throw new IgirException(`BPS patch header is invalid: ${this.getFile().toString()}`);
}
const sourceSize = await Patch.readUpsUint(patchFile);
if (inputRomFile.getSize() !== sourceSize) {
throw new IgirException(
`BPS patch expected ROM size of ${FsUtil.sizeReadable(sourceSize)}: ${this.getFile().toString()}`
);
}
await Patch.readUpsUint(patchFile);
const metadataSize = await Patch.readUpsUint(patchFile);
if (metadataSize) {
patchFile.skipNext(metadataSize);
}
await this.writeOutputFile(inputRomFile, outputRomPath, patchFile, callback);
});
}
async writeOutputFile(inputRomFile, outputRomPath, patchFile, callback) {
await inputRomFile.extractToTempIOFile("r", async (inputRomIOFile) => {
const targetFile = await IOFile.fileOfSize(
outputRomPath,
"r+",
this.getSizeAfter()
);
try {
await BPSPatch.applyPatch(patchFile, inputRomIOFile, targetFile, callback);
} finally {
await targetFile.close();
}
});
}
static async applyPatch(patchFile, sourceFile, targetFile, callback) {
let sourceRelativeOffset = 0;
let targetRelativeOffset = 0;
while (patchFile.getPosition() < patchFile.getSize() - 12) {
const blockHeader = await super.readUpsUint(patchFile);
const action = blockHeader & 3;
const length = (blockHeader >> 2) + 1;
if (action === BPSAction.SOURCE_READ) {
await targetFile.write(await sourceFile.readAt(targetFile.getPosition(), length));
} else if (action === BPSAction.TARGET_READ) {
const data = await patchFile.readNext(length);
await targetFile.write(data);
} else if (action === BPSAction.SOURCE_COPY) {
const offset = await super.readUpsUint(patchFile);
sourceRelativeOffset += (offset & 1 ? -1 : 1) * (offset >> 1);
await targetFile.write(await sourceFile.readAt(sourceRelativeOffset, length));
sourceRelativeOffset += length;
} else if (action === BPSAction.TARGET_COPY) {
const offset = await super.readUpsUint(patchFile);
targetRelativeOffset += (offset & 1 ? -1 : 1) * (offset >> 1);
for (let i = 0; i < length; i += 1) {
await targetFile.write(await targetFile.readAt(targetRelativeOffset, 1));
targetRelativeOffset += 1;
}
} else {
throw new IgirException(`BPS action ${action} isn't supported`);
}
if (callback !== void 0) {
const progressPercentage = patchFile.getPosition() / patchFile.getSize();
callback(Math.floor(progressPercentage * targetFile.getSize()));
}
}
}
}
export {
BPSPatch as default
};
//# sourceMappingURL=bpsPatch.js.map