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.

475 lines (474 loc) • 17 kB
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