1 /* 2 * LZMAOutputStream 3 * 4 * Authors: Lasse Collin <lasse.collin (at) tukaani.org> 5 * Igor Pavlov <http://7-zip.org/> 6 * 7 * This file has been put into the public domain. 8 * You can do whatever you want with this file. 9 */ 10 11 package org.tukaani.xz; 12 13 import java.io.OutputStream; 14 import java.io.IOException; 15 import org.tukaani.xz.lz.LZEncoder; 16 import org.tukaani.xz.rangecoder.RangeEncoderToStream; 17 import org.tukaani.xz.lzma.LZMAEncoder; 18 19 /** 20 * Compresses into the legacy .lzma file format or into a raw LZMA stream. 21 * 22 * @since 1.6 23 */ 24 public class LZMAOutputStream extends FinishableOutputStream { 25 private OutputStream out; 26 27 private final ArrayCache arrayCache; 28 29 private LZEncoder lz; 30 private final RangeEncoderToStream rc; 31 private LZMAEncoder lzma; 32 33 private final int props; 34 private final boolean useEndMarker; 35 private final long expectedUncompressedSize; 36 private long currentUncompressedSize = 0; 37 38 private boolean finished = false; 39 private IOException exception = null; 40 41 private final byte[] tempBuf = new byte[1]; 42 43 private LZMAOutputStream(OutputStream out, LZMA2Options options, 44 boolean useHeader, boolean useEndMarker, 45 long expectedUncompressedSize, 46 ArrayCache arrayCache) 47 throws IOException { 48 if (out == null) 49 throw new NullPointerException(); 50 51 // -1 indicates unknown and >= 0 are for known sizes. 52 if (expectedUncompressedSize < -1) 53 throw new IllegalArgumentException( 54 "Invalid expected input size (less than -1)"); 55 56 this.useEndMarker = useEndMarker; 57 this.expectedUncompressedSize = expectedUncompressedSize; 58 59 this.arrayCache = arrayCache; 60 61 this.out = out; 62 rc = new RangeEncoderToStream(out); 63 64 int dictSize = options.getDictSize(); 65 lzma = LZMAEncoder.getInstance(rc, 66 options.getLc(), options.getLp(), options.getPb(), 67 options.getMode(), 68 dictSize, 0, options.getNiceLen(), 69 options.getMatchFinder(), options.getDepthLimit(), 70 arrayCache); 71 72 lz = lzma.getLZEncoder(); 73 74 byte[] presetDict = options.getPresetDict(); 75 if (presetDict != null && presetDict.length > 0) { 76 if (useHeader) 77 throw new UnsupportedOptionsException( 78 "Preset dictionary cannot be used in .lzma files " 79 + "(try a raw LZMA stream instead)"); 80 81 lz.setPresetDict(dictSize, presetDict); 82 } 83 84 props = (options.getPb() * 5 + options.getLp()) * 9 + options.getLc(); 85 86 if (useHeader) { 87 // Props byte stores lc, lp, and pb. 88 out.write(props); 89 90 // Dictionary size is stored as a 32-bit unsigned little endian 91 // integer. 92 for (int i = 0; i < 4; ++i) { 93 out.write(dictSize & 0xFF); 94 dictSize >>>= 8; 95 } 96 97 // Uncompressed size is stored as a 64-bit unsigned little endian 98 // integer. The max value (-1 in two's complement) indicates 99 // unknown size. 100 for (int i = 0; i < 8; ++i) 101 out.write((int)(expectedUncompressedSize >>> (8 * i)) & 0xFF); 102 } 103 } 104 105 /** 106 * Creates a new compressor for the legacy .lzma file format. 107 * <p> 108 * If the uncompressed size of the input data is known, it will be stored 109 * in the .lzma header and no end of stream marker will be used. Otherwise 110 * the header will indicate unknown uncompressed size and the end of stream 111 * marker will be used. 112 * <p> 113 * Note that a preset dictionary cannot be used in .lzma files but 114 * it can be used for raw LZMA streams. 115 * 116 * @param out output stream to which the compressed data 117 * will be written 118 * 119 * @param options LZMA compression options; the same class 120 * is used here as is for LZMA2 121 * 122 * @param inputSize uncompressed size of the data to be compressed; 123 * use <code>-1</code> when unknown 124 * 125 * @throws IOException may be thrown from <code>out</code> 126 */ 127 public LZMAOutputStream(OutputStream out, LZMA2Options options, 128 long inputSize) 129 throws IOException { 130 this(out, options, inputSize, ArrayCache.getDefaultCache()); 131 } 132 133 /** 134 * Creates a new compressor for the legacy .lzma file format. 135 * <p> 136 * This is identical to 137 * <code>LZMAOutputStream(OutputStream, LZMA2Options, long)</code> 138 * except that this also takes the <code>arrayCache</code> argument. 139 * 140 * @param out output stream to which the compressed data 141 * will be written 142 * 143 * @param options LZMA compression options; the same class 144 * is used here as is for LZMA2 145 * 146 * @param inputSize uncompressed size of the data to be compressed; 147 * use <code>-1</code> when unknown 148 * 149 * @param arrayCache cache to be used for allocating large arrays 150 * 151 * @throws IOException may be thrown from <code>out</code> 152 * 153 * @since 1.7 154 */ 155 public LZMAOutputStream(OutputStream out, LZMA2Options options, 156 long inputSize, ArrayCache arrayCache) 157 throws IOException { 158 this(out, options, true, inputSize == -1, inputSize, arrayCache); 159 } 160 161 /** 162 * Creates a new compressor for raw LZMA (also known as LZMA1) stream. 163 * <p> 164 * Raw LZMA streams can be encoded with or without end of stream marker. 165 * When decompressing the stream, one must know if the end marker was used 166 * and tell it to the decompressor. If the end marker wasn't used, the 167 * decompressor will also need to know the uncompressed size. 168 * 169 * @param out output stream to which the compressed data 170 * will be written 171 * 172 * @param options LZMA compression options; the same class 173 * is used here as is for LZMA2 174 * 175 * @param useEndMarker 176 * if end of stream marker should be written 177 * 178 * @throws IOException may be thrown from <code>out</code> 179 */ 180 public LZMAOutputStream(OutputStream out, LZMA2Options options, 181 boolean useEndMarker) throws IOException { 182 this(out, options, useEndMarker, ArrayCache.getDefaultCache()); 183 } 184 185 /** 186 * Creates a new compressor for raw LZMA (also known as LZMA1) stream. 187 * <p> 188 * This is identical to 189 * <code>LZMAOutputStream(OutputStream, LZMA2Options, boolean)</code> 190 * except that this also takes the <code>arrayCache</code> argument. 191 * 192 * @param out output stream to which the compressed data 193 * will be written 194 * 195 * @param options LZMA compression options; the same class 196 * is used here as is for LZMA2 197 * 198 * @param useEndMarker 199 * if end of stream marker should be written 200 * 201 * @param arrayCache cache to be used for allocating large arrays 202 * 203 * @throws IOException may be thrown from <code>out</code> 204 * 205 * @since 1.7 206 */ 207 public LZMAOutputStream(OutputStream out, LZMA2Options options, 208 boolean useEndMarker, ArrayCache arrayCache) 209 throws IOException { 210 this(out, options, false, useEndMarker, -1, arrayCache); 211 } 212 213 /** 214 * Returns the LZMA lc/lp/pb properties encoded into a single byte. 215 * This might be useful when handling file formats other than .lzma 216 * that use the same encoding for the LZMA properties as .lzma does. 217 */ 218 public int getProps() { 219 return props; 220 } 221 222 /** 223 * Gets the amount of uncompressed data written to the stream. 224 * This is useful when creating raw LZMA streams without 225 * the end of stream marker. 226 */ 227 public long getUncompressedSize() { 228 return currentUncompressedSize; 229 } 230 231 public void write(int b) throws IOException { 232 tempBuf[0] = (byte)b; 233 write(tempBuf, 0, 1); 234 } 235 236 public void write(byte[] buf, int off, int len) throws IOException { 237 if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length) 238 throw new IndexOutOfBoundsException(); 239 240 if (exception != null) 241 throw exception; 242 243 if (finished) 244 throw new XZIOException("Stream finished or closed"); 245 246 if (expectedUncompressedSize != -1 247 && expectedUncompressedSize - currentUncompressedSize < len) 248 throw new XZIOException("Expected uncompressed input size (" 249 + expectedUncompressedSize + " bytes) was exceeded"); 250 251 currentUncompressedSize += len; 252 253 try { 254 while (len > 0) { 255 int used = lz.fillWindow(buf, off, len); 256 off += used; 257 len -= used; 258 lzma.encodeForLZMA1(); 259 } 260 } catch (IOException e) { 261 exception = e; 262 throw e; 263 } 264 } 265 266 /** 267 * Flushing isn't supported and will throw XZIOException. 268 */ 269 public void flush() throws IOException { 270 throw new XZIOException("LZMAOutputStream does not support flushing"); 271 } 272 273 /** 274 * Finishes the stream without closing the underlying OutputStream. 275 */ 276 public void finish() throws IOException { 277 if (!finished) { 278 if (exception != null) 279 throw exception; 280 281 try { 282 if (expectedUncompressedSize != -1 283 && expectedUncompressedSize != currentUncompressedSize) 284 throw new XZIOException("Expected uncompressed size (" 285 + expectedUncompressedSize + ") doesn't equal " 286 + "the number of bytes written to the stream (" 287 + currentUncompressedSize + ")"); 288 289 lz.setFinishing(); 290 lzma.encodeForLZMA1(); 291 292 if (useEndMarker) 293 lzma.encodeLZMA1EndMarker(); 294 295 rc.finish(); 296 } catch (IOException e) { 297 exception = e; 298 throw e; 299 } 300 301 finished = true; 302 303 lzma.putArraysToCache(arrayCache); 304 lzma = null; 305 lz = null; 306 } 307 } 308 309 /** 310 * Finishes the stream and closes the underlying OutputStream. 311 */ 312 public void close() throws IOException { 313 if (out != null) { 314 try { 315 finish(); 316 } catch (IOException e) {} 317 318 try { 319 out.close(); 320 } catch (IOException e) { 321 if (exception == null) 322 exception = e; 323 } 324 325 out = null; 326 } 327 328 if (exception != null) 329 throw exception; 330 } 331 } 332