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 com.aayushatharva.brotli4j.decoder.DecoderJNI;
19 import io.netty.buffer.ByteBuf;
20 import io.netty.buffer.ByteBufAllocator;
21 import io.netty.util.internal.ObjectUtil;
22 import io.netty.util.internal.UnstableApi;
23
24 import java.io.IOException;
25 import java.nio.ByteBuffer;
26
27
28
29
30
31
32 @UnstableApi
33 public final class BrotliDecompressor implements Decompressor {
34 private final ByteBufAllocator allocator;
35 private DecoderJNI.Wrapper decoder;
36 private ByteBuf unusedInput;
37 private boolean pulledSinceLastPush;
38
39 static {
40 try {
41 Brotli.ensureAvailability();
42 } catch (Throwable throwable) {
43 throw new ExceptionInInitializerError(throwable);
44 }
45 }
46
47 BrotliDecompressor(Builder builder, ByteBufAllocator allocator) throws DecompressionException {
48 this.allocator = allocator;
49 try {
50 this.decoder = new DecoderJNI.Wrapper(builder.inputBufferSize, builder.maxOutputChunkSize);
51 } catch (IOException ioe) {
52 throw new DecompressionException(ioe);
53 }
54 }
55
56 @Override
57 public Status status() throws DecompressionException {
58 while (true) {
59 switch (decoder.getStatus()) {
60 case ERROR:
61 throw new DecompressionException("Brotli error status");
62 case DONE:
63 return Status.COMPLETE;
64 case NEEDS_MORE_INPUT:
65 if (decoder.hasOutput()) {
66 return Status.NEED_OUTPUT;
67 }
68 if (unusedInput == null) {
69 if (pulledSinceLastPush) {
70
71
72
73 push(0);
74 break;
75 }
76 return Status.NEED_INPUT;
77 }
78 addSomeInput(unusedInput);
79 if (!unusedInput.isReadable()) {
80 unusedInput.release();
81 unusedInput = null;
82 }
83 break;
84 case OK:
85 push(0);
86 break;
87 case NEEDS_MORE_OUTPUT:
88 return Status.NEED_OUTPUT;
89 default:
90 throw new AssertionError("Unknown status: " + decoder.getStatus());
91 }
92 }
93 }
94
95 @Override
96 public void addInput(ByteBuf buf) throws DecompressionException {
97 try {
98 if (unusedInput != null) {
99 throw new IllegalStateException("Not in state NEED_INPUT");
100 }
101 addSomeInput(buf);
102 } catch (Throwable t) {
103 buf.release();
104 throw t;
105 }
106 if (buf.isReadable()) {
107 this.unusedInput = buf;
108 } else {
109 buf.release();
110 }
111 }
112
113 private void addSomeInput(ByteBuf buf) {
114 ByteBuffer decoderInputBuffer = decoder.getInputBuffer();
115 decoderInputBuffer.clear();
116 int readBytes = readBytes(buf, decoderInputBuffer);
117 push(readBytes);
118 }
119
120 private void push(int length) {
121 pulledSinceLastPush = false;
122 decoder.push(length);
123 }
124
125 @Override
126 public void endOfInput() throws DecompressionException {
127 if (decoder.getStatus() == DecoderJNI.Status.NEEDS_MORE_INPUT) {
128 assert unusedInput == null : "Expected to be in NEED_INPUT state";
129 push(0);
130 if (decoder.getStatus() == DecoderJNI.Status.NEEDS_MORE_INPUT) {
131 throw new DecompressionException("Truncated brotli stream");
132 }
133 }
134 }
135
136 @Override
137 public ByteBuf takeOutput() throws DecompressionException {
138 ByteBuffer nativeBuffer = decoder.pull();
139 pulledSinceLastPush = true;
140
141
142 ByteBuf copy = allocator.buffer(nativeBuffer.remaining());
143 copy.writeBytes(nativeBuffer);
144 return copy;
145 }
146
147 @Override
148 public void close() {
149 if (decoder != null) {
150 decoder.destroy();
151 decoder = null;
152 }
153 if (unusedInput != null) {
154 unusedInput.release();
155 unusedInput = null;
156 }
157 }
158
159 private static int readBytes(ByteBuf in, ByteBuffer dest) {
160 int limit = Math.min(in.readableBytes(), dest.remaining());
161 ByteBuffer slice = dest.slice();
162 slice.limit(limit);
163 in.readBytes(slice);
164 dest.position(dest.position() + limit);
165 return limit;
166 }
167
168 @UnstableApi
169 public static Builder builder() {
170 return new Builder();
171 }
172
173 @UnstableApi
174 public static final class Builder extends AbstractDecompressorBuilder {
175 private int inputBufferSize = 8 * 1024;
176 private int maxOutputChunkSize = 64 * 1024;
177
178 Builder() {
179 }
180
181
182
183
184
185
186
187 public Builder inputBufferSize(int inputBufferSize) {
188 this.inputBufferSize = ObjectUtil.checkPositive(inputBufferSize, "inputBufferSize");
189 return this;
190 }
191
192
193
194
195
196
197
198 public Builder maxOutputChunkSize(int maxOutputChunkSize) {
199 this.maxOutputChunkSize = ObjectUtil.checkPositive(maxOutputChunkSize, "maxOutputChunkSize");
200 return this;
201 }
202
203 @Override
204 public Decompressor build(ByteBufAllocator allocator) throws DecompressionException {
205 return new DefensiveDecompressor(new BrotliDecompressor(this, allocator));
206 }
207 }
208 }