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}