View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.compress.compressors.xz;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  
24  import org.apache.commons.compress.MemoryLimitException;
25  import org.apache.commons.compress.compressors.CompressorInputStream;
26  import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
27  import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
28  import org.apache.commons.compress.utils.InputStreamStatistics;
29  import org.apache.commons.io.build.AbstractStreamBuilder;
30  import org.apache.commons.io.input.BoundedInputStream;
31  import org.tukaani.xz.LZMA2Options;
32  import org.tukaani.xz.SingleXZInputStream;
33  import org.tukaani.xz.XZ;
34  import org.tukaani.xz.XZInputStream;
35  
36  // @formatter:off
37  /**
38   * XZ decompressor.
39   * <p>
40   * For example:
41   * </p>
42   * <pre>{@code
43   * XZCompressorInputStream s = XZCompressorInputStream.builder()
44   *   .setPath(path)
45   *   .setDecompressConcatenated(false)
46   *   .setMemoryLimitKiB(-1)
47   *   .get();
48   * }
49   * </pre>
50   * @since 1.4
51   */
52  // @formatter:on
53  public class XZCompressorInputStream extends CompressorInputStream implements InputStreamStatistics {
54  
55      // @formatter:off
56      /**
57       * Builds a new {@link LZMACompressorInputStream}.
58       *
59       * <p>
60       * For example:
61       * </p>
62       * <pre>{@code
63       * XZCompressorInputStream s = XZCompressorInputStream.builder()
64       *   .setPath(path)
65       *   .setDecompressConcatenated(false)
66       *   .setMemoryLimitKiB(-1)
67       *   .get();
68       * }
69       * </pre>
70       *
71       * @see #get()
72       * @see LZMA2Options
73       * @since 1.28.0
74       */
75      // @formatter:on
76      public static class Builder extends AbstractStreamBuilder<XZCompressorInputStream, Builder> {
77  
78          private int memoryLimitKiB = -1;
79          private boolean decompressConcatenated;
80  
81          @Override
82          public XZCompressorInputStream get() throws IOException {
83              return new XZCompressorInputStream(this);
84          }
85  
86          /**
87           * Whether to decompress until the end of the input.
88           *
89           * @param decompressConcatenated if true, decompress until the end of the input; if false, stop after the first .xz stream and leave the input position
90           *                               to point to the next byte after the .xz stream
91           * @return this instance.
92           */
93          public Builder setDecompressConcatenated(final boolean decompressConcatenated) {
94              this.decompressConcatenated = decompressConcatenated;
95              return this;
96          }
97  
98  
99          /**
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 }