1 /*
2  * LZMA2OutputStream
3  *
4  * Authors: Lasse Collin <lasse.collin@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.DataOutputStream;
14 import java.io.IOException;
15 import org.tukaani.xz.lz.LZEncoder;
16 import org.tukaani.xz.rangecoder.RangeEncoder;
17 import org.tukaani.xz.lzma.LZMAEncoder;
18 
19 class LZMA2OutputStream extends FinishableOutputStream {
20     static final int COMPRESSED_SIZE_MAX = 64 << 10;
21 
22     private FinishableOutputStream out;
23     private final DataOutputStream outData;
24 
25     private final LZEncoder lz;
26     private final RangeEncoder rc;
27     private final LZMAEncoder lzma;
28 
29     private final int props; // Cannot change props on the fly for now.
30     private boolean dictResetNeeded = true;
31     private boolean stateResetNeeded = true;
32     private boolean propsNeeded = true;
33 
34     private int pendingSize = 0;
35     private boolean finished = false;
36     private IOException exception = null;
37 
38     private final byte[] tempBuf = new byte[1];
39 
getExtraSizeBefore(int dictSize)40     private static int getExtraSizeBefore(int dictSize) {
41         return COMPRESSED_SIZE_MAX > dictSize
42                ? COMPRESSED_SIZE_MAX - dictSize : 0;
43     }
44 
getMemoryUsage(LZMA2Options options)45     static int getMemoryUsage(LZMA2Options options) {
46         // 64 KiB buffer for the range encoder + a little extra + LZMAEncoder
47         int dictSize = options.getDictSize();
48         int extraSizeBefore = getExtraSizeBefore(dictSize);
49         return 70 + LZMAEncoder.getMemoryUsage(options.getMode(),
50                                                dictSize, extraSizeBefore,
51                                                options.getMatchFinder());
52     }
53 
LZMA2OutputStream(FinishableOutputStream out, LZMA2Options options)54     LZMA2OutputStream(FinishableOutputStream out, LZMA2Options options) {
55         if (out == null)
56             throw new NullPointerException();
57 
58         this.out = out;
59         outData = new DataOutputStream(out);
60         rc = new RangeEncoder(COMPRESSED_SIZE_MAX);
61 
62         int dictSize = options.getDictSize();
63         int extraSizeBefore = getExtraSizeBefore(dictSize);
64         lzma = LZMAEncoder.getInstance(rc,
65                 options.getLc(), options.getLp(), options.getPb(),
66                 options.getMode(),
67                 dictSize, extraSizeBefore, options.getNiceLen(),
68                 options.getMatchFinder(), options.getDepthLimit());
69 
70         lz = lzma.getLZEncoder();
71 
72         byte[] presetDict = options.getPresetDict();
73         if (presetDict != null && presetDict.length > 0) {
74             lz.setPresetDict(dictSize, presetDict);
75             dictResetNeeded = false;
76         }
77 
78         props = (options.getPb() * 5 + options.getLp()) * 9 + options.getLc();
79     }
80 
write(int b)81     public void write(int b) throws IOException {
82         tempBuf[0] = (byte)b;
83         write(tempBuf, 0, 1);
84     }
85 
write(byte[] buf, int off, int len)86     public void write(byte[] buf, int off, int len) throws IOException {
87         if (off < 0 || len < 0 || off + len < 0 || off + len > buf.length)
88             throw new IndexOutOfBoundsException();
89 
90         if (exception != null)
91             throw exception;
92 
93         if (finished)
94             throw new XZIOException("Stream finished or closed");
95 
96         try {
97             while (len > 0) {
98                 int used = lz.fillWindow(buf, off, len);
99                 off += used;
100                 len -= used;
101                 pendingSize += used;
102 
103                 if (lzma.encodeForLZMA2())
104                     writeChunk();
105             }
106         } catch (IOException e) {
107             exception = e;
108             throw e;
109         }
110     }
111 
writeChunk()112     private void writeChunk() throws IOException {
113         int compressedSize = rc.finish();
114         int uncompressedSize = lzma.getUncompressedSize();
115 
116         assert compressedSize > 0 : compressedSize;
117         assert uncompressedSize > 0 : uncompressedSize;
118 
119         // +2 because the header of a compressed chunk is 2 bytes
120         // bigger than the header of an uncompressed chunk.
121         if (compressedSize + 2 < uncompressedSize) {
122             writeLZMA(uncompressedSize, compressedSize);
123         } else {
124             lzma.reset();
125             uncompressedSize = lzma.getUncompressedSize();
126             assert uncompressedSize > 0 : uncompressedSize;
127             writeUncompressed(uncompressedSize);
128         }
129 
130         pendingSize -= uncompressedSize;
131         lzma.resetUncompressedSize();
132         rc.reset();
133     }
134 
writeLZMA(int uncompressedSize, int compressedSize)135     private void writeLZMA(int uncompressedSize, int compressedSize)
136             throws IOException {
137         int control;
138 
139         if (propsNeeded) {
140             if (dictResetNeeded)
141                 control = 0x80 + (3 << 5);
142             else
143                 control = 0x80 + (2 << 5);
144         } else {
145             if (stateResetNeeded)
146                 control = 0x80 + (1 << 5);
147             else
148                 control = 0x80;
149         }
150 
151         control |= (uncompressedSize - 1) >>> 16;
152         outData.writeByte(control);
153 
154         outData.writeShort(uncompressedSize - 1);
155         outData.writeShort(compressedSize - 1);
156 
157         if (propsNeeded)
158             outData.writeByte(props);
159 
160         rc.write(out);
161 
162         propsNeeded = false;
163         stateResetNeeded = false;
164         dictResetNeeded = false;
165     }
166 
writeUncompressed(int uncompressedSize)167     private void writeUncompressed(int uncompressedSize) throws IOException {
168         while (uncompressedSize > 0) {
169             int chunkSize = Math.min(uncompressedSize, COMPRESSED_SIZE_MAX);
170             outData.writeByte(dictResetNeeded ? 0x01 : 0x02);
171             outData.writeShort(chunkSize - 1);
172             lz.copyUncompressed(out, uncompressedSize, chunkSize);
173             uncompressedSize -= chunkSize;
174             dictResetNeeded = false;
175         }
176 
177         stateResetNeeded = true;
178     }
179 
writeEndMarker()180     private void writeEndMarker() throws IOException {
181         assert !finished;
182 
183         if (exception != null)
184             throw exception;
185 
186         lz.setFinishing();
187 
188         try {
189             while (pendingSize > 0) {
190                 lzma.encodeForLZMA2();
191                 writeChunk();
192             }
193 
194             out.write(0x00);
195         } catch (IOException e) {
196             exception = e;
197             throw e;
198         }
199 
200         finished = true;
201     }
202 
flush()203     public void flush() throws IOException {
204         if (exception != null)
205             throw exception;
206 
207         if (finished)
208             throw new XZIOException("Stream finished or closed");
209 
210         try {
211             lz.setFlushing();
212 
213             while (pendingSize > 0) {
214                 lzma.encodeForLZMA2();
215                 writeChunk();
216             }
217 
218             out.flush();
219         } catch (IOException e) {
220             exception = e;
221             throw e;
222         }
223     }
224 
finish()225     public void finish() throws IOException {
226         if (!finished) {
227             writeEndMarker();
228 
229             try {
230                 out.finish();
231             } catch (IOException e) {
232                 exception = e;
233                 throw e;
234             }
235 
236             finished = true;
237         }
238     }
239 
close()240     public void close() throws IOException {
241         if (out != null) {
242             if (!finished) {
243                 try {
244                     writeEndMarker();
245                 } catch (IOException e) {}
246             }
247 
248             try {
249                 out.close();
250             } catch (IOException e) {
251                 if (exception == null)
252                     exception = e;
253             }
254 
255             out = null;
256         }
257 
258         if (exception != null)
259             throw exception;
260     }
261 }
262