    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 * http://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.compress.compressors.z;
    020
    021import java.io.IOException;
    022import java.io.InputStream;
    023import java.nio.ByteOrder;
    024
    025import org.apache.commons.compress.compressors.lzw.LZWInputStream;
    026
    027/**
    028 * Input stream that decompresses .Z files.
    029 * @NotThreadSafe
    030 * @since 1.7
    031 */
    032public class ZCompressorInputStream extends LZWInputStream {
    033    private static final int MAGIC_1 = 0x1f;
    034    private static final int MAGIC_2 = 0x9d;
    035    private static final int BLOCK_MODE_MASK = 0x80;
    036    private static final int MAX_CODE_SIZE_MASK = 0x1f;
    037    /**
    038     * Checks if the signature matches what is expected for a Unix compress file.
    039     *
    040     * @param signature
    041     *            the bytes to check
    042     * @param length
    043     *            the number of bytes to check
    044     * @return true, if this stream is a Unix compress compressed
    045     * stream, false otherwise
    046     *
    047     * @since 1.9
    048     */
    049    public static boolean matches(final byte[] signature, final int length) {
    050        return length > 3 && signature[0] == MAGIC_1 && signature[1] == (byte) MAGIC_2;
    051    }
    052    private final boolean blockMode;
    053    private final int maxCodeSize;
    054
    055    private long totalCodesRead;
    056
    057    public ZCompressorInputStream(final InputStream inputStream) throws IOException {
    058        this(inputStream, -1);
    059    }
    060
    061    public ZCompressorInputStream(final InputStream inputStream, final int memoryLimitInKb)
    062            throws IOException {
    063        super(inputStream, ByteOrder.LITTLE_ENDIAN);
    064        final int firstByte = (int) in.readBits(8);
    065        final int secondByte = (int) in.readBits(8);
    066        final int thirdByte = (int) in.readBits(8);
    067        if (firstByte != MAGIC_1 || secondByte != MAGIC_2 || thirdByte < 0) {
    068            throw new IOException("Input is not in .Z format");
    069        }
    070        blockMode = (thirdByte & BLOCK_MODE_MASK) != 0;
    071        maxCodeSize = thirdByte & MAX_CODE_SIZE_MASK;
    072        if (blockMode) {
    073            setClearCode(DEFAULT_CODE_SIZE);
    074        }
    075        initializeTables(maxCodeSize, memoryLimitInKb);
    076        clearEntries();
    077    }
    078
    079    /**
    080     * {@inheritDoc}
    081     * <p><strong>This method is only protected for technical reasons
    082     * and is not part of Commons Compress' published API.  It may
    083     * change or disappear without warning.</strong></p>
    084     */
    085    @Override
    086    protected int addEntry(final int previousCode, final byte character) throws IOException {
    087        final int maxTableSize = 1 << getCodeSize();
    088        final int r = addEntry(previousCode, character, maxTableSize);
    089        if (getTableSize() == maxTableSize && getCodeSize() < maxCodeSize) {
    090            reAlignReading();
    091            incrementCodeSize();
    092        }
    093        return r;
    094    }
    095
    096    private void clearEntries() {
    097        setTableSize((1 << 8) + (blockMode ? 1 : 0));
    098    }
    099
    100    /**
    101     * {@inheritDoc}
    102     * <p><strong>This method is only protected for technical reasons
    103     * and is not part of Commons Compress' published API.  It may
    104     * change or disappear without warning.</strong></p>
    105     */
    106    @Override
    107    protected int decompressNextSymbol() throws IOException {
    108        //
    109        //                   table entry    table entry
    110        //                  _____________   _____
    111        //    table entry  /             \ /     \
    112        //    ____________/               \       \
    113        //   /           / \             / \       \
    114        //  +---+---+---+---+---+---+---+---+---+---+
    115        //  | . | . | . | . | . | . | . | . | . | . |
    116        //  +---+---+---+---+---+---+---+---+---+---+
    117        //  |<--------->|<------------->|<----->|<->|
    118        //     symbol        symbol      symbol  symbol
    119        //
    120        final int code = readNextCode();
    121        if (code < 0) {
    122            return -1;
    123        }
    124        if (blockMode && code == getClearCode()) {
    125            clearEntries();
    126            reAlignReading();
    127            resetCodeSize();
    128            resetPreviousCode();
    129            return 0;
    130        }
    131        boolean addedUnfinishedEntry = false;
    132        if (code == getTableSize()) {
    133            addRepeatOfPreviousCode();
    134            addedUnfinishedEntry = true;
    135        } else if (code > getTableSize()) {
    136            throw new IOException(String.format("Invalid %d bit code 0x%x", getCodeSize(), code));
    137        }
    138        return expandCodeToOutputStack(code, addedUnfinishedEntry);
    139    }
    140
    141    /**
    142     * {@inheritDoc}
    143     * <p><strong>This method is only protected for technical reasons
    144     * and is not part of Commons Compress' published API.  It may
    145     * change or disappear without warning.</strong></p>
    146     */
    147    @Override
    148    protected int readNextCode() throws IOException {
    149        final int code = super.readNextCode();
    150        if (code >= 0) {
    151            ++totalCodesRead;
    152        }
    153        return code;
    154    }
    155
    156    private void reAlignReading() throws IOException {
    157        // "compress" works in multiples of 8 symbols, each codeBits bits long.
    158        // When codeBits changes, the remaining unused symbols in the current
    159        // group of 8 are still written out, in the old codeSize,
    160        // as garbage values (usually zeroes) that need to be skipped.
    161        long codeReadsToThrowAway = 8 - totalCodesRead % 8;
    162        if (codeReadsToThrowAway == 8) {
    163            codeReadsToThrowAway = 0;
    164        }
    165        for (long i = 0; i < codeReadsToThrowAway; i++) {
    166            readNextCode();
    167        }
    168        in.clearBitCache();
    169    }
    170
    171}