/* * Copyright 2012 dorkbox, llc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package dorkbox.cabParser.decompress.lzx; import dorkbox.cabParser.CabException; import dorkbox.cabParser.CorruptCabException; import dorkbox.cabParser.decompress.Decompressor; public final class DecompressLzx implements Decompressor, LZXConstants { private int[] extraBits = new int[51]; private int[] positionBase = new int[51]; private DecompressLzxTree mainTree; private DecompressLzxTree lengthTree; private DecompressLzxTree alignedTree; private DecompressLzxTree preTree; private int wndSize; private int windowMask; private int mainElements; private int blocksRemaining; private int blockType; private int blockRemaining; private int blockLength; private int windowPosition; private int R0; private int R1; private int R2; private byte[] localWindow; private int windowSize; private boolean readHeader; private int outputPosition; private int index; private int length; private byte[] inputBytes; private boolean abort; int bitsLeft; private int blockAlignOffset; private int intelFileSize; private int intelCursorPos; private boolean intelStarted; private int framesRead; public DecompressLzx() { int i = 4; int j = 1; do { this.extraBits[i] = j; this.extraBits[i + 1] = j; i += 2; j++; } while (j <= 16); do { this.extraBits[i++] = 17; } while (i < 51); i = -2; for (j = 0; j < this.extraBits.length; j++) { this.positionBase[j] = i; i += 1 << this.extraBits[j]; } this.windowSize = -1; } @Override public void init(int windowBits) throws CabException { this.wndSize = 1 << windowBits; this.windowMask = this.wndSize - 1; reset(windowBits); } @Override public void decompress(byte[] inputBytes, byte[] outputBytes, int inputLength, int outputLength) throws CabException { this.abort = false; this.index = 0; this.inputBytes = inputBytes; this.length = inputBytes.length; initBitStream(); int decompressedOutputLength = decompressLoop(outputLength); System.arraycopy(this.localWindow, this.outputPosition, outputBytes, 0, decompressedOutputLength); if (this.framesRead++ < E8_DISABLE_THRESHOLD && this.intelFileSize != 0) { decodeIntelBlock(outputBytes, decompressedOutputLength); } } @Override public int getMaxGrowth() { return MAX_GROWTH; } @Override public void reset(int windowBits) throws CabException { if (this.windowSize == windowBits) { this.mainTree.reset(); this.lengthTree.reset(); this.alignedTree.reset(); } else { maybeReset(); int i = NUM_CHARS + this.mainElements * ALIGNED_NUM_ELEMENTS; this.localWindow = new byte[this.wndSize + 261]; this.preTree = new DecompressLzxTree(PRETREE_NUM_ELEMENTS, ALIGNED_NUM_ELEMENTS, this, null); this.mainTree = new DecompressLzxTree(i, 9, this, this.preTree); this.lengthTree = new DecompressLzxTree(SECONDARY_NUM_ELEMENTS, 6, this, this.preTree); this.alignedTree = new DecompressLzxTree(ALIGNED_NUM_ELEMENTS, NUM_PRIMARY_LENGTHS, this, this.preTree); } this.windowSize = windowBits; this.R0 = this.R1 = this.R2 = 1; this.blocksRemaining = 0; this.windowPosition = 0; this.intelCursorPos = 0; this.readHeader = true; this.intelStarted = false; this.framesRead = 0; this.blockType = BLOCKTYPE_INVALID; } private int decompressLoop(int bytesToRead) throws CabException { int i = bytesToRead; int lastWindowPosition; int k; int m; if (this.readHeader) { // read header if (readBits(1) == 1) { k = readBits(16); m = readBits(16); this.intelFileSize = k << 16 | m; } else { this.intelFileSize = 0; } this.readHeader = false; } lastWindowPosition = 0; while (bytesToRead > 0) { if (this.blocksRemaining == 0) { if (this.blockType == BLOCKTYPE_UNCOMPRESSED) { if ((this.blockLength & 0x1) != 0 && /* realign bitstream to word */ this.index < this.length) { this.index++; } this.blockType = BLOCKTYPE_INVALID; initBitStream(); } this.blockType = readBits(3); k = readBits(8); m = readBits(8); int n = readBits(8); if (this.abort) { break; } this.blockRemaining = this.blockLength = (k << 16) + (m << 8) + n; if (this.blockType == BLOCKTYPE_ALIGNED) { this.alignedTree.readLengths(); this.alignedTree.buildTable(); // rest of aligned header is same as verbatim } if (this.blockType == BLOCKTYPE_ALIGNED || this.blockType == BLOCKTYPE_VERBATIM) { this.mainTree.read(); this.lengthTree.read(); this.mainTree.readLengths(0, NUM_CHARS); this.mainTree.readLengths(NUM_CHARS, NUM_CHARS + this.mainElements * ALIGNED_NUM_ELEMENTS); this.mainTree.buildTable(); if (this.mainTree.LENS[0xE8] != 0) { this.intelStarted = true; } this.lengthTree.readLengths(0, SECONDARY_NUM_ELEMENTS); this.lengthTree.buildTable(); } else if (this.blockType == BLOCKTYPE_UNCOMPRESSED) { // because we can't assume otherwise this.intelStarted = true; this.index -= 2; // align the bitstream if (this.index < 0 || this.index + 12 >= this.length) { throw new CorruptCabException(); } this.R0 = readInt(); this.R1 = readInt(); this.R2 = readInt(); } else { throw new CorruptCabException(); } } this.blocksRemaining = 1; while (this.blockRemaining > 0 && bytesToRead > 0) { if (this.blockRemaining < bytesToRead) { k = this.blockRemaining; } else { k = bytesToRead; } decompressBlockActions(k); this.blockRemaining -= k; bytesToRead -= k; lastWindowPosition += k; } if (this.blockRemaining == 0) { this.blocksRemaining = 0; } if (bytesToRead == 0 && this.blockAlignOffset != 16) { readNumberBits(this.blockAlignOffset); } } if (lastWindowPosition != i) { throw new CorruptCabException(); } if (this.windowPosition == 0) { this.outputPosition = this.wndSize - lastWindowPosition; } else { this.outputPosition = this.windowPosition - lastWindowPosition; } return lastWindowPosition; } @SuppressWarnings("NumericCastThatLosesPrecision") private void decodeIntelBlock(byte[] bytes, int outLength) { if (outLength <= 10 || !this.intelStarted) { this.intelCursorPos += outLength; return; } int cursorPos = this.intelCursorPos; int fileSize = this.intelFileSize; int abs_off = 0; int adjustedOutLength = outLength - 10; int dataIndex = 0; int cursor_pos = cursorPos + adjustedOutLength; while (cursorPos < cursor_pos) { while (bytes[dataIndex++] == -24) { if (cursorPos >= cursor_pos) { break; } abs_off = bytes[dataIndex] & 0xFF | (bytes[dataIndex + 1] & 0xFF) << 8 | (bytes[dataIndex + 2] & 0xFF) << 16 | (bytes[dataIndex + 3] & 0xFF) << 24; if ((abs_off >= -cursorPos) && (abs_off < fileSize)) { int rel_off = (abs_off >= 0) ? abs_off - cursorPos : abs_off + fileSize; bytes[dataIndex] = (byte) (rel_off & 0xFF); bytes[dataIndex + 1] = (byte) (rel_off >>> 8 & 0xFF); bytes[dataIndex + 2] = (byte) (rel_off >>> 16 & 0xFF); bytes[dataIndex + 3] = (byte) (rel_off >>> 24 & 0xFF); } dataIndex += 4; cursorPos += 5; } cursorPos++; } this.intelCursorPos += outLength; } private void decompressBlockActions(int bytesToRead) throws CabException { this.windowPosition &= this.windowMask; if (this.windowPosition + bytesToRead > this.wndSize) { throw new CabException(); } switch (this.blockType) { case BLOCKTYPE_UNCOMPRESSED : uncompressedAlgo(bytesToRead); return; case BLOCKTYPE_ALIGNED : alignedAlgo(bytesToRead); return; case BLOCKTYPE_VERBATIM : verbatimAlgo(bytesToRead); return; default : throw new CorruptCabException(); } } void readNumberBits(int numBits) { this.bitsLeft <<= numBits; this.blockAlignOffset -= numBits; if (this.blockAlignOffset <= 0) { this.bitsLeft |= readShort() << -this.blockAlignOffset; this.blockAlignOffset += 16; } } private void initBitStream() { if (this.blockType != BLOCKTYPE_UNCOMPRESSED) { this.bitsLeft = readShort() << 16 | readShort(); this.blockAlignOffset = 16; } } private void maybeReset() { this.mainElements = 4; int i = 4; do { i += 1 << this.extraBits[this.mainElements]; this.mainElements++; } while (i < this.wndSize); } @SuppressWarnings("NumericCastThatLosesPrecision") private void verbatimAlgo(int this_run) throws CorruptCabException { int i = this.windowPosition; int mask = this.windowMask; byte[] windowPosition = this.localWindow; int r0 = this.R0; int r1 = this.R1; int r2 = this.R2; int[] arrayOfInt1 = this.extraBits; int[] arrayOfInt2 = this.positionBase; DecompressLzxTree mainTree = this.mainTree; DecompressLzxTree lengthTree = this.lengthTree; while (this_run > 0) { int main_element = mainTree.decodeElement(); if (main_element < NUM_CHARS) { windowPosition[i++] = (byte) main_element; this_run--; } /* is a match */ else { main_element -= NUM_CHARS; int match_length = main_element & NUM_PRIMARY_LENGTHS; if (match_length == NUM_PRIMARY_LENGTHS) { match_length += lengthTree.decodeElement(); } int matchOffset = main_element >>> 3; /* check for repeated offsets (positions 0,1,2) */ if (matchOffset == 0) { matchOffset = r0; } else if (matchOffset == 1) { matchOffset = r1; r1 = r0; r0 = matchOffset; } else if (matchOffset > 2) { // not repeated offset if (matchOffset > 3) { matchOffset = verbatimAlgo2(arrayOfInt1[matchOffset]) + arrayOfInt2[matchOffset]; } else { matchOffset = 1; } r2 = r1; r1 = r0; r0 = matchOffset; } else { matchOffset = r2; r2 = r0; r0 = matchOffset; } match_length += MIN_MATCH; this_run -= match_length; while (match_length > 0) { windowPosition[i] = windowPosition[i - matchOffset & mask]; i++; match_length--; } } } if (this_run != 0) { throw new CorruptCabException(); } this.R0 = r0; this.R1 = r1; this.R2 = r2; this.windowPosition = i; } private int verbatimAlgo2(int position) { int i = this.bitsLeft >>> 32 - position; this.bitsLeft <<= position; this.blockAlignOffset -= position; if (this.blockAlignOffset <= 0) { this.bitsLeft |= readShort() << -this.blockAlignOffset; this.blockAlignOffset += 16; if (this.blockAlignOffset <= 0) { this.bitsLeft |= readShort() << -this.blockAlignOffset; this.blockAlignOffset += 16; } } return i; } @SuppressWarnings("NumericCastThatLosesPrecision") private void alignedAlgo(int this_run) throws CorruptCabException { int windowPos = this.windowPosition; int mask = this.windowMask; byte[] window = this.localWindow; int r0 = this.R0; int r1 = this.R1; int r2 = this.R2; while (this_run > 0) { int mainElement = this.mainTree.decodeElement(); if (mainElement < NUM_CHARS) { window[windowPos] = (byte) mainElement; windowPos = windowPos + 1 & mask; this_run--; } /* is a match */ else { mainElement -= NUM_CHARS; int matchLength = mainElement & NUM_PRIMARY_LENGTHS; if (matchLength == NUM_PRIMARY_LENGTHS) { matchLength += this.lengthTree.decodeElement(); } int match_offset = mainElement >>> 3; if (match_offset > 2) { // not repeated offset int extra = this.extraBits[match_offset]; match_offset = this.positionBase[match_offset]; if (extra > 3) { // verbatim and aligned bits match_offset += (readBits(extra - 3) << 3) + this.alignedTree.decodeElement(); } else if (extra == 3) { // aligned bits only match_offset += this.alignedTree.decodeElement(); } else if (extra > 0) { // verbatim bits only match_offset += readBits(extra); } else { match_offset = 1; } // update repeated offset LRU queue r2 = r1; r1 = r0; r0 = match_offset; } else if (match_offset == 0) { match_offset = r0; } else if (match_offset == 1) { match_offset = r1; r1 = r0; r0 = match_offset; } else { match_offset = r2; r2 = r0; r0 = match_offset; } matchLength += MIN_MATCH; this_run -= matchLength; while (matchLength > 0) { window[windowPos] = window[windowPos - match_offset & mask]; windowPos = windowPos + 1 & mask; matchLength--; } } } if (this_run != 0) { throw new CorruptCabException(); } this.R0 = r0; this.R1 = r1; this.R2 = r2; this.windowPosition = windowPos; } private int readShort() { if (this.index < this.length) { int i = this.inputBytes[this.index] & 0xFF | (this.inputBytes[this.index + 1] & 0xFF) << 8; this.index += 2; return i; } this.abort = true; this.index = 0; return 0; } int readBits(int numBitsToRead) { int i = this.bitsLeft >>> 32 - numBitsToRead; readNumberBits(numBitsToRead); return i; } private void uncompressedAlgo(int length) throws CorruptCabException { if (this.index + length > this.length || this.windowPosition + length > this.wndSize) { throw new CorruptCabException(); } this.intelStarted = true; System.arraycopy(this.inputBytes, this.index, this.localWindow, this.windowPosition, length); this.index += length; this.windowPosition += length; } private int readInt() { int i = this.inputBytes[this.index] & 0xFF | (this.inputBytes[this.index + 1] & 0xFF) << 8 | (this.inputBytes[this.index + 2] & 0xFF) << 16 | (this.inputBytes[this.index + 3] & 0xFF) << 24; this.index += 4; return i; } }