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.
475 lines (474 loc) • 17 kB
JavaScript
import IgirException from "../../exceptions/igirException.js";
import IOFile from "../../models/files/ioFile.js";
import FsUtil from "../../utils/fsUtil.js";
import Patch from "./patch.js";
const VcdiffSecondaryCompression = {
NONE: 0,
DJW_STATIC_HUFFMAN: 1,
LZMA: 2,
FGK_ADAPTIVE_HUFFMAN: 16
};
const VcdiffSecondaryCompressionInverted = Object.fromEntries(
Object.entries(VcdiffSecondaryCompression).map(([key, value]) => [value, key])
);
const VcdiffHdrIndicator = {
DECOMPRESS: 1,
CODETABLE: 2,
APPHEADER: 4
};
const VcdiffWinIndicator = {
SOURCE: 1,
TARGET: 2,
ADLER32: 4
};
const VcdiffDeltaIndicator = {
DATACOMP: 1,
INSTCOMP: 2,
ADDRCOMP: 4
};
const VcdiffCopyAddressMode = {
SELF: 0,
HERE: 1
// s_near "near modes"
// s_same "same modes"
};
const VcdiffInstruction = {
NOOP: 0,
ADD: 1,
RUN: 2,
COPY: 3
};
class VcdiffHeader {
static FILE_SIGNATURE = Buffer.from("d6c3c4", "hex");
static DEFAULT_CODE_TABLE = (() => {
const entries = [
[
{ type: VcdiffInstruction.RUN, size: 0, mode: 0 },
{ type: VcdiffInstruction.NOOP, size: 0, mode: 0 }
]
];
for (let sizeToAdd = 0; sizeToAdd <= 17; sizeToAdd += 1) {
entries.push([
{ type: VcdiffInstruction.ADD, size: sizeToAdd, mode: 0 },
{ type: VcdiffInstruction.NOOP, size: 0, mode: 0 }
]);
}
for (let copyMode = 0; copyMode <= 8; copyMode += 1) {
entries.push([
{ type: VcdiffInstruction.COPY, size: 0, mode: copyMode },
{ type: VcdiffInstruction.NOOP, size: 0, mode: 0 }
]);
for (let copySize = 4; copySize <= 18; copySize += 1) {
entries.push([
{
type: VcdiffInstruction.COPY,
size: copySize,
mode: copyMode
},
{ type: VcdiffInstruction.NOOP, size: 0, mode: 0 }
]);
}
}
for (let copyMode = 0; copyMode <= 5; copyMode += 1) {
for (let sizeToAdd = 1; sizeToAdd <= 4; sizeToAdd += 1) {
for (let copySize = 4; copySize <= 6; copySize += 1) {
entries.push([
{ type: VcdiffInstruction.ADD, size: sizeToAdd, mode: 0 },
{
type: VcdiffInstruction.COPY,
size: copySize,
mode: copyMode
}
]);
}
}
}
for (let copyMode = 6; copyMode <= 8; copyMode += 1) {
for (let sizeToAdd = 1; sizeToAdd <= 4; sizeToAdd += 1) {
entries.push([
{ type: VcdiffInstruction.ADD, size: sizeToAdd, mode: 0 },
{ type: VcdiffInstruction.COPY, size: 4, mode: copyMode }
]);
}
}
for (let copyMode = 0; copyMode <= 8; copyMode += 1) {
entries.push([
{ type: VcdiffInstruction.COPY, size: 4, mode: copyMode },
{ type: VcdiffInstruction.ADD, size: 1, mode: 0 }
]);
}
return entries;
})();
secondaryDecompressorId;
codeTable;
constructor(secondaryDecompressorId, codeTable) {
this.secondaryDecompressorId = secondaryDecompressorId;
this.codeTable = codeTable;
}
/**
* Read and validate the VCDIFF header from the patch file, advancing the read position past
* it.
*/
static async fromIOFile(patchFile) {
const header = await patchFile.readNext(3);
if (!header.equals(this.FILE_SIGNATURE)) {
await patchFile.close();
throw new IgirException(
`Vcdiff patch header is invalid: ${patchFile.getPathLike().toString()}`
);
}
patchFile.skipNext(1);
const hdrIndicator = (await patchFile.readNext(1)).readUInt8();
let secondaryDecompressorId = 0;
if (hdrIndicator & VcdiffHdrIndicator.DECOMPRESS) {
secondaryDecompressorId = (await patchFile.readNext(1)).readUInt8();
if (secondaryDecompressorId) {
await patchFile.close();
throw new IgirException(
`unsupported Vcdiff secondary decompressor ${VcdiffSecondaryCompressionInverted[secondaryDecompressorId]}: ${patchFile.getPathLike().toString()}`
);
}
}
const codeTable = this.DEFAULT_CODE_TABLE;
if (hdrIndicator & VcdiffHdrIndicator.CODETABLE) {
const codeTableLength = await Patch.readVcdiffUintFromFile(patchFile);
if (codeTableLength) {
await patchFile.close();
throw new IgirException(
`can't parse Vcdiff application-defined code table: ${patchFile.getPathLike().toString()}`
);
}
}
if (hdrIndicator & VcdiffHdrIndicator.APPHEADER) {
const appHeaderLength = await Patch.readVcdiffUintFromFile(patchFile);
patchFile.skipNext(appHeaderLength);
}
return new VcdiffHeader(secondaryDecompressorId, codeTable);
}
}
class VcdiffWindow {
winIndicator;
sourceSegmentSize;
sourceSegmentPosition;
deltaEncodingTargetWindowSize;
targetWindowOffset = 0;
addsAndRunsOffset = 0;
addsAndRunsData;
instructionsAndSizeOffset = 0;
instructionsAndSizesData;
copyAddressesOffset = 0;
copyAddressesData;
constructor(winIndicator, sourceSegmentSize, sourceSegmentPosition, deltaEncodingTargetWindowSize, addsAndRunsData, instructionsAndSizesData, copyAddressesData) {
this.winIndicator = winIndicator;
this.sourceSegmentSize = sourceSegmentSize;
this.sourceSegmentPosition = sourceSegmentPosition;
this.deltaEncodingTargetWindowSize = deltaEncodingTargetWindowSize;
this.addsAndRunsData = addsAndRunsData;
this.instructionsAndSizesData = instructionsAndSizesData;
this.copyAddressesData = copyAddressesData;
}
/**
* Read a single VCDIFF window from the patch file, advancing the read position past it.
*/
static async fromIOFile(patchFile) {
const winIndicator = (await patchFile.readNext(1)).readUInt8();
let sourceSegmentSize = 0;
let sourceSegmentPosition = 0;
if (winIndicator & (VcdiffWinIndicator.SOURCE | VcdiffWinIndicator.TARGET)) {
sourceSegmentSize = await Patch.readVcdiffUintFromFile(patchFile);
sourceSegmentPosition = await Patch.readVcdiffUintFromFile(patchFile);
}
await Patch.readVcdiffUintFromFile(patchFile);
const deltaEncodingTargetWindowSize = await Patch.readVcdiffUintFromFile(patchFile);
const deltaEncodingIndicator = (await patchFile.readNext(1)).readUInt8();
const addsAndRunsDataLength = await Patch.readVcdiffUintFromFile(patchFile);
const instructionsAndSizesLength = await Patch.readVcdiffUintFromFile(patchFile);
const copyAddressesLength = await Patch.readVcdiffUintFromFile(patchFile);
if (winIndicator & VcdiffWinIndicator.ADLER32) {
(await patchFile.readNext(4)).readUInt32BE();
}
const addsAndRunsData = await patchFile.readNext(addsAndRunsDataLength);
if (deltaEncodingIndicator & VcdiffDeltaIndicator.DATACOMP) {
throw new IgirException(
`unsupported Vcdiff compression of instruction data (DATACOMP): ${patchFile.getPathLike().toString()}`
);
}
const instructionsAndSizesData = await patchFile.readNext(instructionsAndSizesLength);
if (deltaEncodingIndicator & VcdiffDeltaIndicator.INSTCOMP) {
throw new IgirException(
`unsupported Vcdiff compression of delta instructions (INSTCOMP): ${patchFile.getPathLike().toString()}`
);
}
const copyAddressesData = await patchFile.readNext(copyAddressesLength);
if (deltaEncodingIndicator & VcdiffDeltaIndicator.ADDRCOMP) {
throw new IgirException(
`unsupported Vcdiff compression of instruction addresses (ADDRCOMP): ${patchFile.getPathLike().toString()}`
);
}
return new VcdiffWindow(
winIndicator,
sourceSegmentSize,
sourceSegmentPosition,
deltaEncodingTargetWindowSize,
addsAndRunsData,
instructionsAndSizesData,
copyAddressesData
);
}
/**
* Returns true if the entire instructions-and-sizes section of this window has been consumed.
*/
isEOF() {
return this.instructionsAndSizeOffset >= this.instructionsAndSizesData.length;
}
getProgressPercentage() {
return this.instructionsAndSizeOffset / this.instructionsAndSizesData.length;
}
/**
* Read and consume the next instruction-code-table index from the instructions stream.
*/
readInstructionIndex() {
const instructionCodeIdx = this.instructionsAndSizesData.readUInt8(
this.instructionsAndSizeOffset
);
this.instructionsAndSizeOffset += 1;
return instructionCodeIdx;
}
/**
* Read and consume the next variable-length size value from the instructions stream.
*/
readInstructionSize() {
const [size, instructionsAndSizeOffset] = Patch.readVcdiffUintFromBuffer(
this.instructionsAndSizesData,
this.instructionsAndSizeOffset
);
this.instructionsAndSizeOffset = instructionsAndSizeOffset;
return size;
}
/**
* Apply a VCDIFF ADD instruction, writing the next `size` bytes from this window's data
* section into the target file at the current window position.
*/
async writeAddData(targetFile, targetWindowPosition, size) {
const data = this.addsAndRunsData.subarray(
this.addsAndRunsOffset,
this.addsAndRunsOffset + size
);
this.addsAndRunsOffset += size;
await targetFile.writeAt(data, targetWindowPosition + this.targetWindowOffset);
this.targetWindowOffset += size;
}
/**
* Apply a VCDIFF RUN instruction, writing `size` repetitions of the next byte from this
* window's data section into the target file at the current window position.
*/
async writeRunData(targetFile, targetWindowPosition, size) {
const data = Buffer.from(
this.addsAndRunsData.subarray(this.targetWindowOffset, this.targetWindowOffset + 1).toString("hex").repeat(size),
"hex"
);
this.addsAndRunsOffset += 1;
await targetFile.writeAt(data, targetWindowPosition + this.targetWindowOffset);
this.targetWindowOffset += size;
}
/**
* Apply a VCDIFF COPY instruction, copying `size` bytes from the source segment or the
* already-written target window into the target file at the current window position.
*/
async writeCopyData(sourceFile, targetFile, targetWindowPosition, size, copyCache, mode) {
const [addr, copyAddressesOffset] = copyCache.decode(
this.copyAddressesData,
this.copyAddressesOffset,
this.targetWindowOffset,
mode
);
this.copyAddressesOffset = copyAddressesOffset;
for (let byteNum = 0; byteNum < size; byteNum += 1) {
let byte;
if (addr < this.sourceSegmentSize) {
if (this.winIndicator & VcdiffWinIndicator.SOURCE) {
byte = await sourceFile.readAt(this.sourceSegmentPosition + addr + byteNum, 1);
} else {
byte = await targetFile.readAt(this.sourceSegmentPosition + addr + byteNum, 1);
}
} else {
byte = await targetFile.readAt(
targetWindowPosition + (addr - this.sourceSegmentSize) + byteNum,
1
);
}
await targetFile.writeAt(byte, targetWindowPosition + this.targetWindowOffset + byteNum);
}
this.targetWindowOffset += size;
}
}
class VcdiffCache {
sNear;
near;
nextSlot = 0;
sSame;
same;
constructor(sNear = 4, sSame = 3) {
this.sNear = sNear;
this.near = Array.from({ length: sNear });
this.sSame = sSame;
this.same = Array.from({ length: sSame * 256 });
}
/**
* Clear both address caches back to their initial state.
*/
reset() {
this.near.fill(0);
this.same.fill(0);
this.nextSlot = 0;
}
update(addr) {
if (this.sNear > 0) {
this.near[this.nextSlot] = addr;
this.nextSlot = (this.nextSlot + 1) % this.sNear;
}
if (this.sSame > 0) {
this.same[addr % (this.sSame * 256)] = addr;
}
}
/**
* Decode a copy address from the buffer at the given offset under the given address mode,
* returning the resolved address and the new offset past the encoded bytes.
*/
decode(copyAddressesData, copyAddressesOffset, here, mode) {
let addr;
let readValue;
let copyAddressesOffsetAfter = copyAddressesOffset;
if (mode === VcdiffCopyAddressMode.SELF) {
[readValue, copyAddressesOffsetAfter] = Patch.readVcdiffUintFromBuffer(
copyAddressesData,
copyAddressesOffset
);
addr = readValue;
} else if (mode === VcdiffCopyAddressMode.HERE) {
[readValue, copyAddressesOffsetAfter] = Patch.readVcdiffUintFromBuffer(
copyAddressesData,
copyAddressesOffset
);
addr = here - readValue;
} else if (mode >= 2 && mode <= this.sNear + 1) {
const m = mode - 2;
[readValue, copyAddressesOffsetAfter] = Patch.readVcdiffUintFromBuffer(
copyAddressesData,
copyAddressesOffset
);
addr = this.near[m] + readValue;
} else {
const m = mode - (2 + this.sNear);
readValue = copyAddressesData.readUInt8(copyAddressesOffset);
copyAddressesOffsetAfter += 1;
addr = this.same[m * 256 + readValue];
}
this.update(addr);
return [addr, copyAddressesOffsetAfter];
}
}
class VcdiffPatch extends Patch {
static SUPPORTED_EXTENSIONS = [".vcdiff", ".xdelta"];
static FILE_SIGNATURE = VcdiffHeader.FILE_SIGNATURE;
/**
* Parse a .vcdiff/.xdelta patch file and return a {@link VcdiffPatch}.
*/
static patchFrom(file) {
const crcBefore = super.getCrcFromPath(file.getExtractedFilePath());
return new VcdiffPatch(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 copyCache = new VcdiffCache();
const header = await VcdiffHeader.fromIOFile(patchFile);
await VcdiffPatch.writeOutputFile(
inputRomFile,
outputRomPath,
patchFile,
header,
copyCache,
callback
);
});
}
static async writeOutputFile(inputRomFile, outputRomPath, patchFile, header, copyCache, callback) {
await inputRomFile.extractToTempFile(async (tempRomFile) => {
const sourceFile = await IOFile.fileFrom(tempRomFile, "r");
await FsUtil.copyFile(tempRomFile, outputRomPath);
const targetFile = await IOFile.fileFrom(outputRomPath, "r+");
try {
await this.applyPatch(patchFile, sourceFile, targetFile, header, copyCache, callback);
} finally {
await targetFile.close();
await sourceFile.close();
}
});
}
static async applyPatch(patchFile, sourceFile, targetFile, header, copyCache, callback) {
let targetWindowPosition = 0;
while (!patchFile.isEOF()) {
const windowStartOffset = patchFile.getPosition();
const window = await VcdiffWindow.fromIOFile(patchFile);
const windowEndOffset = patchFile.getPosition();
copyCache.reset();
const startPercentage = windowStartOffset / patchFile.getSize();
const windowSizePercentage = (windowEndOffset - windowStartOffset) / patchFile.getSize();
const patchWindowCallback = callback === void 0 ? void 0 : ((windowProgressPercentage) => {
const progressPercentage = startPercentage + windowSizePercentage * windowProgressPercentage;
callback(Math.floor(progressPercentage * targetFile.getSize()));
});
await this.applyPatchWindow(
sourceFile,
targetFile,
header,
copyCache,
targetWindowPosition,
window,
patchWindowCallback
);
targetWindowPosition += window.deltaEncodingTargetWindowSize;
}
}
static async applyPatchWindow(sourceFile, targetFile, header, copyCache, targetWindowPosition, window, callback) {
while (!window.isEOF()) {
if (callback !== void 0) {
callback(window.getProgressPercentage());
}
const instructionCodeIdx = window.readInstructionIndex();
for (let i = 0; i <= 1; i += 1) {
const instruction = header.codeTable[instructionCodeIdx][i];
if (instruction.type === VcdiffInstruction.NOOP) {
continue;
}
let { size } = instruction;
if (!size) {
size = window.readInstructionSize();
}
if (instruction.type === VcdiffInstruction.ADD) {
await window.writeAddData(targetFile, targetWindowPosition, size);
} else if (instruction.type === VcdiffInstruction.RUN) {
await window.writeRunData(targetFile, targetWindowPosition, size);
} else if (instruction.type === VcdiffInstruction.COPY) {
await window.writeCopyData(
sourceFile,
targetFile,
targetWindowPosition,
size,
copyCache,
instruction.mode
);
} else {
throw new IgirException(`Vcdiff instruction ${instruction.type} isn't supported`);
}
}
}
}
}
export {
VcdiffPatch as default
};
//# sourceMappingURL=vcdiffPatch.js.map