1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package io.netty.handler.codec.compression;
17
18 import io.netty.buffer.ByteBuf;
19 import io.netty.buffer.ByteBufAllocator;
20 import io.netty.util.internal.UnstableApi;
21
22 import java.util.zip.Adler32;
23 import java.util.zip.Checksum;
24
25 import static io.netty.handler.codec.compression.FastLz.BLOCK_TYPE_COMPRESSED;
26 import static io.netty.handler.codec.compression.FastLz.BLOCK_WITH_CHECKSUM;
27 import static io.netty.handler.codec.compression.FastLz.MAGIC_NUMBER;
28 import static io.netty.handler.codec.compression.FastLz.decompress;
29
30
31
32
33
34
35
36
37
38 @UnstableApi
39 public final class FastLzFrameDecompressor extends InputBufferingDecompressor {
40
41
42
43 private enum State {
44 INIT_BLOCK,
45 INIT_BLOCK_PARAMS,
46 DECOMPRESS_DATA,
47 COMPLETE
48 }
49
50 private State currentState = State.INIT_BLOCK;
51
52
53
54
55 private final ByteBufChecksum checksum;
56
57
58
59
60 private int chunkLength;
61
62
63
64
65
66 private int originalLength;
67
68
69
70
71 private boolean isCompressed;
72
73
74
75
76 private boolean hasChecksum;
77
78
79
80
81 private int currentChecksum;
82
83 FastLzFrameDecompressor(Builder builder, ByteBufAllocator allocator) {
84 super(allocator);
85 this.checksum = builder.checksum == null ? null : ByteBufChecksum.wrapChecksum(builder.checksum);
86 }
87
88 @Override
89 void processInput(ByteBuf buf) throws DecompressionException {
90 switch (currentState) {
91 case INIT_BLOCK:
92 if (buf.readableBytes() < 4) {
93 break;
94 }
95
96 final int magic = buf.readUnsignedMedium();
97 if (magic != MAGIC_NUMBER) {
98 throw new DecompressionException("unexpected block identifier");
99 }
100
101 final byte options = buf.readByte();
102 isCompressed = (options & 0x01) == BLOCK_TYPE_COMPRESSED;
103 hasChecksum = (options & 0x10) == BLOCK_WITH_CHECKSUM;
104
105 currentState = State.INIT_BLOCK_PARAMS;
106
107 case INIT_BLOCK_PARAMS:
108 if (buf.readableBytes() < 2 + (isCompressed ? 2 : 0) + (hasChecksum ? 4 : 0)) {
109 break;
110 }
111 currentChecksum = hasChecksum ? buf.readInt() : 0;
112 chunkLength = buf.readUnsignedShort();
113 originalLength = isCompressed ? buf.readUnsignedShort() : chunkLength;
114
115 currentState = State.DECOMPRESS_DATA;
116
117 case DECOMPRESS_DATA:
118 break;
119 default:
120 throw new IllegalStateException();
121 }
122 }
123
124 @Override
125 public Status status() throws DecompressionException {
126 switch (currentState) {
127 case INIT_BLOCK:
128 case INIT_BLOCK_PARAMS:
129 return Status.NEED_INPUT;
130 case DECOMPRESS_DATA:
131 if (available() < chunkLength) {
132 return Status.NEED_INPUT;
133 } else {
134 return Status.NEED_OUTPUT;
135 }
136 case COMPLETE:
137 return Status.COMPLETE;
138 default:
139 throw new AssertionError("Unknown state: " + currentState);
140 }
141 }
142
143 @Override
144 public void endOfInput() throws DecompressionException {
145 if (currentState != State.INIT_BLOCK) {
146 throw new DecompressionException("Unexpected end of input");
147 }
148 currentState = State.COMPLETE;
149 }
150
151 @Override
152 ByteBuf processOutput(ByteBuf in) throws DecompressionException {
153 final int chunkLength = this.chunkLength;
154 if (in.readableBytes() < chunkLength) {
155 throw new IllegalStateException("Not in state NEED_OUTPUT");
156 }
157
158 final int idx = in.readerIndex();
159 final int originalLength = this.originalLength;
160
161 ByteBuf output = null;
162
163 try {
164 if (isCompressed) {
165 output = allocator.buffer(originalLength);
166 int outputOffset = output.writerIndex();
167 final int decompressedBytes = decompress(in, idx, chunkLength,
168 output, outputOffset, originalLength);
169 if (originalLength != decompressedBytes) {
170 throw new DecompressionException(String.format(
171 "stream corrupted: originalLength(%d) and actual length(%d) mismatch",
172 originalLength, decompressedBytes));
173 }
174 output.writerIndex(output.writerIndex() + decompressedBytes);
175 } else {
176 output = in.retainedSlice(idx, chunkLength);
177 }
178
179 final ByteBufChecksum checksum = this.checksum;
180 if (hasChecksum && checksum != null) {
181 checksum.reset();
182 checksum.update(output, output.readerIndex(), output.readableBytes());
183 final int checksumResult = (int) checksum.getValue();
184 if (checksumResult != currentChecksum) {
185 throw new DecompressionException(String.format(
186 "stream corrupted: mismatching checksum: %d (expected: %d)",
187 checksumResult, currentChecksum));
188 }
189 }
190
191 in.skipBytes(chunkLength);
192
193 currentState = State.INIT_BLOCK;
194 ByteBuf b = output;
195 output = null;
196 return b;
197 } finally {
198 if (output != null) {
199 output.release();
200 }
201 }
202 }
203
204 @UnstableApi
205 public static Builder builder() {
206 return new Builder();
207 }
208
209 @UnstableApi
210 public static final class Builder extends AbstractDecompressorBuilder {
211 private Checksum checksum;
212
213 Builder() {
214 }
215
216
217
218
219
220
221
222 @UnstableApi
223 public Builder checksum(Checksum checksum) {
224 this.checksum = checksum;
225 return this;
226 }
227
228
229
230
231
232
233 @UnstableApi
234 public Builder defaultChecksum() {
235 return checksum(new Adler32());
236 }
237
238 @Override
239 @UnstableApi
240 public Decompressor build(ByteBufAllocator allocator) throws DecompressionException {
241 return new DefensiveDecompressor(new FastLzFrameDecompressor(this, allocator));
242 }
243 }
244 }