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.github.luben.zstd.ZstdInputStreamNoFinalizer;
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufAllocator;
21  import io.netty.buffer.Unpooled;
22  import io.netty.util.internal.ObjectUtil;
23  import io.netty.util.internal.UnstableApi;
24  
25  import java.io.IOException;
26  import java.io.InputStream;
27  
28  /**
29   * Decompresses a compressed block {@link ByteBuf} using the Zstandard algorithm.
30   * See <a href="https://facebook.github.io/zstd">Zstandard</a>.
31   */
32  @UnstableApi
33  public final class ZstdDecompressor implements Decompressor {
34      /**
35       * Default upper bound on the {@code Window_Log} accepted by the decompressor.
36       * {@code 27} corresponds to a 128 MiB decompression window.
37       */
38      public static final int DEFAULT_MAX_WINDOW_LOG = 27;
39      private static final int MIN_WINDOW_LOG = 10;
40      private static final int MAX_WINDOW_LOG = 31;
41      private static final int DEFAULT_MAX_FORWARD_BYTES = CompressionUtil.DEFAULT_MAX_FORWARD_BYTES;
42  
43      private final ByteBufAllocator allocator;
44  
45      private final MutableByteBufInputStream mutableInput = new MutableByteBufInputStream();
46      private final ZstdInputStreamNoFinalizer output;
47  
48      ZstdDecompressor(Builder builder, ByteBufAllocator allocator) {
49          // Don't use static here as we want to still allow to load the classes.
50          try {
51              Zstd.ensureAvailability();
52          } catch (Throwable throwable) {
53              throw new ExceptionInInitializerError(throwable);
54          }
55          this.allocator = allocator;
56          ZstdInputStreamNoFinalizer output = null;
57          try {
58              output = new ZstdInputStreamNoFinalizer(mutableInput);
59              output.setContinuous(true);
60              output.setLongMax(builder.maxWindowLog);
61              this.output = output;
62          } catch (IOException e) {
63              if (output != null) {
64                  try {
65                      output.close();
66                  } catch (IOException closeException) {
67                      e.addSuppressed(closeException);
68                  }
69              }
70              throw new DecompressionException(e);
71          }
72      }
73  
74      @Override
75      public Status status() throws DecompressionException {
76          try {
77              if (output.available() == 0) {
78                  if (!output.getContinuous()) {
79                      return Status.COMPLETE;
80                  }
81                  return Status.NEED_INPUT;
82              }
83              return Status.NEED_OUTPUT;
84          } catch (IOException e) {
85              throw new DecompressionException(e);
86          }
87      }
88  
89      @Override
90      public void addInput(ByteBuf buf) throws DecompressionException {
91          if (!buf.isReadable()) {
92              buf.release();
93              return;
94          }
95          if (mutableInput.current != null) {
96              mutableInput.current.release();
97          }
98          mutableInput.current = buf;
99      }
100 
101     @Override
102     public void endOfInput() throws DecompressionException {
103         try {
104             output.setContinuous(false);
105             if (output.read() != -1) {
106                 throw new DecompressionException("Unexpected output after end of input");
107             }
108         } catch (IOException e) {
109             throw new DecompressionException(e);
110         }
111     }
112 
113     @Override
114     public ByteBuf takeOutput() throws DecompressionException {
115         ByteBuf buf = allocator.buffer(DEFAULT_MAX_FORWARD_BYTES, DEFAULT_MAX_FORWARD_BYTES);
116         try {
117             buf.writeBytes(output, DEFAULT_MAX_FORWARD_BYTES);
118         } catch (IOException e) {
119             buf.release();
120             throw new DecompressionException(e);
121         }
122         if (buf.isReadable()) {
123             return buf;
124         }
125         buf.release();
126         return Unpooled.EMPTY_BUFFER;
127     }
128 
129     @Override
130     public void close() {
131         if (mutableInput.current != null) {
132             mutableInput.current.release();
133             mutableInput.current = null;
134         }
135         try {
136             output.close();
137         } catch (IOException ignored) {
138             // ignore
139         }
140     }
141 
142     @UnstableApi
143     public static Builder builder() {
144         return new Builder();
145     }
146 
147     @UnstableApi
148     public static final class Builder extends AbstractDecompressorBuilder {
149         private int maxWindowLog = DEFAULT_MAX_WINDOW_LOG;
150 
151         Builder() {
152         }
153 
154         /**
155          * Set the upper bound on the accepted {@code Window_Log}.
156          * <p>
157          * The window log size bounds the memory usage of the sliding window for ZSTD frame decompression. Frames
158          * declaring a larger window will be rejected to bound the memory the decompressor may allocate per stream.
159          *
160          * @param maxWindowLog upper bound on the {@code Window_Log} field of incoming frames; must be in
161          *                     {@code [10, 31]}
162          * @return This builder
163          */
164         @UnstableApi
165         public Builder maxWindowLog(int maxWindowLog) {
166             this.maxWindowLog = ObjectUtil.checkInRange(
167                     maxWindowLog, MIN_WINDOW_LOG, MAX_WINDOW_LOG, "maxWindowLog");
168             return this;
169         }
170 
171         @Override
172         public Decompressor build(ByteBufAllocator allocator) throws DecompressionException {
173             return new DefensiveDecompressor(new ZstdDecompressor(this, allocator));
174         }
175     }
176 
177     private static final class MutableByteBufInputStream extends InputStream {
178         ByteBuf current;
179 
180         @Override
181         public int read() {
182             if (available() == 0) {
183                 return -1;
184             }
185             return current.readByte() & 0xff;
186         }
187 
188         @Override
189         public int read(byte[] b, int off, int len) {
190             int available = available();
191             if (available == 0) {
192                 return -1;
193             }
194 
195             len = Math.min(available, len);
196             current.readBytes(b, off, len);
197             return len;
198         }
199 
200         @Override
201         public int available() {
202             return current == null ? 0 : current.readableBytes();
203         }
204     }
205 }