Add lax gzip implementation

This is closer to the client's implementation, which ignores the gzip
header/trailer entirely and just decompresses the inner DEFLATE stream.

While Jagex always produce valid gzip files, some cache editors in the
private server scene do not set the checksum/length correctly in the
trailer. I'm planning to add an option to use the lax gzip
implementation to support reading these caches.

Signed-off-by: Graham <gpe@openrs2.org>
pull/132/head
Graham 3 years ago
parent 689fe7c372
commit db43c73085
  1. 2
      compress-cli/src/main/kotlin/org/openrs2/compress/cli/CompressCommand.kt
  2. 22
      compress-cli/src/main/kotlin/org/openrs2/compress/cli/gzip/GunzipLaxCommand.kt
  3. 4
      compress/src/main/kotlin/org/openrs2/compress/gzip/Gzip.kt
  4. 116
      compress/src/main/kotlin/org/openrs2/compress/gzip/GzipLaxInputStream.kt

@ -7,6 +7,7 @@ import org.openrs2.compress.cli.bzip2.Bzip2Command
import org.openrs2.compress.cli.deflate.DeflateCommand
import org.openrs2.compress.cli.deflate.InflateCommand
import org.openrs2.compress.cli.gzip.GunzipCommand
import org.openrs2.compress.cli.gzip.GunzipLaxCommand
import org.openrs2.compress.cli.gzip.GzipCommand
import org.openrs2.compress.cli.lzma.LzmaCommand
import org.openrs2.compress.cli.lzma.UnlzmaCommand
@ -22,6 +23,7 @@ public class CompressCommand : NoOpCliktCommand(name = "compress") {
InflateCommand(),
GzipCommand(),
GunzipCommand(),
GunzipLaxCommand(),
LzmaCommand(),
UnlzmaCommand()
)

@ -0,0 +1,22 @@
package org.openrs2.compress.cli.gzip
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.defaultStdin
import com.github.ajalt.clikt.parameters.types.defaultStdout
import com.github.ajalt.clikt.parameters.types.inputStream
import com.github.ajalt.clikt.parameters.types.outputStream
import org.openrs2.compress.gzip.Gzip
public class GunzipLaxCommand : CliktCommand(name = "gunzip-lax") {
private val input by option().inputStream().defaultStdin()
private val output by option().outputStream(truncateExisting = true).defaultStdout()
override fun run() {
Gzip.createLaxInputStream(input).use { input ->
output.use { output ->
input.copyTo(output)
}
}
}
}

@ -21,4 +21,8 @@ public object Gzip {
): OutputStream {
return GzipLevelOutputStream(SkipOutputStream(output, HEADER.size.toLong()), level)
}
public fun createLaxInputStream(input: InputStream): InputStream {
return GzipLaxInputStream(input)
}
}

@ -0,0 +1,116 @@
package org.openrs2.compress.gzip
import java.io.DataInputStream
import java.io.IOException
import java.io.InputStream
import java.util.zip.Inflater
public class GzipLaxInputStream(
private val input: InputStream
) : InputStream() {
private val inflater = Inflater(true)
private val buffer = ByteArray(4096)
private var checkedTrailer = false
init {
val dataInput = DataInputStream(input)
if (dataInput.readUnsignedShort() != HEADER_MAGIC) {
throw IOException("Invalid GZIP header magic")
} else if (dataInput.readUnsignedByte() != METHOD_DEFLATE) {
throw IOException("Unsupported compression method")
}
val flags = dataInput.readUnsignedByte()
dataInput.skip(6)
if ((flags and FLAG_EXTRA) != 0) {
dataInput.skip(dataInput.readUnsignedShort().toLong())
}
if ((flags and FLAG_NAME) != 0) {
while (dataInput.readUnsignedByte() != 0) {
// empty
}
}
if ((flags and FLAG_COMMENT) != 0) {
while (dataInput.readUnsignedByte() != 0) {
// empty
}
}
if ((flags and FLAG_HEADER_CRC) != 0) {
dataInput.skip(2)
}
}
override fun read(): Int {
val n = read(buffer, 0, 1)
return if (n < 0) {
-1
} else {
buffer[0].toInt() and 0xFF
}
}
override fun read(b: ByteArray, off: Int, len: Int): Int {
while (true) {
val n = inflater.inflate(b, off, len)
if (n != 0) {
return n
}
when {
inflater.finished() -> {
checkTrailer()
return -1
}
inflater.needsInput() -> fill()
inflater.needsDictionary() -> throw IOException("Dictionaries not supported")
}
}
}
override fun close() {
inflater.end()
input.close()
}
private fun fill() {
val n = input.read(buffer, 0, buffer.size)
if (n < 0) {
throw IOException("Compressed data truncated")
}
inflater.setInput(buffer, 0, n)
}
private fun checkTrailer() {
if (checkedTrailer) {
return
}
checkedTrailer = true
var len = TRAILER_LEN
len -= inflater.remaining
if (len < 0) {
throw IOException("Compressed data overflow")
} else if (input.skip(len) != len) {
throw IOException("GZIP trailer missing")
}
}
private companion object {
private const val HEADER_MAGIC = 0x1F8B
private const val METHOD_DEFLATE = 8
private const val FLAG_HEADER_CRC = 0x2
private const val FLAG_EXTRA = 0x4
private const val FLAG_NAME = 0x8
private const val FLAG_COMMENT = 0x10
private const val TRAILER_LEN = 8L
}
}
Loading…
Cancel
Save