    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.xz;
    020
    021import java.io.IOException;
    022import java.io.InputStream;
    023
    024import org.tukaani.xz.XZ;
    025import org.tukaani.xz.SingleXZInputStream;
    026import org.tukaani.xz.XZInputStream;
    027
    028import org.apache.commons.compress.MemoryLimitException;
    029import org.apache.commons.compress.compressors.CompressorInputStream;
    030import org.apache.commons.compress.utils.CountingInputStream;
    031import org.apache.commons.compress.utils.IOUtils;
    032import org.apache.commons.compress.utils.InputStreamStatistics;
    033
    034/**
    035 * XZ decompressor.
    036 * @since 1.4
    037 */
    038public class XZCompressorInputStream extends CompressorInputStream
    039    implements InputStreamStatistics {
    040
    041    private final CountingInputStream countingStream;
    042    private final InputStream in;
    043
    044    /**
    045     * Checks if the signature matches what is expected for a .xz file.
    046     *
    047     * @param   signature     the bytes to check
    048     * @param   length        the number of bytes to check
    049     * @return  true if signature matches the .xz magic bytes, false otherwise
    050     */
    051    public static boolean matches(final byte[] signature, final int length) {
    052        if (length < XZ.HEADER_MAGIC.length) {
    053            return false;
    054        }
    055
    056        for (int i = 0; i < XZ.HEADER_MAGIC.length; ++i) {
    057            if (signature[i] != XZ.HEADER_MAGIC[i]) {
    058                return false;
    059            }
    060        }
    061
    062        return true;
    063    }
    064
    065    /**
    066     * Creates a new input stream that decompresses XZ-compressed data
    067     * from the specified input stream. This doesn't support
    068     * concatenated .xz files.
    069     *
    070     * @param       inputStream where to read the compressed data
    071     *
    072     * @throws      IOException if the input is not in the .xz format,
    073     *                          the input is corrupt or truncated, the .xz
    074     *                          headers specify options that are not supported
    075     *                          by this implementation, or the underlying
    076     *                          <code>inputStream</code> throws an exception
    077     */
    078    public XZCompressorInputStream(final InputStream inputStream)
    079            throws IOException {
    080        this(inputStream, false);
    081    }
    082
    083    /**
    084     * Creates a new input stream that decompresses XZ-compressed data
    085     * from the specified input stream.
    086     *
    087     * @param       inputStream where to read the compressed data
    088     * @param       decompressConcatenated
    089     *                          if true, decompress until the end of the
    090     *                          input; if false, stop after the first .xz
    091     *                          stream and leave the input position to point
    092     *                          to the next byte after the .xz stream
    093     *
    094     * @throws      IOException if the input is not in the .xz format,
    095     *                          the input is corrupt or truncated, the .xz
    096     *                          headers specify options that are not supported
    097     *                          by this implementation, or the underlying
    098     *                          <code>inputStream</code> throws an exception
    099     */
    100    public XZCompressorInputStream(final InputStream inputStream,
    101                                   final boolean decompressConcatenated)
    102            throws IOException {
    103        this(inputStream, decompressConcatenated, -1);
    104    }
    105
    106    /**
    107     * Creates a new input stream that decompresses XZ-compressed data
    108     * from the specified input stream.
    109     *
    110     * @param       inputStream where to read the compressed data
    111     * @param       decompressConcatenated
    112     *                          if true, decompress until the end of the
    113     *                          input; if false, stop after the first .xz
    114     *                          stream and leave the input position to point
    115     *                          to the next byte after the .xz stream
    116     * @param       memoryLimitInKb memory limit used when reading blocks.  If
    117     *                          the estimated memory limit is exceeded on {@link #read()},
    118     *                          a {@link MemoryLimitException} is thrown.
    119     *
    120     * @throws      IOException if the input is not in the .xz format,
    121     *                          the input is corrupt or truncated, the .xz
    122     *                          headers specify options that are not supported
    123     *                          by this implementation,
    124     *                          or the underlying <code>inputStream</code> throws an exception
    125     *
    126     * @since 1.14
    127     */
    128    public XZCompressorInputStream(final InputStream inputStream,
    129                                   final boolean decompressConcatenated, final int memoryLimitInKb)
    130            throws IOException {
    131        countingStream = new CountingInputStream(inputStream);
    132        if (decompressConcatenated) {
    133            in = new XZInputStream(countingStream, memoryLimitInKb);
    134        } else {
    135            in = new SingleXZInputStream(countingStream, memoryLimitInKb);
    136        }
    137    }
    138
    139    @Override
    140    public int read() throws IOException {
    141        try {
    142            final int ret = in.read();
    143            count(ret == -1 ? -1 : 1);
    144            return ret;
    145        } catch (final org.tukaani.xz.MemoryLimitException e) {
    146            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
    147        }
    148    }
    149
    150    @Override
    151    public int read(final byte[] buf, final int off, final int len) throws IOException {
    152        if (len == 0) {
    153            return 0;
    154        }
    155        try {
    156            final int ret = in.read(buf, off, len);
    157            count(ret);
    158            return ret;
    159        } catch (final org.tukaani.xz.MemoryLimitException e) {
    160            //convert to commons-compress MemoryLimtException
    161            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
    162        }
    163    }
    164
    165    @Override
    166    public long skip(final long n) throws IOException {
    167        try {
    168            return IOUtils.skip(in, n);
    169        } catch (final org.tukaani.xz.MemoryLimitException e) {
    170            //convert to commons-compress MemoryLimtException
    171            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
    172        }
    173    }
    174
    175    @Override
    176    public int available() throws IOException {
    177        return in.available();
    178    }
    179
    180    @Override
    181    public void close() throws IOException {
    182        in.close();
    183    }
    184
    185    /**
    186     * @since 1.17
    187     */
    188    @Override
    189    public long getCompressedCount() {
    190        return countingStream.getBytesRead();
    191    }
    192}