diff --git a/compress-cli/src/main/kotlin/org/openrs2/compress/cli/CompressCommand.kt b/compress-cli/src/main/kotlin/org/openrs2/compress/cli/CompressCommand.kt index f3544743..c77ea13c 100644 --- a/compress-cli/src/main/kotlin/org/openrs2/compress/cli/CompressCommand.kt +++ b/compress-cli/src/main/kotlin/org/openrs2/compress/cli/CompressCommand.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() ) diff --git a/compress-cli/src/main/kotlin/org/openrs2/compress/cli/gzip/GunzipLaxCommand.kt b/compress-cli/src/main/kotlin/org/openrs2/compress/cli/gzip/GunzipLaxCommand.kt new file mode 100644 index 00000000..d2616628 --- /dev/null +++ b/compress-cli/src/main/kotlin/org/openrs2/compress/cli/gzip/GunzipLaxCommand.kt @@ -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) + } + } + } +} diff --git a/compress/src/main/kotlin/org/openrs2/compress/gzip/Gzip.kt b/compress/src/main/kotlin/org/openrs2/compress/gzip/Gzip.kt index 037ebf40..44073769 100644 --- a/compress/src/main/kotlin/org/openrs2/compress/gzip/Gzip.kt +++ b/compress/src/main/kotlin/org/openrs2/compress/gzip/Gzip.kt @@ -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) + } } diff --git a/compress/src/main/kotlin/org/openrs2/compress/gzip/GzipLaxInputStream.kt b/compress/src/main/kotlin/org/openrs2/compress/gzip/GzipLaxInputStream.kt new file mode 100644 index 00000000..68b324a1 --- /dev/null +++ b/compress/src/main/kotlin/org/openrs2/compress/gzip/GzipLaxInputStream.kt @@ -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 + } +}