Home | History | Annotate | Download | only in xz
      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