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