Introduce BrotliEncoder (#11256)
Motivation: Currently, Netty only has BrotliDecoder which can decode Brotli encoded data. However, BrotliEncoder is missing which will encode normal data to Brotli encoded data. Modification: Added BrotliEncoder and CompressionOption Result: Fixes #6899. Co-authored-by: Norman Maurer <norman_maurer@apple.com>
This commit is contained in:
parent
7825fa8d7a
commit
fef761d03e
@ -254,3 +254,11 @@ This private header is also used by Apple's open source
|
||||
* license/LICENSE.dnsinfo.txt (Apple Public Source License 2.0)
|
||||
* HOMEPAGE:
|
||||
* https://www.opensource.apple.com/source/configd/configd-453.19/dnsinfo/dnsinfo.h
|
||||
|
||||
This product optionally depends on 'Brotli4j', Brotli compression and
|
||||
decompression for Java., which can be obtained at:
|
||||
|
||||
* LICENSE:
|
||||
* license/LICENSE.brotli4j.txt (Apache License 2.0)
|
||||
* HOMEPAGE:
|
||||
* https://github.com/hyperxpro/Brotli4j
|
||||
|
@ -74,7 +74,7 @@
|
||||
<dependency>
|
||||
<groupId>com.aayushatharva.brotli4j</groupId>
|
||||
<artifactId>brotli4j</artifactId>
|
||||
<scope>test</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aayushatharva.brotli4j</groupId>
|
||||
|
@ -17,8 +17,15 @@ package io.netty.handler.codec.http;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.codec.compression.Brotli;
|
||||
import io.netty.handler.codec.compression.BrotliEncoder;
|
||||
import io.netty.handler.codec.compression.ZlibCodecFactory;
|
||||
import io.netty.handler.codec.compression.ZlibWrapper;
|
||||
import io.netty.handler.codec.compression.BrotliOptions;
|
||||
import io.netty.handler.codec.compression.CompressionOptions;
|
||||
import io.netty.handler.codec.compression.DeflateOptions;
|
||||
import io.netty.handler.codec.compression.GzipOptions;
|
||||
import io.netty.handler.codec.compression.StandardCompressionOptions;
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
|
||||
/**
|
||||
@ -30,6 +37,11 @@ import io.netty.util.internal.ObjectUtil;
|
||||
*/
|
||||
public class HttpContentCompressor extends HttpContentEncoder {
|
||||
|
||||
private final boolean supportsCompressionOptions;
|
||||
private final BrotliOptions brotliOptions;
|
||||
private final GzipOptions gzipOptions;
|
||||
private final DeflateOptions deflateOptions;
|
||||
|
||||
private final int compressionLevel;
|
||||
private final int windowBits;
|
||||
private final int memLevel;
|
||||
@ -53,6 +65,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
||||
* best compression. {@code 0} means no compression. The default
|
||||
* compression level is {@code 6}.
|
||||
*/
|
||||
@Deprecated
|
||||
public HttpContentCompressor(int compressionLevel) {
|
||||
this(compressionLevel, 15, 8, 0);
|
||||
}
|
||||
@ -76,6 +89,7 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
||||
* memory. Larger values result in better and faster compression
|
||||
* at the expense of memory usage. The default value is {@code 8}
|
||||
*/
|
||||
@Deprecated
|
||||
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
|
||||
this(compressionLevel, windowBits, memLevel, 0);
|
||||
}
|
||||
@ -103,11 +117,80 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
||||
* body exceeds the threshold. The value should be a non negative
|
||||
* number. {@code 0} will enable compression for all responses.
|
||||
*/
|
||||
@Deprecated
|
||||
public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
|
||||
this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
|
||||
this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
|
||||
this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
|
||||
this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
|
||||
this.brotliOptions = null;
|
||||
this.gzipOptions = null;
|
||||
this.deflateOptions = null;
|
||||
supportsCompressionOptions = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HttpContentCompressor} Instance with specified
|
||||
* {@link CompressionOptions}s and contentSizeThreshold set to {@code 0}
|
||||
*
|
||||
* @param compressionOptions {@link CompressionOptions} or {@code null} if the default
|
||||
* should be used.
|
||||
*/
|
||||
public HttpContentCompressor(CompressionOptions... compressionOptions) {
|
||||
this(0, compressionOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link HttpContentCompressor} instance with specified
|
||||
* {@link CompressionOptions}s
|
||||
*
|
||||
* @param contentSizeThreshold
|
||||
* The response body is compressed when the size of the response
|
||||
* body exceeds the threshold. The value should be a non negative
|
||||
* number. {@code 0} will enable compression for all responses.
|
||||
* @param compressionOptions {@link CompressionOptions} or {@code null}
|
||||
* if the default should be used.
|
||||
*/
|
||||
public HttpContentCompressor(int contentSizeThreshold, CompressionOptions... compressionOptions) {
|
||||
this.contentSizeThreshold = ObjectUtil.checkPositiveOrZero(contentSizeThreshold, "contentSizeThreshold");
|
||||
BrotliOptions brotliOptions = null;
|
||||
GzipOptions gzipOptions = null;
|
||||
DeflateOptions deflateOptions = null;
|
||||
if (compressionOptions == null || compressionOptions.length == 0) {
|
||||
brotliOptions = StandardCompressionOptions.brotli();
|
||||
gzipOptions = StandardCompressionOptions.gzip();
|
||||
deflateOptions = StandardCompressionOptions.deflate();
|
||||
} else {
|
||||
ObjectUtil.deepCheckNotNull("compressionOptionsIterable", compressionOptions);
|
||||
for (CompressionOptions compressionOption : compressionOptions) {
|
||||
if (compressionOption instanceof BrotliOptions) {
|
||||
brotliOptions = (BrotliOptions) compressionOption;
|
||||
} else if (compressionOption instanceof GzipOptions) {
|
||||
gzipOptions = (GzipOptions) compressionOption;
|
||||
} else if (compressionOption instanceof DeflateOptions) {
|
||||
deflateOptions = (DeflateOptions) compressionOption;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
|
||||
": " + compressionOption);
|
||||
}
|
||||
}
|
||||
if (brotliOptions == null) {
|
||||
brotliOptions = StandardCompressionOptions.brotli();
|
||||
}
|
||||
if (gzipOptions == null) {
|
||||
gzipOptions = StandardCompressionOptions.gzip();
|
||||
}
|
||||
if (deflateOptions == null) {
|
||||
deflateOptions = StandardCompressionOptions.deflate();
|
||||
}
|
||||
}
|
||||
this.brotliOptions = brotliOptions;
|
||||
this.gzipOptions = gzipOptions;
|
||||
this.deflateOptions = deflateOptions;
|
||||
this.compressionLevel = -1;
|
||||
this.windowBits = -1;
|
||||
this.memLevel = -1;
|
||||
supportsCompressionOptions = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -131,30 +214,109 @@ public class HttpContentCompressor extends HttpContentEncoder {
|
||||
return null;
|
||||
}
|
||||
|
||||
ZlibWrapper wrapper = determineWrapper(acceptEncoding);
|
||||
if (wrapper == null) {
|
||||
return null;
|
||||
}
|
||||
if (supportsCompressionOptions) {
|
||||
String targetContentEncoding = determineEncoding(acceptEncoding);
|
||||
if (targetContentEncoding == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String targetContentEncoding;
|
||||
switch (wrapper) {
|
||||
case GZIP:
|
||||
targetContentEncoding = "gzip";
|
||||
break;
|
||||
case ZLIB:
|
||||
targetContentEncoding = "deflate";
|
||||
break;
|
||||
default:
|
||||
if (targetContentEncoding.equals("gzip")) {
|
||||
return new Result(targetContentEncoding,
|
||||
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
|
||||
ZlibWrapper.GZIP, gzipOptions.compressionLevel()
|
||||
, gzipOptions.windowBits(), gzipOptions.memLevel())));
|
||||
}
|
||||
if (targetContentEncoding.equals("deflate")) {
|
||||
return new Result(targetContentEncoding,
|
||||
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
|
||||
ZlibWrapper.ZLIB, deflateOptions.compressionLevel(),
|
||||
deflateOptions.windowBits(), deflateOptions.memLevel())));
|
||||
}
|
||||
if (targetContentEncoding.equals("br")) {
|
||||
return new Result(targetContentEncoding,
|
||||
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), new BrotliEncoder(brotliOptions.parameters())));
|
||||
}
|
||||
throw new Error();
|
||||
}
|
||||
} else {
|
||||
ZlibWrapper wrapper = determineWrapper(acceptEncoding);
|
||||
if (wrapper == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Result(
|
||||
targetContentEncoding,
|
||||
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
|
||||
wrapper, compressionLevel, windowBits, memLevel)));
|
||||
String targetContentEncoding;
|
||||
switch (wrapper) {
|
||||
case GZIP:
|
||||
targetContentEncoding = "gzip";
|
||||
break;
|
||||
case ZLIB:
|
||||
targetContentEncoding = "deflate";
|
||||
break;
|
||||
default:
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
return new Result(
|
||||
targetContentEncoding,
|
||||
new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
|
||||
wrapper, compressionLevel, windowBits, memLevel)));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("FloatingPointEquality")
|
||||
protected String determineEncoding(String acceptEncoding) {
|
||||
float starQ = -1.0f;
|
||||
float brQ = -1.0f;
|
||||
float gzipQ = -1.0f;
|
||||
float deflateQ = -1.0f;
|
||||
for (String encoding : acceptEncoding.split(",")) {
|
||||
float q = 1.0f;
|
||||
int equalsPos = encoding.indexOf('=');
|
||||
if (equalsPos != -1) {
|
||||
try {
|
||||
q = Float.parseFloat(encoding.substring(equalsPos + 1));
|
||||
} catch (NumberFormatException e) {
|
||||
// Ignore encoding
|
||||
q = 0.0f;
|
||||
}
|
||||
}
|
||||
if (encoding.contains("*")) {
|
||||
starQ = q;
|
||||
} else if (encoding.contains("br") && q > brQ) {
|
||||
brQ = q;
|
||||
} else if (encoding.contains("gzip") && q > gzipQ) {
|
||||
gzipQ = q;
|
||||
} else if (encoding.contains("deflate") && q > deflateQ) {
|
||||
deflateQ = q;
|
||||
}
|
||||
}
|
||||
if (brQ > 0.0f || gzipQ > 0.0f || deflateQ > 0.0f) {
|
||||
if (brQ != -1.0f && brQ >= gzipQ) {
|
||||
return Brotli.isAvailable() ? "br" : null;
|
||||
} else if (gzipQ != -1.0f && gzipQ >= deflateQ) {
|
||||
return "gzip";
|
||||
} else if (deflateQ != -1.0f) {
|
||||
return "deflate";
|
||||
}
|
||||
}
|
||||
if (starQ > 0.0f) {
|
||||
if (brQ == -1.0f) {
|
||||
return Brotli.isAvailable() ? "br" : null;
|
||||
}
|
||||
if (gzipQ == -1.0f) {
|
||||
return "gzip";
|
||||
}
|
||||
if (deflateQ == -1.0f) {
|
||||
return "deflate";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@SuppressWarnings("FloatingPointEquality")
|
||||
protected ZlibWrapper determineWrapper(String acceptEncoding) {
|
||||
float starQ = -1.0f;
|
||||
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.http;
|
||||
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class BrotliContentCompressorTest {
|
||||
|
||||
@Test
|
||||
void testGetTargetContentEncoding() {
|
||||
HttpContentCompressor compressor = new HttpContentCompressor();
|
||||
|
||||
String[] tests = {
|
||||
// Accept-Encoding -> Content-Encoding
|
||||
"", null,
|
||||
"*", "br",
|
||||
"*;q=0.0", null,
|
||||
"br", "br",
|
||||
"compress, br;q=0.5", "br",
|
||||
"br; q=0.5, identity", "br",
|
||||
"br; q=0, deflate", "br",
|
||||
};
|
||||
for (int i = 0; i < tests.length; i += 2) {
|
||||
String acceptEncoding = tests[i];
|
||||
String contentEncoding = tests[i + 1];
|
||||
String targetEncoding = compressor.determineEncoding(acceptEncoding);
|
||||
assertEquals(contentEncoding, targetEncoding);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAcceptEncodingHttpRequest() {
|
||||
EmbeddedChannel ch = new EmbeddedChannel(new HttpContentCompressor(null));
|
||||
ch.writeInbound(newRequest());
|
||||
FullHttpRequest fullHttpRequest = ch.readInbound();
|
||||
fullHttpRequest.release();
|
||||
|
||||
HttpResponse res = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
|
||||
res.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
|
||||
ch.writeOutbound(res);
|
||||
|
||||
assertEncodedResponse(ch);
|
||||
|
||||
assertTrue(ch.close().isSuccess());
|
||||
}
|
||||
|
||||
private static void assertEncodedResponse(EmbeddedChannel ch) {
|
||||
Object o = ch.readOutbound();
|
||||
assertThat(o, is(instanceOf(HttpResponse.class)));
|
||||
|
||||
assertEncodedResponse((HttpResponse) o);
|
||||
}
|
||||
|
||||
private static void assertEncodedResponse(HttpResponse res) {
|
||||
assertThat(res, is(not(instanceOf(HttpContent.class))));
|
||||
assertThat(res.headers().get(HttpHeaderNames.TRANSFER_ENCODING), is("chunked"));
|
||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
|
||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("br"));
|
||||
}
|
||||
|
||||
private static FullHttpRequest newRequest() {
|
||||
FullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
|
||||
req.headers().set(HttpHeaderNames.ACCEPT_ENCODING, "br, gzip, deflate");
|
||||
return req;
|
||||
}
|
||||
}
|
@ -778,6 +778,7 @@ public class HttpContentCompressorTest {
|
||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_LENGTH), is(nullValue()));
|
||||
assertThat(res.headers().get(HttpHeaderNames.CONTENT_ENCODING), is("gzip"));
|
||||
}
|
||||
|
||||
private static void assertAssembledEncodedResponse(EmbeddedChannel ch) {
|
||||
Object o = ch.readOutbound();
|
||||
assertThat(o, is(instanceOf(AssembledHttpResponse.class)));
|
||||
|
@ -88,6 +88,11 @@
|
||||
<scope>test</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aayushatharva.brotli4j</groupId>
|
||||
<artifactId>brotli4j</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
|
@ -21,14 +21,21 @@ import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelPromise;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.handler.codec.compression.BrotliEncoder;
|
||||
import io.netty.handler.codec.compression.ZlibCodecFactory;
|
||||
import io.netty.handler.codec.compression.ZlibWrapper;
|
||||
import io.netty.handler.codec.compression.BrotliOptions;
|
||||
import io.netty.handler.codec.compression.CompressionOptions;
|
||||
import io.netty.handler.codec.compression.DeflateOptions;
|
||||
import io.netty.handler.codec.compression.GzipOptions;
|
||||
import io.netty.handler.codec.compression.StandardCompressionOptions;
|
||||
import io.netty.util.concurrent.PromiseCombiner;
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
import io.netty.util.internal.UnstableApi;
|
||||
|
||||
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
|
||||
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
|
||||
import static io.netty.handler.codec.http.HttpHeaderValues.BR;
|
||||
import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE;
|
||||
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
|
||||
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
|
||||
@ -41,19 +48,35 @@ import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP;
|
||||
*/
|
||||
@UnstableApi
|
||||
public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionEncoder {
|
||||
// We cannot remove this because it'll be breaking change
|
||||
public static final int DEFAULT_COMPRESSION_LEVEL = 6;
|
||||
public static final int DEFAULT_WINDOW_BITS = 15;
|
||||
public static final int DEFAULT_MEM_LEVEL = 8;
|
||||
|
||||
private final int compressionLevel;
|
||||
private final int windowBits;
|
||||
private final int memLevel;
|
||||
private int compressionLevel;
|
||||
private int windowBits;
|
||||
private int memLevel;
|
||||
private final Http2Connection.PropertyKey propertyKey;
|
||||
|
||||
private final boolean supportsCompressionOptions;
|
||||
|
||||
private BrotliOptions brotliOptions;
|
||||
private GzipOptions gzipCompressionOptions;
|
||||
private DeflateOptions deflateOptions;
|
||||
|
||||
/**
|
||||
* Create a new {@link CompressorHttp2ConnectionEncoder} instance
|
||||
* with default implementation of {@link StandardCompressionOptions}
|
||||
*/
|
||||
public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate) {
|
||||
this(delegate, DEFAULT_COMPRESSION_LEVEL, DEFAULT_WINDOW_BITS, DEFAULT_MEM_LEVEL);
|
||||
this(delegate, StandardCompressionOptions.brotli(), StandardCompressionOptions.gzip(),
|
||||
StandardCompressionOptions.deflate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link CompressorHttp2ConnectionEncoder} instance
|
||||
*/
|
||||
@Deprecated
|
||||
public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate, int compressionLevel, int windowBits,
|
||||
int memLevel) {
|
||||
super(delegate);
|
||||
@ -71,6 +94,45 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
supportsCompressionOptions = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link CompressorHttp2ConnectionEncoder} with
|
||||
* specified {@link StandardCompressionOptions}
|
||||
*/
|
||||
public CompressorHttp2ConnectionEncoder(Http2ConnectionEncoder delegate,
|
||||
CompressionOptions... compressionOptionsArgs) {
|
||||
super(delegate);
|
||||
ObjectUtil.checkNotNull(compressionOptionsArgs, "CompressionOptions");
|
||||
ObjectUtil.deepCheckNotNull("CompressionOptions", compressionOptionsArgs);
|
||||
|
||||
for (CompressionOptions compressionOptions : compressionOptionsArgs) {
|
||||
if (compressionOptions instanceof BrotliOptions) {
|
||||
brotliOptions = (BrotliOptions) compressionOptions;
|
||||
} else if (compressionOptions instanceof GzipOptions) {
|
||||
gzipCompressionOptions = (GzipOptions) compressionOptions;
|
||||
} else if (compressionOptions instanceof DeflateOptions) {
|
||||
deflateOptions = (DeflateOptions) compressionOptions;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported " + CompressionOptions.class.getSimpleName() +
|
||||
": " + compressionOptions);
|
||||
}
|
||||
}
|
||||
|
||||
supportsCompressionOptions = true;
|
||||
|
||||
propertyKey = connection().newKey();
|
||||
connection().addListener(new Http2ConnectionAdapter() {
|
||||
@Override
|
||||
public void onStreamRemoved(Http2Stream stream) {
|
||||
final EmbeddedChannel compressor = stream.getProperty(propertyKey);
|
||||
if (compressor != null) {
|
||||
cleanup(stream, compressor);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -190,6 +252,10 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
|
||||
if (DEFLATE.contentEqualsIgnoreCase(contentEncoding) || X_DEFLATE.contentEqualsIgnoreCase(contentEncoding)) {
|
||||
return newCompressionChannel(ctx, ZlibWrapper.ZLIB);
|
||||
}
|
||||
if (brotliOptions != null && BR.contentEqualsIgnoreCase(contentEncoding)) {
|
||||
return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), new BrotliEncoder(brotliOptions.parameters()));
|
||||
}
|
||||
// 'identity' or unsupported
|
||||
return null;
|
||||
}
|
||||
@ -212,9 +278,25 @@ public class CompressorHttp2ConnectionEncoder extends DecoratingHttp2ConnectionE
|
||||
* @param wrapper Defines what type of encoder should be used
|
||||
*/
|
||||
private EmbeddedChannel newCompressionChannel(final ChannelHandlerContext ctx, ZlibWrapper wrapper) {
|
||||
return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper, compressionLevel, windowBits,
|
||||
memLevel));
|
||||
if (supportsCompressionOptions) {
|
||||
if (wrapper == ZlibWrapper.GZIP && gzipCompressionOptions != null) {
|
||||
return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper,
|
||||
gzipCompressionOptions.compressionLevel(), gzipCompressionOptions.windowBits(),
|
||||
gzipCompressionOptions.memLevel()));
|
||||
} else if (wrapper == ZlibWrapper.ZLIB && deflateOptions != null) {
|
||||
return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper,
|
||||
deflateOptions.compressionLevel(), deflateOptions.windowBits(),
|
||||
deflateOptions.memLevel()));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported ZlibWrapper: " + wrapper);
|
||||
}
|
||||
} else {
|
||||
return new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
|
||||
ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(wrapper, compressionLevel, windowBits,
|
||||
memLevel));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -235,6 +235,54 @@ public class DataCompressionHttp2Test {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void brotliEncodingSingleEmptyMessage() throws Exception {
|
||||
final String text = "";
|
||||
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
|
||||
bootstrapEnv(data.readableBytes());
|
||||
try {
|
||||
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
|
||||
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.BR);
|
||||
|
||||
runInChannel(clientChannel, new Http2Runnable() {
|
||||
@Override
|
||||
public void run() throws Http2Exception {
|
||||
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
|
||||
clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
|
||||
clientHandler.flush(ctxClient());
|
||||
}
|
||||
});
|
||||
awaitServer();
|
||||
assertEquals(text, serverOut.toString(CharsetUtil.UTF_8.name()));
|
||||
} finally {
|
||||
data.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void brotliEncodingSingleMessage() throws Exception {
|
||||
final String text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc";
|
||||
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes(CharsetUtil.UTF_8.name()));
|
||||
bootstrapEnv(data.readableBytes());
|
||||
try {
|
||||
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
|
||||
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.BR);
|
||||
|
||||
runInChannel(clientChannel, new Http2Runnable() {
|
||||
@Override
|
||||
public void run() throws Http2Exception {
|
||||
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
|
||||
clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
|
||||
clientHandler.flush(ctxClient());
|
||||
}
|
||||
});
|
||||
awaitServer();
|
||||
assertEquals(text, serverOut.toString(CharsetUtil.UTF_8.name()));
|
||||
} finally {
|
||||
data.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deflateEncodingWriteLargeMessage() throws Exception {
|
||||
final int BUFFER_SIZE = 1 << 12;
|
||||
|
@ -25,6 +25,7 @@ public final class Brotli {
|
||||
|
||||
private static final InternalLogger logger = InternalLoggerFactory.getInstance(Brotli.class);
|
||||
private static final ClassNotFoundException CNFE;
|
||||
private static Throwable cause;
|
||||
|
||||
static {
|
||||
ClassNotFoundException cnfe = null;
|
||||
@ -42,7 +43,7 @@ public final class Brotli {
|
||||
|
||||
// If in the classpath, try to load the native library and initialize brotli4j.
|
||||
if (cnfe == null) {
|
||||
Throwable cause = Brotli4jLoader.getUnavailabilityCause();
|
||||
cause = Brotli4jLoader.getUnavailabilityCause();
|
||||
if (cause != null) {
|
||||
logger.debug("Failed to load brotli4j; Brotli support will be unavailable.", cause);
|
||||
}
|
||||
@ -70,6 +71,13 @@ public final class Brotli {
|
||||
Brotli4jLoader.ensureAvailability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@link Throwable} of unavailability cause
|
||||
*/
|
||||
public static Throwable cause() {
|
||||
return cause;
|
||||
}
|
||||
|
||||
private Brotli() {
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
import com.aayushatharva.brotli4j.encoder.Encoder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
|
||||
/**
|
||||
* Compress a {@link ByteBuf} with the brotli format.
|
||||
*
|
||||
* See <a href="https://github.com/google/brotli">brotli</a>.
|
||||
*/
|
||||
@ChannelHandler.Sharable
|
||||
public final class BrotliEncoder extends MessageToByteEncoder<ByteBuf> {
|
||||
|
||||
private final Encoder.Parameters parameters;
|
||||
|
||||
/**
|
||||
* Create a new {@link BrotliEncoder} Instance
|
||||
* with {@link Encoder.Parameters#setQuality(int)} set to 4
|
||||
* and {@link Encoder.Parameters#setMode(Encoder.Mode)} set to {@link Encoder.Mode#TEXT}
|
||||
*/
|
||||
public BrotliEncoder() {
|
||||
this(BrotliOptions.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link BrotliEncoder} Instance
|
||||
*
|
||||
* @param parameters {@link Encoder.Parameters} Instance
|
||||
*/
|
||||
public BrotliEncoder(Encoder.Parameters parameters) {
|
||||
this.parameters = ObjectUtil.checkNotNull(parameters, "Parameters");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link BrotliEncoder} Instance
|
||||
*
|
||||
* @param brotliOptions {@link BrotliOptions} to use.
|
||||
*/
|
||||
public BrotliEncoder(BrotliOptions brotliOptions) {
|
||||
this(brotliOptions.parameters());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg, boolean preferDirect) throws Exception {
|
||||
// If ByteBuf is unreadable, then return EMPTY_BUFFER.
|
||||
if (!msg.isReadable()) {
|
||||
return Unpooled.EMPTY_BUFFER;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] uncompressed = ByteBufUtil.getBytes(msg, msg.readerIndex(), msg.readableBytes(), false);
|
||||
byte[] compressed = Encoder.compress(uncompressed, parameters);
|
||||
if (preferDirect) {
|
||||
ByteBuf out = ctx.alloc().ioBuffer(compressed.length);
|
||||
out.writeBytes(compressed);
|
||||
return out;
|
||||
} else {
|
||||
return Unpooled.wrappedBuffer(compressed);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ReferenceCountUtil.release(msg);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
import com.aayushatharva.brotli4j.encoder.Encoder;
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
|
||||
/**
|
||||
* {@link BrotliOptions} holds {@link Encoder.Parameters} for
|
||||
* Brotli compression.
|
||||
*/
|
||||
public final class BrotliOptions implements CompressionOptions {
|
||||
|
||||
private final Encoder.Parameters parameters;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link BrotliOptions} with{@link Encoder.Parameters#setQuality(int)} set to 4
|
||||
* and {@link Encoder.Parameters#setMode(Encoder.Mode)} set to {@link Encoder.Mode#TEXT}
|
||||
*/
|
||||
static final BrotliOptions DEFAULT = new BrotliOptions(
|
||||
new Encoder.Parameters().setQuality(4).setMode(Encoder.Mode.TEXT)
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new {@link BrotliOptions}
|
||||
*
|
||||
* @param parameters {@link Encoder.Parameters} Instance
|
||||
* @throws NullPointerException If {@link Encoder.Parameters} is {@code null}
|
||||
*/
|
||||
BrotliOptions(Encoder.Parameters parameters) {
|
||||
this.parameters = ObjectUtil.checkNotNull(parameters, "Parameters");
|
||||
|
||||
if (!Brotli.isAvailable()) {
|
||||
throw new IllegalStateException("Brotli is not available", Brotli.cause());
|
||||
}
|
||||
}
|
||||
|
||||
public Encoder.Parameters parameters() {
|
||||
return parameters;
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
/**
|
||||
* {@link CompressionOptions} provides compression options for
|
||||
* various types of compressor types, like Brotli.
|
||||
*
|
||||
* A {@link CompressionOptions} instance is thread-safe
|
||||
* and should be shared between multiple instances of Compressor.
|
||||
*/
|
||||
public interface CompressionOptions {
|
||||
// Empty
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
import io.netty.util.internal.ObjectUtil;
|
||||
|
||||
/**
|
||||
* {@link DeflateOptions} holds {@link #compressionLevel()},
|
||||
* {@link #memLevel()} and {@link #windowBits()} for Deflate compression.
|
||||
*/
|
||||
public class DeflateOptions implements CompressionOptions {
|
||||
|
||||
private final int compressionLevel;
|
||||
private final int windowBits;
|
||||
private final int memLevel;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link DeflateOptions} with
|
||||
* {@link #compressionLevel} set to 6, {@link #windowBits} set to 15
|
||||
* and {@link #memLevel} set to 8.
|
||||
*/
|
||||
static final DeflateOptions DEFAULT = new DeflateOptions(
|
||||
6, 15, 8
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new {@link DeflateOptions} Instance
|
||||
*
|
||||
* @param compressionLevel {@code 1} yields the fastest compression and {@code 9} yields the
|
||||
* best compression. {@code 0} means no compression. The default
|
||||
* compression level is {@code 6}.
|
||||
*
|
||||
* @param windowBits The base two logarithm of the size of the history buffer. The
|
||||
* value should be in the range {@code 9} to {@code 15} inclusive.
|
||||
* Larger values result in better compression at the expense of
|
||||
* memory usage. The default value is {@code 15}.
|
||||
*
|
||||
* @param memLevel How much memory should be allocated for the internal compression
|
||||
* state. {@code 1} uses minimum memory and {@code 9} uses maximum
|
||||
* memory. Larger values result in better and faster compression
|
||||
* at the expense of memory usage. The default value is {@code 8}
|
||||
*/
|
||||
DeflateOptions(int compressionLevel, int windowBits, int memLevel) {
|
||||
this.compressionLevel = ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
|
||||
this.windowBits = ObjectUtil.checkInRange(windowBits, 9, 15, "windowBits");
|
||||
this.memLevel = ObjectUtil.checkInRange(memLevel, 1, 9, "memLevel");
|
||||
}
|
||||
|
||||
public int compressionLevel() {
|
||||
return compressionLevel;
|
||||
}
|
||||
|
||||
public int windowBits() {
|
||||
return windowBits;
|
||||
}
|
||||
|
||||
public int memLevel() {
|
||||
return memLevel;
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
/**
|
||||
* {@link GzipOptions} holds {@link #compressionLevel()},
|
||||
* {@link #memLevel()} and {@link #windowBits()} for Gzip compression.
|
||||
* This class is an extension of {@link DeflateOptions}
|
||||
*/
|
||||
public final class GzipOptions extends DeflateOptions {
|
||||
|
||||
/**
|
||||
* Default implementation of {@link GzipOptions} with
|
||||
* {@link #compressionLevel()} set to 6, {@link #windowBits()} set to 15
|
||||
* and {@link #memLevel()} set to 8.
|
||||
*/
|
||||
static final GzipOptions DEFAULT = new GzipOptions(
|
||||
6, 15, 8
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a new {@link GzipOptions} Instance
|
||||
*
|
||||
* @param compressionLevel {@code 1} yields the fastest compression and {@code 9} yields the
|
||||
* best compression. {@code 0} means no compression. The default
|
||||
* compression level is {@code 6}.
|
||||
*
|
||||
* @param windowBits The base two logarithm of the size of the history buffer. The
|
||||
* value should be in the range {@code 9} to {@code 15} inclusive.
|
||||
* Larger values result in better compression at the expense of
|
||||
* memory usage. The default value is {@code 15}.
|
||||
*
|
||||
* @param memLevel How much memory should be allocated for the internal compression
|
||||
* state. {@code 1} uses minimum memory and {@code 9} uses maximum
|
||||
* memory. Larger values result in better and faster compression
|
||||
* at the expense of memory usage. The default value is {@code 8}
|
||||
*/
|
||||
GzipOptions(int compressionLevel, int windowBits, int memLevel) {
|
||||
super(compressionLevel, windowBits, memLevel);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
import com.aayushatharva.brotli4j.encoder.Encoder;
|
||||
|
||||
/**
|
||||
* Standard Compression Options for {@link BrotliOptions},
|
||||
* {@link GzipOptions} and {@link DeflateOptions}
|
||||
*/
|
||||
public final class StandardCompressionOptions {
|
||||
|
||||
private StandardCompressionOptions() {
|
||||
// Prevent outside initialization
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BrotliOptions#DEFAULT
|
||||
*/
|
||||
public static BrotliOptions brotli() {
|
||||
return BrotliOptions.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see BrotliOptions#BrotliOptions(Encoder.Parameters)
|
||||
*/
|
||||
public static BrotliOptions brotli(Encoder.Parameters parameters) {
|
||||
return new BrotliOptions(parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see GzipOptions#DEFAULT
|
||||
*/
|
||||
public static GzipOptions gzip() {
|
||||
return GzipOptions.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see GzipOptions#GzipOptions(int, int, int)
|
||||
*/
|
||||
public static GzipOptions gzip(int compressionLevel, int windowBits, int memLevel) {
|
||||
return new GzipOptions(compressionLevel, windowBits, memLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DeflateOptions#DEFAULT
|
||||
*/
|
||||
public static DeflateOptions deflate() {
|
||||
return DeflateOptions.DEFAULT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DeflateOptions#DeflateOptions(int, int, int)
|
||||
*/
|
||||
public static DeflateOptions deflate(int compressionLevel, int windowBits, int memLevel) {
|
||||
return new DeflateOptions(compressionLevel, windowBits, memLevel);
|
||||
}
|
||||
}
|
@ -136,7 +136,7 @@ public class BrotliDecoderTest {
|
||||
decompressed.release();
|
||||
}
|
||||
|
||||
private void testDecompressionOfBatchedFlow(final ByteBuf expected, final ByteBuf data) {
|
||||
private void testDecompressionOfBatchedFlow(final ByteBuf expected, final ByteBuf data) {
|
||||
final int compressedLength = data.readableBytes();
|
||||
int written = 0, length = RANDOM.nextInt(100);
|
||||
while (written + length < compressedLength) {
|
||||
@ -155,7 +155,7 @@ public class BrotliDecoderTest {
|
||||
data.release();
|
||||
}
|
||||
|
||||
private static ByteBuf readDecompressed(final EmbeddedChannel channel) {
|
||||
private static ByteBuf readDecompressed(final EmbeddedChannel channel) {
|
||||
CompositeByteBuf decompressed = Unpooled.compositeBuffer();
|
||||
ByteBuf msg;
|
||||
while ((msg = channel.readInbound()) != null) {
|
||||
|
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2021 The Netty Project
|
||||
*
|
||||
* The Netty Project licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package io.netty.handler.codec.compression;
|
||||
|
||||
import com.aayushatharva.brotli4j.decoder.Decoder;
|
||||
import com.aayushatharva.brotli4j.decoder.DecoderJNI;
|
||||
import com.aayushatharva.brotli4j.decoder.DirectDecompress;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.CompositeByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import io.netty.util.internal.PlatformDependent;
|
||||
import org.junit.jupiter.api.condition.DisabledIf;
|
||||
|
||||
@DisabledIf(value = "isNotSupported", disabledReason = "Brotli is not supported on this platform")
|
||||
public class BrotliEncoderTest extends AbstractEncoderTest {
|
||||
|
||||
static {
|
||||
try {
|
||||
Brotli.ensureAvailability();
|
||||
} catch (Throwable throwable) {
|
||||
throw new ExceptionInInitializerError(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmbeddedChannel createChannel() {
|
||||
return new EmbeddedChannel(new BrotliEncoder());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuf decompress(ByteBuf compressed, int originalLength) throws Exception {
|
||||
byte[] compressedArray = new byte[compressed.readableBytes()];
|
||||
compressed.readBytes(compressedArray);
|
||||
compressed.release();
|
||||
|
||||
DirectDecompress decompress = Decoder.decompress(compressedArray);
|
||||
if (decompress.getResultStatus() == DecoderJNI.Status.ERROR) {
|
||||
throw new DecompressionException("Brotli stream corrupted");
|
||||
}
|
||||
|
||||
byte[] decompressed = decompress.getDecompressedData();
|
||||
return Unpooled.wrappedBuffer(decompressed);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ByteBuf readDecompressed(final int dataLength) throws Exception {
|
||||
CompositeByteBuf decompressed = Unpooled.compositeBuffer();
|
||||
ByteBuf msg;
|
||||
while ((msg = channel.readOutbound()) != null) {
|
||||
if (msg.isReadable()) {
|
||||
decompressed.addComponent(true, decompress(msg, -1));
|
||||
} else {
|
||||
msg.release();
|
||||
}
|
||||
}
|
||||
return decompressed;
|
||||
}
|
||||
|
||||
static boolean isNotSupported() {
|
||||
return (PlatformDependent.isOsx() || PlatformDependent.isWindows())
|
||||
&& "aarch_64".equals(PlatformDependent.normalizedArch());
|
||||
}
|
||||
}
|
@ -41,6 +41,26 @@ public final class ObjectUtil {
|
||||
return arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the given varargs is not null and does not contain elements
|
||||
* null elements.
|
||||
*
|
||||
* If it is, throws {@link NullPointerException}.
|
||||
* Otherwise, returns the argument.
|
||||
*/
|
||||
public static <T> T[] deepCheckNotNull(String text, T... varargs) {
|
||||
if (varargs == null) {
|
||||
throw new NullPointerException(text);
|
||||
}
|
||||
|
||||
for (T element : varargs) {
|
||||
if (element == null) {
|
||||
throw new NullPointerException(text);
|
||||
}
|
||||
}
|
||||
return varargs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the given argument is not null. If it is, throws {@link IllegalArgumentException}.
|
||||
* Otherwise, returns the argument.
|
||||
|
201
license/LICENSE.brotli4j.txt
Normal file
201
license/LICENSE.brotli4j.txt
Normal file
@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
Loading…
Reference in New Issue
Block a user