Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opt-in for FHCRC in gzip compression #2696

Merged
merged 5 commits into from Oct 30, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 15 additions & 7 deletions core/jvm/src/main/scala/fs2/compression/CompressionPlatform.scala
Expand Up @@ -404,7 +404,8 @@ private[compression] trait CompressionCompanionPlatform {
fileName,
modificationTime,
comment,
params.level.juzDeflaterLevel
params.level.juzDeflaterLevel,
params.fhCrcEnabled
) ++
_deflate(
params,
Expand All @@ -425,7 +426,8 @@ private[compression] trait CompressionCompanionPlatform {
fileName: Option[String],
modificationTime: Option[Instant],
comment: Option[String],
deflateLevel: Int
deflateLevel: Int,
fhCrcEnabled: Boolean
): Stream[F, Byte] = {
// See RFC 1952: https://www.ietf.org/rfc/rfc1952.txt
val secondsSince197001010000: Long =
Expand All @@ -434,7 +436,7 @@ private[compression] trait CompressionCompanionPlatform {
gzipMagicFirstByte, // ID1: Identification 1
gzipMagicSecondByte, // ID2: Identification 2
gzipCompressionMethod.DEFLATE, // CM: Compression Method
(gzipFlag.FHCRC + // FLG: Header CRC
((if (fhCrcEnabled) gzipFlag.FHCRC else zeroByte) + // FLG: Header CRC
fileName.map(_ => gzipFlag.FNAME).getOrElse(zeroByte) + // FLG: File name
comment.map(_ => gzipFlag.FCOMMENT).getOrElse(zeroByte)).toByte, // FLG: Comment
(secondsSince197001010000 & 0xff).toByte, // MTIME: Modification Time
Expand Down Expand Up @@ -463,10 +465,16 @@ private[compression] trait CompressionCompanionPlatform {
bytes
}
val crc32Value = crc32.getValue
val crc16 = Array[Byte](
(crc32Value & 0xff).toByte,
((crc32Value >> 8) & 0xff).toByte
)

val crc16 =
if (fhCrcEnabled)
Array[Byte](
(crc32Value & 0xff).toByte,
((crc32Value >> 8) & 0xff).toByte
)
else
Array.emptyByteArray

Stream.chunk(moveAsChunkBytes(header)) ++
fileNameEncoded
.map(bytes => Stream.chunk(moveAsChunkBytes(bytes)) ++ Stream.emit(zeroByte))
Expand Down
65 changes: 65 additions & 0 deletions core/jvm/src/test/scala/fs2/JvmCompressionSuite.scala
Expand Up @@ -246,6 +246,71 @@ class JvmCompressionSuite extends CompressionSuite {
.map(compressed => assert(compressed.length < uncompressed.length))
}

test("gzip.compresses input, with FLG.FHCRC set") {
val uncompressed: Array[Byte] = getBytes("Foo")
val crc16: (Byte, (Byte, Byte)) = { // precomputing for all OSs so that we can have a green run regardless of the OS running the test
AL333Z marked this conversation as resolved.
Show resolved Hide resolved
val os = System.getProperty("os.name").toLowerCase()
if (
os.name.indexOf("nux") > 0 || os.name.indexOf("nix") > 0 || os.indexOf("aix") >= 0
) // UNIX
(3.toByte, (-89.toByte, 119.toByte))
else if (os.indexOf("win") >= 0) // NTFS_FILESYSTEM
(11.toByte, (-107.toByte, -1.toByte))
else if (os.indexOf("mac") >= 0) // MACINTOSH
(7.toByte, (-66.toByte, -77.toByte))
else // UNKNOWN
(255.toByte, (-112.toByte, -55.toByte))
}

val expected = Vector[Byte](
31, // magic number (2B)
-117,
8, // CM
2, // FLG.FHCRC
0, // MTIME (4B)
0,
0,
0,
0, // XFL
crc16._1, // OS
crc16._2._1, // // CRC16 (2B)
crc16._2._2,
115, // compressed blocks
-53,
-49,
7,
0,
-63, // CRC32 (4B)
35,
62,
-76,
3, // ISIZE (4B)
0,
0,
0
)
Stream
.chunk[IO, Byte](Chunk.array(uncompressed))
.through(
Compression[IO].gzip(
fileName = None,
modificationTime = None,
comment = None,
deflateParams = DeflateParams.apply(
bufferSize = 1024 * 32,
header = ZLibParams.Header.GZIP,
level = DeflateParams.Level.DEFAULT,
strategy = DeflateParams.Strategy.DEFAULT,
flushMode = DeflateParams.FlushMode.DEFAULT,
fhCrcEnabled = true
)
)
)
.compile
.toVector
.map(compressed => assertEquals(compressed, expected))
}

test("gunzip limit fileName and comment length") {
val longString: String =
Array
Expand Down
22 changes: 20 additions & 2 deletions core/shared/src/main/scala/fs2/compression/DeflateParams.scala
Expand Up @@ -45,6 +45,13 @@ sealed trait DeflateParams {
*/
val flushMode: DeflateParams.FlushMode

/** A [[Boolean]] indicating whether the `FLG.FHCRC` bit is set. Default is `false`.
* This is provided so that the client can opt-in and enable the CRC16 check for the gzip header.
AL333Z marked this conversation as resolved.
Show resolved Hide resolved
* Why opt-in and not opt-out? It turned out not all clients implemented that right.
* More context [[https://github.com/http4s/http4s/issues/5417 in this issue]].
*/
val fhCrcEnabled: Boolean
AL333Z marked this conversation as resolved.
Show resolved Hide resolved

private[fs2] val bufferSizeOrMinimum: Int = bufferSize.max(128)
}

Expand All @@ -57,14 +64,25 @@ object DeflateParams {
strategy: DeflateParams.Strategy = DeflateParams.Strategy.DEFAULT,
flushMode: DeflateParams.FlushMode = DeflateParams.FlushMode.DEFAULT
): DeflateParams =
DeflateParamsImpl(bufferSize, header, level, strategy, flushMode)
DeflateParamsImpl(bufferSize, header, level, strategy, flushMode, false)

def apply(
bufferSize: Int,
header: ZLibParams.Header,
level: DeflateParams.Level,
strategy: DeflateParams.Strategy,
flushMode: DeflateParams.FlushMode,
fhCrcEnabled: Boolean
): DeflateParams =
DeflateParamsImpl(bufferSize, header, level, strategy, flushMode, fhCrcEnabled)

private case class DeflateParamsImpl(
bufferSize: Int,
header: ZLibParams.Header,
level: DeflateParams.Level,
strategy: DeflateParams.Strategy,
flushMode: DeflateParams.FlushMode
flushMode: DeflateParams.FlushMode,
fhCrcEnabled: Boolean
) extends DeflateParams

sealed abstract class Level(private[fs2] val juzDeflaterLevel: Int)
Expand Down