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.buffer.ByteBufAllocator;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.handler.codec.ByteToMessageDecoder;
24  import io.netty.util.internal.ObjectUtil;
25  
26  import java.nio.ByteBuffer;
27  import java.util.List;
28  
29  /**
30   * Decompresses a {@link ByteBuf} encoded with the brotli format.
31   *
32   * See <a href="https://github.com/google/brotli">brotli</a>.
33   */
34  public final class BrotliDecoder extends ByteToMessageDecoder {
35  
36      private enum State {
37          DONE, NEEDS_MORE_INPUT, ERROR
38      }
39  
40      static {
41          try {
42              Brotli.ensureAvailability();
43          } catch (Throwable throwable) {
44              throw new ExceptionInInitializerError(throwable);
45          }
46      }
47  
48      private final int inputBufferSize;
49      private DecoderJNI.Wrapper decoder;
50      private boolean destroyed;
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 ByteBuf pull(ByteBufAllocator alloc) {
68          ByteBuffer nativeBuffer = decoder.pull();
69          // nativeBuffer actually wraps brotli's internal buffer so we need to copy its content
70          ByteBuf copy = alloc.buffer(nativeBuffer.remaining());
71          copy.writeBytes(nativeBuffer);
72          return copy;
73      }
74  
75      private State decompress(ByteBuf input, List<Object> output, ByteBufAllocator alloc) {
76          for (;;) {
77              switch (decoder.getStatus()) {
78                  case DONE:
79                      return State.DONE;
80  
81                  case OK:
82                      decoder.push(0);
83                      break;
84  
85                  case NEEDS_MORE_INPUT:
86                      if (decoder.hasOutput()) {
87                          output.add(pull(alloc));
88                      }
89  
90                      if (!input.isReadable()) {
91                          return State.NEEDS_MORE_INPUT;
92                      }
93  
94                      ByteBuffer decoderInputBuffer = decoder.getInputBuffer();
95                      decoderInputBuffer.clear();
96                      int readBytes = readBytes(input, decoderInputBuffer);
97                      decoder.push(readBytes);
98                      break;
99  
100                 case NEEDS_MORE_OUTPUT:
101                     output.add(pull(alloc));
102                     break;
103 
104                 default:
105                     return State.ERROR;
106             }
107         }
108     }
109 
110     private static int readBytes(ByteBuf in, ByteBuffer dest) {
111         int limit = Math.min(in.readableBytes(), dest.remaining());
112         ByteBuffer slice = dest.slice();
113         slice.limit(limit);
114         in.readBytes(slice);
115         dest.position(dest.position() + limit);
116         return limit;
117     }
118 
119     @Override
120     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
121         decoder = new DecoderJNI.Wrapper(inputBufferSize);
122     }
123 
124     @Override
125     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
126         if (destroyed) {
127             // Skip data received after finished.
128             in.skipBytes(in.readableBytes());
129             return;
130         }
131 
132         if (!in.isReadable()) {
133             return;
134         }
135 
136         try {
137             State state = decompress(in, out, ctx.alloc());
138             if (state == State.DONE) {
139                 destroy();
140             } else if (state == State.ERROR) {
141                 throw new DecompressionException("Brotli stream corrupted");
142             }
143         } catch (Exception e) {
144             destroy();
145             throw e;
146         }
147     }
148 
149     private void destroy() {
150         if (!destroyed) {
151             destroyed = true;
152             decoder.destroy();
153         }
154     }
155 
156     @Override
157     protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
158         try {
159             destroy();
160         } finally {
161             super.handlerRemoved0(ctx);
162         }
163     }
164 
165     @Override
166     public void channelInactive(ChannelHandlerContext ctx) throws Exception {
167         try {
168             destroy();
169         } finally {
170             super.channelInactive(ctx);
171         }
172     }
173 }