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