    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 *
    030 * @NotThreadSafe
    031 * @since 1.7
    032 */
    033public class ZCompressorInputStream extends LZWInputStream {
    034    private static final int MAGIC_1 = 0x1f;
    035    private static final int MAGIC_2 = 0x9d;
    036    private static final int BLOCK_MODE_MASK = 0x80;
    037    private static final int MAX_CODE_SIZE_MASK = 0x1f;
    038
    039    /**
    040     * Checks if the signature matches what is expected for a Unix compress file.
    041     *
    042     * @param signature the bytes to check
    043     * @param length    the number of bytes to check
    044     * @return true, if this stream is a Unix compress compressed stream, false otherwise
    045     *
    046     * @since 1.9
    047     */
    048    public static boolean matches(final byte[] signature, final int length) {
    049        return length > 3 && signature[0] == MAGIC_1 && signature[1] == (byte) MAGIC_2;
    050    }
    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) throws IOException {
    062        super(inputStream, ByteOrder.LITTLE_ENDIAN);
    063        final int firstByte = (int) in.readBits(8);
    064        final int secondByte = (int) in.readBits(8);
    065        final int thirdByte = (int) in.readBits(8);
    066        if (firstByte != MAGIC_1 || secondByte != MAGIC_2 || thirdByte < 0) {
    067            throw new IOException("Input is not in .Z format");
    068        }
    069        blockMode = (thirdByte & BLOCK_MODE_MASK) != 0;
    070        maxCodeSize = thirdByte & MAX_CODE_SIZE_MASK;
    071        if (blockMode) {
    072            setClearCode(DEFAULT_CODE_SIZE);
    073        }
    074        initializeTables(maxCodeSize, memoryLimitInKb);
    075        clearEntries();
    076    }
    077
    078    /**
    079     * {@inheritDoc}
    080     * <p>
    081     * <strong>This method is only protected for technical reasons and is not part of Commons Compress' published API. It may change or disappear without
    082     * warning.</strong>
    083     * </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>
    103     * <strong>This method is only protected for technical reasons and is not part of Commons Compress' published API. It may change or disappear without
    104     * warning.</strong>
    105     * </p>
    106     */
    107    @Override
    108    protected int decompressNextSymbol() throws IOException {
    109        //
    110        // table entry table entry
    111        // _____________ _____
    112        // table entry / \ / \
    113        // ____________/ \ \
    114        // / / \ / \ \
    115        // +---+---+---+---+---+---+---+---+---+---+
    116        // | . | . | . | . | . | . | . | . | . | . |
    117        // +---+---+---+---+---+---+---+---+---+---+
    118        // |<--------->|<------------->|<----->|<->|
    119        // symbol symbol symbol symbol
    120        //
    121        final int code = readNextCode();
    122        if (code < 0) {
    123            return -1;
    124        }
    125        if (blockMode && code == getClearCode()) {
    126            clearEntries();
    127            reAlignReading();
    128            resetCodeSize();
    129            resetPreviousCode();
    130            return 0;
    131        }
    132        boolean addedUnfinishedEntry = false;
    133        if (code == getTableSize()) {
    134            addRepeatOfPreviousCode();
    135            addedUnfinishedEntry = true;
    136        } else if (code > getTableSize()) {
    137            throw new IOException(String.format("Invalid %d bit code 0x%x", getCodeSize(), code));
    138        }
    139        return expandCodeToOutputStack(code, addedUnfinishedEntry);
    140    }
    141
    142    /**
    143     * {@inheritDoc}
    144     * <p>
    145     * <strong>This method is only protected for technical reasons and is not part of Commons Compress' published API. It may change or disappear without
    146     * warning.</strong>
    147     * </p>
    148     */
    149    @Override
    150    protected int readNextCode() throws IOException {
    151        final int code = super.readNextCode();
    152        if (code >= 0) {
    153            ++totalCodesRead;
    154        }
    155        return code;
    156    }
    157
    158    private void reAlignReading() throws IOException {
    159        // "compress" works in multiples of 8 symbols, each codeBits bits long.
    160        // When codeBits changes, the remaining unused symbols in the current
    161        // group of 8 are still written out, in the old codeSize,
    162        // as garbage values (usually zeroes) that need to be skipped.
    163        long codeReadsToThrowAway = 8 - totalCodesRead % 8;
    164        if (codeReadsToThrowAway == 8) {
    165            codeReadsToThrowAway = 0;
    166        }
    167        for (long i = 0; i < codeReadsToThrowAway; i++) {
    168            readNextCode();
    169        }
    170        in.clearBitCache();
    171    }
    172
    173}