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.netty5.handler.codec.compression;
18  
19  import com.aayushatharva.brotli4j.decoder.DecoderJNI;
20  import io.netty5.buffer.api.Buffer;
21  import io.netty5.buffer.api.BufferAllocator;
22  import io.netty5.util.internal.ObjectUtil;
23  
24  import java.io.IOException;
25  import java.nio.ByteBuffer;
26  import java.util.function.Supplier;
27  
28  /**
29   * Uncompresses a {@link Buffer} encoded with the brotli format.
30   *
31   * See <a href="https://github.com/google/brotli">brotli</a>.
32   */
33  public final class BrotliDecompressor implements Decompressor {
34  
35      static {
36          try {
37              Brotli.ensureAvailability();
38          } catch (Throwable throwable) {
39              throw new ExceptionInInitializerError(throwable);
40          }
41      }
42  
43      private enum State {
44          PROCESSING,
45          FINISHED,
46          CLOSED
47      }
48  
49      private State state = State.PROCESSING;
50  
51      private final DecoderJNI.Wrapper decoder;
52  
53      /**
54       * Creates a new factory for {@link BrotliDecompressor}s with a default 8kB input buffer
55       *
56       * @return the factory.
57       */
58      public static Supplier<BrotliDecompressor> newFactory() {
59          return newFactory(8 * 1024);
60      }
61  
62      /**
63       * Creates a new factory for {@link BrotliDecompressor}s.
64       *
65       * @param inputBufferSize desired size of the input buffer in bytes
66       * @return the factory.
67       */
68      public static Supplier<BrotliDecompressor> newFactory(int inputBufferSize) {
69          ObjectUtil.checkPositive(inputBufferSize, "inputBufferSize");
70          return () -> {
71              try {
72                  return new BrotliDecompressor(inputBufferSize);
73              } catch (IOException e) {
74                  throw new DecompressionException(e);
75              }
76          };
77      }
78  
79      /**
80       * Creates a new BrotliDecoder
81       *
82       * @param inputBufferSize desired size of the input buffer in bytes
83       */
84      private BrotliDecompressor(int inputBufferSize) throws IOException {
85          decoder = new DecoderJNI.Wrapper(inputBufferSize);
86      }
87  
88      private Buffer pull(BufferAllocator alloc) {
89          ByteBuffer nativeBuffer = decoder.pull();
90          // nativeBuffer actually wraps brotli's internal buffer so we need to copy its content
91          return alloc.copyOf(nativeBuffer);
92      }
93  
94      private static int readBytes(Buffer in, ByteBuffer dest) {
95          int limit = Math.min(in.readableBytes(), dest.remaining());
96          ByteBuffer slice = dest.slice();
97          slice.limit(limit);
98          in.readBytes(slice);
99          dest.position(dest.position() + limit);
100         return limit;
101     }
102 
103     @Override
104     public Buffer decompress(Buffer input, BufferAllocator allocator) throws DecompressionException {
105         switch (state) {
106             case CLOSED:
107                 throw new DecompressionException("Decompressor closed");
108             case FINISHED:
109                 return allocator.allocate(0);
110             case PROCESSING:
111                 for (;;) {
112                     switch (decoder.getStatus()) {
113                         case DONE:
114                             state = State.FINISHED;
115                             return null;
116                         case OK:
117                             decoder.push(0);
118                             break;
119                         case NEEDS_MORE_INPUT:
120                             if (decoder.hasOutput()) {
121                                 return pull(allocator);
122                             }
123 
124                             if (input.readableBytes() == 0) {
125                                 return null;
126                             }
127 
128                             ByteBuffer decoderInputBuffer = decoder.getInputBuffer();
129                             decoderInputBuffer.clear();
130                             int readBytes = readBytes(input, decoderInputBuffer);
131                             decoder.push(readBytes);
132                             break;
133                         case NEEDS_MORE_OUTPUT:
134                             return pull(allocator);
135                         default:
136                             state = State.FINISHED;
137                             throw new DecompressionException("Brotli stream corrupted");
138                     }
139                 }
140             default:
141                 throw new IllegalStateException();
142         }
143     }
144 
145     @Override
146     public boolean isFinished() {
147         return state != State.PROCESSING;
148     }
149 
150     @Override
151     public boolean isClosed() {
152         return state == State.CLOSED;
153     }
154 
155     @Override
156     public void close() {
157         if (state != State.FINISHED) {
158             state = State.FINISHED;
159             decoder.destroy();
160         }
161     }
162 }