From 0789c1b55bbd4cb5a54839d8dca48e7dcb0b6484 Mon Sep 17 00:00:00 2001 From: Nabil Abdel-Hafeez <7283535+987Nabil@users.noreply.github.com> Date: Thu, 8 Feb 2024 22:47:45 +0100 Subject: [PATCH] Add brotli compression (#2646) --- project/Dependencies.scala | 4 +- project/Shading.scala | 1 + .../zio/http/netty/model/Conversions.scala | 29 ++-- .../src/main/scala/zio/http/Server.scala | 142 ++++++++++++------ 4 files changed, 117 insertions(+), 59 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f88b1ffc69..85da7fc24b 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,5 +1,4 @@ -import sbt._ -import sbt.Keys.scalaVersion +import sbt.* object Dependencies { val JwtCoreVersion = "9.1.1" @@ -26,6 +25,7 @@ object Dependencies { "io.netty" % "netty-transport-native-kqueue" % NettyVersion, "io.netty" % "netty-transport-native-kqueue" % NettyVersion % Runtime classifier "osx-x86_64", "io.netty" % "netty-transport-native-kqueue" % NettyVersion % Runtime classifier "osx-aarch_64", + "com.aayushatharva.brotli4j" % "brotli4j" % "1.16.0", ) val `netty-incubator` = diff --git a/project/Shading.scala b/project/Shading.scala index ac86dca6c5..0c142525a9 100644 --- a/project/Shading.scala +++ b/project/Shading.scala @@ -16,6 +16,7 @@ object Shading { Seq( shadedModules ++= (netty :+ `netty-incubator`).map(_.module).toSet, shadingRules += ShadingRule.rename("io.netty.**", "zio.http.shaded.netty.@1"), + shadingRules += ShadingRule.rename("com.aayushatharva.brotli4j.**", "zio.http.shaded.brotli4j.@1"), validNamespaces += "zio", ) } else Nil diff --git a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala index e0dc8b1604..7756d4797a 100644 --- a/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala +++ b/zio-http/jvm/src/main/scala/zio/http/netty/model/Conversions.scala @@ -18,12 +18,11 @@ package zio.http.netty.model import scala.collection.AbstractIterator -import zio.stacktracer.TracingImplicits.disableAutoTrace - import zio.http.Server.Config.CompressionOptions import zio.http._ -import io.netty.handler.codec.compression.{DeflateOptions, StandardCompressionOptions} +import com.aayushatharva.brotli4j.encoder.Encoder +import io.netty.handler.codec.compression.StandardCompressionOptions import io.netty.handler.codec.http._ import io.netty.handler.codec.http.websocketx.WebSocketScheme @@ -132,14 +131,26 @@ private[netty] object Conversions { case _ => None } - def compressionOptionsToNetty(compressionOptions: CompressionOptions): DeflateOptions = - compressionOptions.kind match { - case CompressionOptions.CompressionType.GZip => - StandardCompressionOptions.gzip(compressionOptions.level, compressionOptions.bits, compressionOptions.mem) - case CompressionOptions.CompressionType.Deflate => - StandardCompressionOptions.deflate(compressionOptions.level, compressionOptions.bits, compressionOptions.mem) + def compressionOptionsToNetty( + compressionOptions: CompressionOptions, + ): io.netty.handler.codec.compression.CompressionOptions = + compressionOptions match { + case CompressionOptions.GZip(cfg) => + StandardCompressionOptions.gzip(cfg.level, cfg.bits, cfg.mem) + case CompressionOptions.Deflate(cfg) => + StandardCompressionOptions.deflate(cfg.level, cfg.bits, cfg.mem) + case CompressionOptions.Brotli(cfg) => + StandardCompressionOptions.brotli( + new Encoder.Parameters().setQuality(cfg.quality).setWindow(cfg.lgwin).setMode(brotliModeToJava(cfg.mode)), + ) } + def brotliModeToJava(brotli: CompressionOptions.Mode): Encoder.Mode = brotli match { + case CompressionOptions.Mode.Font => Encoder.Mode.FONT + case CompressionOptions.Mode.Text => Encoder.Mode.TEXT + case CompressionOptions.Mode.Generic => Encoder.Mode.GENERIC + } + def versionToNetty(version: Version): HttpVersion = version match { case Version.Http_1_0 => HttpVersion.HTTP_1_0 case Version.Http_1_1 => HttpVersion.HTTP_1_1 diff --git a/zio-http/shared/src/main/scala/zio/http/Server.scala b/zio-http/shared/src/main/scala/zio/http/Server.scala index dcd9cb7be5..a3207069d6 100644 --- a/zio-http/shared/src/main/scala/zio/http/Server.scala +++ b/zio-http/shared/src/main/scala/zio/http/Server.scala @@ -20,7 +20,6 @@ import java.net.{InetAddress, InetSocketAddress} import java.util.concurrent.atomic._ import zio._ -import zio.stacktracer.TracingImplicits.disableAutoTrace import zio.http.Server.Config.ResponseCompressionConfig @@ -241,69 +240,116 @@ object Server extends ServerPlatformSpecific { ResponseCompressionConfig(0, IndexedSeq(CompressionOptions.gzip(), CompressionOptions.deflate())) } - /** - * @param level - * defines compression level, {@code 1} yields the fastest compression and - * {@code 9} yields the best compression. {@code 0} means no compression. - * @param bits - * defines 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 - * @param mem - * defines 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 - */ - final case class CompressionOptions( - level: Int, - bits: Int, - mem: Int, - kind: CompressionOptions.CompressionType, - ) + sealed trait CompressionOptions object CompressionOptions { - val DefaultLevel = 6 - val DefaultBits = 15 - val DefaultMem = 8 + + final case class GZip(cfg: DeflateConfig) extends CompressionOptions + final case class Deflate(cfg: DeflateConfig) extends CompressionOptions + final case class Brotli(cfg: BrotliConfig) extends CompressionOptions + + /** + * @param level + * defines compression level, {@code 1} yields the fastest compression + * and {@code 9} yields the best compression. {@code 0} means no + * compression. + * @param bits + * defines 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 + * @param mem + * defines 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 + */ + final case class DeflateConfig( + level: Int, + bits: Int, + mem: Int, + ) + + object DeflateConfig { + val DefaultLevel = 6 + val DefaultBits = 15 + val DefaultMem = 8 + } + + final case class BrotliConfig( + quality: Int, + lgwin: Int, + mode: Mode, + ) + + object BrotliConfig { + val DefaultQuality = 4 + val DefaultLgwin = -1 + val DefaultMode = Mode.Text + } + + sealed trait Mode + object Mode { + case object Generic extends Mode + case object Text extends Mode + case object Font extends Mode + + def fromString(s: String): Mode = s.toLowerCase match { + case "generic" => Generic + case "text" => Text + case "font" => Font + case _ => Text + } + } /** * Creates GZip CompressionOptions. Defines defaults as per * io.netty.handler.codec.compression.GzipOptions#DEFAULT */ - def gzip(level: Int = DefaultLevel, bits: Int = DefaultBits, mem: Int = DefaultMem): CompressionOptions = - CompressionOptions(level, bits, mem, CompressionType.GZip) + def gzip( + level: Int = DeflateConfig.DefaultLevel, + bits: Int = DeflateConfig.DefaultBits, + mem: Int = DeflateConfig.DefaultMem, + ): CompressionOptions = + CompressionOptions.GZip(DeflateConfig(level, bits, mem)) /** * Creates Deflate CompressionOptions. Defines defaults as per * io.netty.handler.codec.compression.DeflateOptions#DEFAULT */ - def deflate(level: Int = DefaultLevel, bits: Int = DefaultBits, mem: Int = DefaultMem): CompressionOptions = - CompressionOptions(level, bits, mem, CompressionType.Deflate) - - sealed trait CompressionType - - private[http] object CompressionType { - case object GZip extends CompressionType - case object Deflate extends CompressionType + def deflate( + level: Int = DeflateConfig.DefaultLevel, + bits: Int = DeflateConfig.DefaultBits, + mem: Int = DeflateConfig.DefaultMem, + ): CompressionOptions = + CompressionOptions.Deflate(DeflateConfig(level, bits, mem)) - lazy val config: zio.Config[CompressionType] = - zio.Config.string.mapOrFail { - case "gzip" => Right(GZip) - case "deflate" => Right(Deflate) - case other => Left(zio.Config.Error.InvalidData(message = s"Invalid compression type: $other")) - } - } + /** + * Creates Brotli CompressionOptions. Defines defaults as per + * io.netty.handler.codec.compression.BrotliOptions#DEFAULT + */ + def brotli( + quality: Int = BrotliConfig.DefaultQuality, + lgwin: Int = BrotliConfig.DefaultLgwin, + mode: Mode = BrotliConfig.DefaultMode, + ): CompressionOptions = + CompressionOptions.Brotli(BrotliConfig(quality, lgwin, mode)) lazy val config: zio.Config[CompressionOptions] = ( - zio.Config.int("level").withDefault(DefaultLevel) ++ - zio.Config.int("bits").withDefault(DefaultBits) ++ - zio.Config.int("mem").withDefault(DefaultMem) ++ - CompressionOptions.CompressionType.config.nested("type"), - ).map { case (level, bits, mem, kind) => - CompressionOptions(level, bits, mem, kind) + (zio.Config.int("level").withDefault(DeflateConfig.DefaultLevel) ++ + zio.Config.int("bits").withDefault(DeflateConfig.DefaultBits) ++ + zio.Config.int("mem").withDefault(DeflateConfig.DefaultMem)) ++ + zio.Config.int("quantity").withDefault(BrotliConfig.DefaultQuality) ++ + zio.Config.int("lgwin").withDefault(BrotliConfig.DefaultLgwin) ++ + zio.Config.string("mode").map(Mode.fromString).withDefault(BrotliConfig.DefaultMode) ++ + zio.Config.string("type"), + ).map { case (level, bits, mem, quantity, lgwin, mode, typ) => + typ.toLowerCase match { + case "gzip" => gzip(level, bits, mem) + case "deflate" => deflate(level, bits, mem) + case "brotli" => brotli(quantity, lgwin, mode) + } } } }