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 }