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}