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   * http://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.utils.InputStreamStatistics;
27  import org.apache.commons.io.input.BoundedInputStream;
28  import org.tukaani.xz.SingleXZInputStream;
29  import org.tukaani.xz.XZ;
30  import org.tukaani.xz.XZInputStream;
31  
32  /**
33   * XZ decompressor.
34   *
35   * @since 1.4
36   */
37  public class XZCompressorInputStream extends CompressorInputStream implements InputStreamStatistics {
38  
39      /**
40       * Checks if the signature matches what is expected for a .xz file.
41       *
42       * @param signature the bytes to check
43       * @param length    the number of bytes to check
44       * @return true if signature matches the .xz magic bytes, false otherwise
45       */
46      public static boolean matches(final byte[] signature, final int length) {
47          if (length < XZ.HEADER_MAGIC.length) {
48              return false;
49          }
50  
51          for (int i = 0; i < XZ.HEADER_MAGIC.length; ++i) {
52              if (signature[i] != XZ.HEADER_MAGIC[i]) {
53                  return false;
54              }
55          }
56  
57          return true;
58      }
59  
60      private final BoundedInputStream countingStream;
61  
62      private final InputStream in;
63  
64      /**
65       * Creates a new input stream that decompresses XZ-compressed data from the specified input stream. This doesn't support concatenated .xz files.
66       *
67       * @param inputStream where to read the compressed data
68       *
69       * @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
70       *                     this implementation, or the underlying {@code inputStream} throws an exception
71       */
72      public XZCompressorInputStream(final InputStream inputStream) throws IOException {
73          this(inputStream, false);
74      }
75  
76      /**
77       * Creates a new input stream that decompresses XZ-compressed data from the specified input stream.
78       *
79       * @param inputStream            where to read the compressed data
80       * @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
81       *                               point to the next byte after the .xz stream
82       *
83       * @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
84       *                     this implementation, or the underlying {@code inputStream} throws an exception
85       */
86      public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated) throws IOException {
87          this(inputStream, decompressConcatenated, -1);
88      }
89  
90      /**
91       * Creates a new input stream that decompresses XZ-compressed data from the specified input stream.
92       *
93       * @param inputStream            where to read the compressed data
94       * @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
95       *                               point to the next byte after the .xz stream
96       * @param memoryLimitInKb        memory limit used when reading blocks. If the estimated memory limit is exceeded on {@link #read()}, a
97       *                               {@link MemoryLimitException} is thrown.
98       *
99       * @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
100      *                     this implementation, or the underlying {@code inputStream} throws an exception
101      *
102      * @since 1.14
103      */
104     public XZCompressorInputStream(final InputStream inputStream, final boolean decompressConcatenated, final int memoryLimitInKb) throws IOException {
105         countingStream = BoundedInputStream.builder().setInputStream(inputStream).get();
106         if (decompressConcatenated) {
107             in = new XZInputStream(countingStream, memoryLimitInKb);
108         } else {
109             in = new SingleXZInputStream(countingStream, memoryLimitInKb);
110         }
111     }
112 
113     @Override
114     public int available() throws IOException {
115         return in.available();
116     }
117 
118     @Override
119     public void close() throws IOException {
120         in.close();
121     }
122 
123     /**
124      * @since 1.17
125      */
126     @Override
127     public long getCompressedCount() {
128         return countingStream.getCount();
129     }
130 
131     @Override
132     public int read() throws IOException {
133         try {
134             final int ret = in.read();
135             count(ret == -1 ? -1 : 1);
136             return ret;
137         } catch (final org.tukaani.xz.MemoryLimitException e) {
138             throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
139         }
140     }
141 
142     @Override
143     public int read(final byte[] buf, final int off, final int len) throws IOException {
144         if (len == 0) {
145             return 0;
146         }
147         try {
148             final int ret = in.read(buf, off, len);
149             count(ret);
150             return ret;
151         } catch (final org.tukaani.xz.MemoryLimitException e) {
152             // convert to commons-compress MemoryLimtException
153             throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
154         }
155     }
156 
157     @Override
158     public long skip(final long n) throws IOException {
159         try {
160             return org.apache.commons.io.IOUtils.skip(in, n);
161         } catch (final org.tukaani.xz.MemoryLimitException e) {
162             // convert to commons-compress MemoryLimtException
163             throw new MemoryLimitException(e.getMemoryNeeded(), e.getMemoryLimit(), e);
164         }
165     }
166 }