1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty5.handler.codec.compression;
17
18 import io.netty5.buffer.BufferInputStream;
19 import io.netty5.buffer.BufferOutputStream;
20 import io.netty5.buffer.api.Buffer;
21 import io.netty5.buffer.api.BufferAllocator;
22 import io.netty5.util.internal.logging.InternalLogger;
23 import io.netty5.util.internal.logging.InternalLoggerFactory;
24 import lzma.sdk.lzma.Base;
25 import lzma.sdk.lzma.Encoder;
26
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.util.function.Supplier;
30
31 import static lzma.sdk.lzma.Encoder.EMatchFinderTypeBT4;
32
33
34
35
36
37
38
39
40 public final class LzmaCompressor implements Compressor {
41 private static final InternalLogger logger = InternalLoggerFactory.getInstance(LzmaCompressor.class);
42
43 private static final int MEDIUM_DICTIONARY_SIZE = 1 << 16;
44
45 private static final int MIN_FAST_BYTES = 5;
46 private static final int MEDIUM_FAST_BYTES = 0x20;
47 private static final int MAX_FAST_BYTES = Base.kMatchMaxLen;
48
49 private static final int DEFAULT_MATCH_FINDER = EMatchFinderTypeBT4;
50
51 private static final int DEFAULT_LC = 3;
52 private static final int DEFAULT_LP = 0;
53 private static final int DEFAULT_PB = 2;
54
55
56
57
58 private final Encoder encoder;
59
60
61
62
63
64
65
66
67
68
69
70
71
72 private final byte properties;
73
74
75
76
77 private final int littleEndianDictionarySize;
78
79
80
81
82 private static boolean warningLogged;
83
84 private enum State {
85 PROCESSING,
86 FINISHED,
87 CLOSED
88 }
89
90 private State state = State.PROCESSING;
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113 private LzmaCompressor(int lc, int lp, int pb, int dictionarySize, boolean endMarkerMode, int numFastBytes) {
114 encoder = new Encoder();
115 encoder.setDictionarySize(dictionarySize);
116 encoder.setEndMarkerMode(endMarkerMode);
117 encoder.setMatchFinder(DEFAULT_MATCH_FINDER);
118 encoder.setNumFastBytes(numFastBytes);
119 encoder.setLcLpPb(lc, lp, pb);
120
121 properties = (byte) ((pb * 5 + lp) * 9 + lc);
122 littleEndianDictionarySize = Integer.reverseBytes(dictionarySize);
123 }
124
125
126
127
128
129
130 public static Supplier<LzmaCompressor> newFactory() {
131 return newFactory(LzmaCompressor.MEDIUM_DICTIONARY_SIZE);
132 }
133
134
135
136
137
138
139
140 public static Supplier<LzmaCompressor> newFactory(int lc, int lp, int pb) {
141 return newFactory(lc, lp, pb, LzmaCompressor.MEDIUM_DICTIONARY_SIZE);
142 }
143
144
145
146
147
148
149
150
151
152 public static Supplier<LzmaCompressor> newFactory(int dictionarySize) {
153 return newFactory(LzmaCompressor.DEFAULT_LC, LzmaCompressor.DEFAULT_LP,
154 LzmaCompressor.DEFAULT_PB, dictionarySize);
155 }
156
157
158
159
160
161
162
163 public static Supplier<LzmaCompressor> newFactory(int lc, int lp, int pb, int dictionarySize) {
164 return newFactory(lc, lp, pb, dictionarySize, false, LzmaCompressor.MEDIUM_FAST_BYTES);
165 }
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189 public static Supplier<LzmaCompressor> newFactory(int lc, int lp, int pb, int dictionarySize,
190 boolean endMarkerMode, int numFastBytes) {
191 if (lc < 0 || lc > 8) {
192 throw new IllegalArgumentException("lc: " + lc + " (expected: 0-8)");
193 }
194 if (lp < 0 || lp > 4) {
195 throw new IllegalArgumentException("lp: " + lp + " (expected: 0-4)");
196 }
197 if (pb < 0 || pb > 4) {
198 throw new IllegalArgumentException("pb: " + pb + " (expected: 0-4)");
199 }
200 if (lc + lp > 4) {
201 if (!warningLogged) {
202 logger.warn("The latest versions of LZMA libraries (for example, XZ Utils) " +
203 "has an additional requirement: lc + lp <= 4. Data which don't follow " +
204 "this requirement cannot be decompressed with this libraries.");
205 warningLogged = true;
206 }
207 }
208 if (dictionarySize < 0) {
209 throw new IllegalArgumentException("dictionarySize: " + dictionarySize + " (expected: 0+)");
210 }
211 if (numFastBytes < MIN_FAST_BYTES || numFastBytes > MAX_FAST_BYTES) {
212 throw new IllegalArgumentException(String.format(
213 "numFastBytes: %d (expected: %d-%d)", numFastBytes, MIN_FAST_BYTES, MAX_FAST_BYTES
214 ));
215 }
216
217 return () -> new LzmaCompressor(lc, lp, pb, dictionarySize, endMarkerMode, numFastBytes);
218 }
219
220 @Override
221 public Buffer compress(Buffer in, BufferAllocator allocator) throws CompressionException {
222 switch (state) {
223 case CLOSED:
224 throw new CompressionException("Compressor closed");
225 case FINISHED:
226 return allocator.allocate(0);
227 case PROCESSING:
228
229 final int length = in.readableBytes();
230 Buffer out = allocateBuffer(in, allocator);
231 try {
232 try (InputStream bbIn = new BufferInputStream(in.send());
233 BufferOutputStream bbOut = new BufferOutputStream(out)) {
234 bbOut.writeByte(properties);
235 bbOut.writeInt(littleEndianDictionarySize);
236 bbOut.writeLong(Long.reverseBytes(length));
237 encoder.code(bbIn, bbOut, -1, -1, null);
238 }
239 } catch (IOException e) {
240 out.close();
241 throw new CompressionException(e);
242 } catch (Throwable cause) {
243 out.close();
244 throw cause;
245 }
246 return out;
247 default:
248 throw new IllegalStateException();
249 }
250 }
251
252 private static Buffer allocateBuffer(Buffer in, BufferAllocator allocator) {
253 final int length = in.readableBytes();
254 final int maxOutputLength = maxOutputBufferLength(length);
255 return allocator.allocate(maxOutputLength);
256 }
257
258
259
260
261 private static int maxOutputBufferLength(int inputLength) {
262 double factor;
263 if (inputLength < 200) {
264 factor = 1.5;
265 } else if (inputLength < 500) {
266 factor = 1.2;
267 } else if (inputLength < 1000) {
268 factor = 1.1;
269 } else if (inputLength < 10000) {
270 factor = 1.05;
271 } else {
272 factor = 1.02;
273 }
274 return 13 + (int) (inputLength * factor);
275 }
276
277 @Override
278 public Buffer finish(BufferAllocator allocator) {
279 switch (state) {
280 case CLOSED:
281 throw new CompressionException("Compressor closed");
282 case FINISHED:
283 case PROCESSING:
284 state = State.FINISHED;
285 return allocator.allocate(0);
286 default:
287 throw new IllegalStateException();
288 }
289 }
290
291 @Override
292 public boolean isFinished() {
293 return state != State.PROCESSING;
294 }
295
296 @Override
297 public boolean isClosed() {
298 return state == State.CLOSED;
299 }
300
301 @Override
302 public void close() {
303 state = State.CLOSED;
304 }
305 }