View Javadoc
1   /*
2    * Copyright 2025 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
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   * Decompresses a {@link ByteBuf} encoded with the brotli format.
29   *
30   * See <a href="https://github.com/google/brotli">brotli</a>.
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         // nativeBuffer actually wraps brotli's internal buffer so we need to copy its content
127         // size limited by maxOutputChunkSize
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          * Desired size of the input buffer in bytes. Default 8K.
169          *
170          * @param inputBufferSize desired size of the input buffer in bytes
171          * @return This builder
172          */
173         public Builder inputBufferSize(int inputBufferSize) {
174             this.inputBufferSize = ObjectUtil.checkPositive(inputBufferSize, "inputBufferSize");
175             return this;
176         }
177 
178         /**
179          * Number of bytes of output to consume at a time. Default 64K.
180          *
181          * @param maxOutputChunkSize Maximum output chunk size
182          * @return This builder
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 }