    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 *   https://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.apache.commons.compress.MemoryLimitException;
    025import org.apache.commons.compress.compressors.CompressorInputStream;
    026import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
    027import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
    028import org.apache.commons.compress.utils.InputStreamStatistics;
    029import org.apache.commons.io.build.AbstractStreamBuilder;
    030import org.apache.commons.io.input.BoundedInputStream;
    031import org.tukaani.xz.LZMA2Options;
    032import org.tukaani.xz.SingleXZInputStream;
    033import org.tukaani.xz.XZ;
    034import org.tukaani.xz.XZInputStream;
    035
    036// @formatter:off
    037/**
    038 * XZ decompressor.
    039 * <p>
    040 * For example:
    041 * </p>
    042 * <pre>{@code
    043 * XZCompressorInputStream s = XZCompressorInputStream.builder()
    044 *   .setPath(path)
    045 *   .setDecompressConcatenated(false)
    046 *   .setMemoryLimitKiB(-1)
    047 *   .get();
    048 * }
    049 * </pre>
    050 * @since 1.4
    051 */
    052// @formatter:on
    053public class XZCompressorInputStream extends CompressorInputStream implements InputStreamStatistics {
    054
    055    // @formatter:off
    056    /**
    057     * Builds a new {@link LZMACompressorInputStream}.
    058     *
    059     * <p>
    060     * For example:
    061     * </p>
    062     * <pre>{@code
    063     * XZCompressorInputStream s = XZCompressorInputStream.builder()
    064     *   .setPath(path)
    065     *   .setDecompressConcatenated(false)
    066     *   .setMemoryLimitKiB(-1)
    067     *   .get();
    068     * }
    069     * </pre>
    070     *
    071     * @see #get()
    072     * @see LZMA2Options
    073     * @since 1.28.0
    074     */
    075    // @formatter:on
    076    public static class Builder extends AbstractStreamBuilder<XZCompressorInputStream, Builder> {
    077
    078        private int memoryLimitKiB = -1;
    079        private boolean decompressConcatenated;
    080
    081        @Override
    082        public XZCompressorInputStream get() throws IOException {
    083            return new XZCompressorInputStream(this);
    084        }
    085
    086        /**
    087         * Whether to decompress until the end of the input.
    088         *
    089         * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first .xz stream and leave the input position
    090         *                               to point to the next byte after the .xz stream
    091         * @return this instance.
    092         */
    093        public Builder setDecompressConcatenated(final boolean decompressConcatenated) {
    094            this.decompressConcatenated = decompressConcatenated;
    095            return this;
    096        }
    097
    098
    099        /**
    100         * Sets a working memory threshold in kibibytes (KiB).
    101         *
    102         * @param memoryLimitKiB The memory limit used when reading blocks. The memory usage limit is expressed in kibibytes (KiB) or {@code -1} to impose no
    103         *                       memory usage limit. If the estimated memory limit is exceeded on {@link #read()}, a {@link MemoryLimitException} is thrown.
    104         * @return this instance.
    105         */
    106        public Builder setMemoryLimitKiB(final int memoryLimitKiB) {
    107            this.memoryLimitKiB = memoryLimitKiB;
    108            return this;
    109        }
    110    }
    111
    112    /**
    113     * Constructs a new builder of {@link LZMACompressorOutputStream}.
    114     *
    115     * @return a new builder of {@link LZMACompressorOutputStream}.
    116     * @since 1.28.0
    117     */
    118    public static Builder builder() {
    119        return new Builder();
    120    }
    121
    122
    123    /**
    124     * Checks if the signature matches what is expected for a .xz file.
    125     *
    126     * @param signature the bytes to check
    127     * @param length    the number of bytes to check
    128     * @return true if signature matches the .xz magic bytes, false otherwise
    129     */
    130    public static boolean matches(final byte[] signature, final int length) {
    131        if (length < XZ.HEADER_MAGIC.length) {
    132            return false;
    133        }
    134
    135        for (int i = 0; i < XZ.HEADER_MAGIC.length; ++i) {
    136            if (signature[i] != XZ.HEADER_MAGIC[i]) {
    137                return false;
    138            }
    139        }
    140
    141        return true;
    142    }
    143
    144    private final BoundedInputStream countingStream;
    145
    146    private final InputStream in;
    147
    148    @SuppressWarnings("resource") // Caller closes
    149    private XZCompressorInputStream(final Builder builder) throws IOException {
    150        countingStream = BoundedInputStream.builder().setInputStream(builder.getInputStream()).get();
    151        if (builder.decompressConcatenated) {
    152            in = new XZInputStream(countingStream, builder.memoryLimitKiB);
    153        } else {
    154            in = new SingleXZInputStream(countingStream, builder.memoryLimitKiB);
    155        }
    156    }
    157
    158    /**
    159     * Creates a new input stream that decompresses XZ-compressed data from the specified input stream. This doesn't support concatenated .xz files.
    160     *
    161     * @param inputStream where to read the compressed data
    162     * @throws IOException if the input is not in the .xz format, the input is corrupt or truncated, the .xz headers specify options that are not supported by
    163     *                     this implementation, or the underlying {@code inputStream} throws an exception
    164     */
    165    public XZCompressorInputStream(final InputStream inputStream) throws IOException {
    166        this(builder().setInputStream(inputStream));
    167    }
    168
    169    /**
    170     * Creates a new input stream that decompresses XZ-compressed data from the specified input stream.
    171     *
    172     * @param inputStream            where to read the compressed data
    173     * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first .xz stream and leave the input position to
    174     *                               point to the next byte after the .xz stream
    175     *
    176     * @throws IOException if the input is not in the .xz format, the input is corrupt or truncated, the .xz headers specify options that are not supported by
    177     *                     this implementation, or the underlying {@code inputStream} throws an exception
    178     * @deprecated Use {@link #builder()}.
    179     */
    180    @Deprecated
    181    public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated) throws IOException {
    182        this(builder().setInputStream(inputStream).setDecompressConcatenated(decompressConcatenated));
    183    }
    184
    185    /**
    186     * Creates a new input stream that decompresses XZ-compressed data from the specified input stream.
    187     *
    188     * @param inputStream            where to read the compressed data
    189     * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first .xz stream and leave the input position to
    190     *                               point to the next byte after the .xz stream
    191     * @param memoryLimitKiB         The memory limit used when reading blocks. The memory usage limit is expressed in kibibytes (KiB) or {@code -1} to impose
    192     *                               no memory usage limit. If the estimated memory limit is exceeded on {@link #read()}, a {@link MemoryLimitException} is
    193     *                               thrown.
    194     *
    195     * @throws IOException if the input is not in the .xz format, the input is corrupt or truncated, the .xz headers specify options that are not supported by
    196     *                     this implementation, or the underlying {@code inputStream} throws an exception
    197     *
    198     * @deprecated Use {@link #builder()}.
    199     * @since 1.14
    200     */
    201    @Deprecated
    202    public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated, final int memoryLimitKiB) throws IOException {
    203        this(builder().setInputStream(inputStream).setDecompressConcatenated(decompressConcatenated).setMemoryLimitKiB(memoryLimitKiB));
    204    }
    205
    206
    207    @Override
    208    public int available() throws IOException {
    209        return in.available();
    210    }
    211
    212    @Override
    213    public void close() throws IOException {
    214        in.close();
    215    }
    216
    217    /**
    218     * @since 1.17
    219     */
    220    @Override
    221    public long getCompressedCount() {
    222        return countingStream.getCount();
    223    }
    224
    225    @Override
    226    public int read() throws IOException {
    227        try {
    228            final int ret = in.read();
    229            count(ret == -1 ? -1 : 1);
    230            return ret;
    231        } catch (final org.tukaani.xz.MemoryLimitException e) {
    232            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), (Throwable) e);
    233        }
    234    }
    235
    236    @Override
    237    public int read(final byte[] buf, final int off, final int len) throws IOException {
    238        if (len == 0) {
    239            return 0;
    240        }
    241        try {
    242            final int ret = in.read(buf, off, len);
    243            count(ret);
    244            return ret;
    245        } catch (final org.tukaani.xz.MemoryLimitException e) {
    246            // convert to commons-compress MemoryLimtException
    247            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), (Throwable) e);
    248        }
    249    }
    250
    251    @Override
    252    public long skip(final long n) throws IOException {
    253        try {
    254            return org.apache.commons.io.IOUtils.skip(in, n);
    255        } catch (final org.tukaani.xz.MemoryLimitException e) {
    256            // convert to commons-compress MemoryLimtException
    257            throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), (Throwable) e);
    258        }
    259    }
    260}