View Javadoc
1   /*
2    * Copyright 2021 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  
17  package io.netty.handler.codec.compression;
18  
19  import com.aayushatharva.brotli4j.decoder.DecoderJNI;
20  import io.netty.buffer.ByteBuf;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.handler.codec.ByteToMessageDecoder;
23  import io.netty.util.internal.ObjectUtil;
24  
25  import java.nio.ByteBuffer;
26  import java.util.List;
27  
28  /**
29   * Decompresses a {@link ByteBuf} encoded with the brotli format.
30   *
31   * See <a href="https://github.com/google/brotli">brotli</a>.
32   */
33  public final class BrotliDecoder extends ByteToMessageDecoder {
34  
35      private enum State {
36          DONE, NEEDS_MORE_INPUT, ERROR
37      }
38  
39      static {
40          try {
41              Brotli.ensureAvailability();
42          } catch (Throwable throwable) {
43              throw new ExceptionInInitializerError(throwable);
44          }
45      }
46  
47      private final int inputBufferSize;
48      private DecoderJNI.Wrapper decoder;
49      private boolean destroyed;
50      private boolean needsRead;
51  
52      /**
53       * Creates a new BrotliDecoder with a default 8kB input buffer
54       */
55      public BrotliDecoder() {
56          this(8 * 1024);
57      }
58  
59      /**
60       * Creates a new BrotliDecoder
61       * @param inputBufferSize desired size of the input buffer in bytes
62       */
63      public BrotliDecoder(int inputBufferSize) {
64          this.inputBufferSize = ObjectUtil.checkPositive(inputBufferSize, "inputBufferSize");
65      }
66  
67      private void forwardOutput(ChannelHandlerContext ctx) {
68          ByteBuffer nativeBuffer = decoder.pull();
69          // nativeBuffer actually wraps brotli's internal buffer so we need to copy its content
70          ByteBuf copy = ctx.alloc().buffer(nativeBuffer.remaining());
71          copy.writeBytes(nativeBuffer);
72          needsRead = false;
73          ctx.fireChannelRead(copy);
74      }
75  
76      private State decompress(ChannelHandlerContext ctx, ByteBuf input) {
77          for (;;) {
78              switch (decoder.getStatus()) {
79                  case DONE:
80                      return State.DONE;
81  
82                  case OK:
83                      decoder.push(0);
84                      break;
85  
86                  case NEEDS_MORE_INPUT:
87                      if (decoder.hasOutput()) {
88                          forwardOutput(ctx);
89                      }
90  
91                      if (!input.isReadable()) {
92                          return State.NEEDS_MORE_INPUT;
93                      }
94  
95                      ByteBuffer decoderInputBuffer = decoder.getInputBuffer();
96                      decoderInputBuffer.clear();
97                      int readBytes = readBytes(input, decoderInputBuffer);
98                      decoder.push(readBytes);
99                      break;
100 
101                 case NEEDS_MORE_OUTPUT:
102                     forwardOutput(ctx);
103                     break;
104 
105                 default:
106                     return State.ERROR;
107             }
108         }
109     }
110 
111     private static int readBytes(ByteBuf in, ByteBuffer dest) {
112         int limit = Math.min(in.readableBytes(), dest.remaining());
113         ByteBuffer slice = dest.slice();
114         slice.limit(limit);
115         in.readBytes(slice);
116         dest.position(dest.position() + limit);
117         return limit;
118     }
119 
120     @Override
121     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
122         decoder = new DecoderJNI.Wrapper(inputBufferSize);
123     }
124 
125     @Override
126     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
127         needsRead = true;
128         if (destroyed) {
129             // Skip data received after finished.
130             in.skipBytes(in.readableBytes());
131             return;
132         }
133 
134         if (!in.isReadable()) {
135             return;
136         }
137 
138         try {
139             State state = decompress(ctx, in);
140             if (state == State.DONE) {
141                 destroy();
142             } else if (state == State.ERROR) {
143                 throw new DecompressionException("Brotli stream corrupted");
144             }
145         } catch (Exception e) {
146             destroy();
147             throw e;
148         }
149     }
150 
151     private void destroy() {
152         if (!destroyed) {
153             destroyed = true;
154             decoder.destroy();
155         }
156     }
157 
158     @Override
159     protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
160         try {
161             destroy();
162         } finally {
163             super.handlerRemoved0(ctx);
164         }
165     }
166 
167     @Override
168     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
169         try {
170             destroy();
171         } finally {
172             super.channelInactive(ctx);
173         }
174     }
175 
176     @Override
177     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
178         // Discard bytes of the cumulation buffer if needed.
179         discardSomeReadBytes();
180 
181         if (needsRead && !ctx.channel().config().isAutoRead()) {
182             ctx.read();
183         }
184         ctx.fireChannelReadComplete();
185     }
186 }