Add JNR-based bzip2 implementation compatible with Jagex's

Signed-off-by: Graham <gpe@openrs2.org>
Graham 3 years ago
parent a6dbb29ea0
commit c21895f052
  1. 1
      compress/build.gradle.kts
  2. 20
      compress/src/main/kotlin/org/openrs2/compress/bzip2/Bzip2.kt
  3. 98
      compress/src/main/kotlin/org/openrs2/compress/bzip2/Bzip2OutputStream.kt
  4. 64
      compress/src/main/kotlin/org/openrs2/compress/bzip2/LibBzip2.kt
  5. 1
      gradle/libs.versions.toml

@ -9,6 +9,7 @@ dependencies {
api(libs.xz) api(libs.xz)
implementation(projects.util) implementation(projects.util)
implementation(libs.jnr)
} }
publishing { publishing {

@ -1,5 +1,6 @@
package org.openrs2.compress.bzip2 package org.openrs2.compress.bzip2
import com.github.michaelbull.logging.InlineLogger
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream
import org.openrs2.util.io.SkipOutputStream import org.openrs2.util.io.SkipOutputStream
@ -17,11 +18,28 @@ public object Bzip2 {
('0' + BLOCK_SIZE).code.toByte() ('0' + BLOCK_SIZE).code.toByte()
) )
private val logger = InlineLogger()
private val library: LibBzip2? = try {
LibBzip2.load()
} catch (ex: Throwable) {
logger.warn(ex) {
"Falling back to pure Java bzip2 implementation, " +
"output may not be bit-for-bit identical to Jagex's implementation"
}
null
}
public fun createHeaderlessInputStream(input: InputStream): InputStream { public fun createHeaderlessInputStream(input: InputStream): InputStream {
return BZip2CompressorInputStream(SequenceInputStream(ByteArrayInputStream(HEADER), input)) return BZip2CompressorInputStream(SequenceInputStream(ByteArrayInputStream(HEADER), input))
} }
public fun createHeaderlessOutputStream(output: OutputStream): OutputStream { public fun createHeaderlessOutputStream(output: OutputStream): OutputStream {
return BZip2CompressorOutputStream(SkipOutputStream(output, HEADER.size.toLong()), BLOCK_SIZE) val skipOutput = SkipOutputStream(output, HEADER.size.toLong())
return if (library != null) {
Bzip2OutputStream(library, skipOutput, BLOCK_SIZE)
} else {
BZip2CompressorOutputStream(skipOutput, BLOCK_SIZE)
}
} }
} }

@ -0,0 +1,98 @@
package org.openrs2.compress.bzip2
import jnr.ffi.Runtime
import java.io.FilterOutputStream
import java.io.IOException
import java.io.OutputStream
import kotlin.math.min
public class Bzip2OutputStream(
private val library: LibBzip2,
out: OutputStream,
blockSize: Int
) : FilterOutputStream(out) {
private val singleByteBuf = ByteArray(1)
private val runtime = Runtime.getRuntime(library)
private val stream = LibBzip2.BzStream(runtime)
private val nextIn = runtime.memoryManager.allocateDirect(BUFFER_SIZE, false)
private val nextOut = runtime.memoryManager.allocateDirect(BUFFER_SIZE, false)
private val buf = ByteArray(BUFFER_SIZE)
private var closed = false
init {
val result = library.BZ2_bzCompressInit(stream, blockSize, 0, 0)
if (result != LibBzip2.BZ_OK) {
throw IOException("bzCompressInit failed: $result")
}
stream.nextIn.set(nextIn)
stream.nextOut.set(nextOut)
}
override fun write(b: Int) {
singleByteBuf[0] = b.toByte()
write(singleByteBuf, 0, singleByteBuf.size)
}
override fun write(b: ByteArray, off: Int, len: Int) {
var off = off
var remaining = len
while (remaining > 0) {
val availIn = min(remaining, BUFFER_SIZE)
nextIn.put(0, b, off, availIn)
stream.nextIn.set(nextIn)
stream.availIn.set(availIn)
stream.nextOut.set(nextOut)
stream.availOut.set(BUFFER_SIZE)
val result = library.BZ2_bzCompress(stream, LibBzip2.BZ_RUN)
if (result != LibBzip2.BZ_RUN_OK) {
throw IOException("bzCompress failed: $result")
}
val read = (availIn - stream.availIn.get()).toInt()
off += read
remaining -= read
val written = (BUFFER_SIZE - stream.availOut.get()).toInt()
nextOut.get(0, buf, 0, written)
out.write(buf, 0, written)
}
}
override fun close() {
if (closed) {
return
}
closed = true
try {
do {
stream.nextOut.set(nextOut)
stream.availOut.set(BUFFER_SIZE)
val streamEnd = when (val result = library.BZ2_bzCompress(stream, LibBzip2.BZ_FINISH)) {
LibBzip2.BZ_STREAM_END -> true
LibBzip2.BZ_FINISH_OK -> false
else -> throw IOException("bzCompress failed: $result")
}
val written = (BUFFER_SIZE - stream.availOut.get()).toInt()
nextOut.get(0, buf, 0, written)
out.write(buf, 0, written)
} while (!streamEnd)
} finally {
val result = library.BZ2_bzCompressEnd(stream)
if (result != LibBzip2.BZ_OK) {
throw IOException("bzCompressEnd failed: $result")
}
}
}
private companion object {
private const val BUFFER_SIZE = 4096
}
}

@ -0,0 +1,64 @@
package org.openrs2.compress.bzip2
import jnr.ffi.LibraryLoader
import jnr.ffi.LibraryOption
import jnr.ffi.Runtime
import jnr.ffi.Struct
import jnr.ffi.annotations.Direct
public interface LibBzip2 {
public class BzStream(runtime: Runtime) : Struct(runtime) {
public val nextIn: Pointer = Pointer()
public val availIn: Unsigned32 = Unsigned32()
public val totalInLo32: Unsigned32 = Unsigned32()
public val totalInHi32: Unsigned32 = Unsigned32()
public val nextOut: Pointer = Pointer()
public val availOut: Unsigned32 = Unsigned32()
public val totalOutLo32: Unsigned32 = Unsigned32()
public val totalOutHi32: Unsigned32 = Unsigned32()
public val state: Pointer = Pointer()
public val alloc: Pointer = Pointer()
public val free: Pointer = Pointer()
public val opaque: Pointer = Pointer()
}
public fun BZ2_bzCompressInit(@Direct stream: BzStream, blockSize100k: Int, verbosity: Int, workFactor: Int): Int
public fun BZ2_bzCompress(stream: BzStream, action: Int): Int
public fun BZ2_bzCompressEnd(stream: BzStream): Int
public fun BZ2_bzDecompressInit(@Direct stream: BzStream, blockSize100k: Int, verbosity: Int, small: Int): Int
public fun BZ2_bzDecompress(stream: BzStream): Int
public fun BZ2_bzDecompressEnd(stream: BzStream): Int
public companion object {
public const val BZ_RUN: Int = 0
public const val BZ_FLUSH: Int = 1
public const val BZ_FINISH: Int = 2
public const val BZ_OK: Int = 0
public const val BZ_RUN_OK: Int = 1
public const val BZ_FLUSH_OK: Int = 2
public const val BZ_FINISH_OK: Int = 3
public const val BZ_STREAM_END: Int = 4
public const val BZ_SEQUENCE_ERROR: Int = -1
public const val BZ_PARAM_ERROR: Int = -2
public const val BZ_MEM_ERROR: Int = -3
public const val BZ_DATA_ERROR: Int = -4
public const val BZ_DATA_ERROR_MAGIC: Int = -5
public const val BZ_IO_ERROR: Int = -6
public const val BZ_UNEXPECTED_EOF: Int = -7
public const val BZ_OUTBUFF_FULL: Int = -8
public const val BZ_CONFIG_ERROR: Int = -9
public fun load(): LibBzip2 {
return LibraryLoader.loadLibrary(
LibBzip2::class.java, mapOf(
LibraryOption.LoadNow to true,
), "bz2"
)
}
}
}

@ -48,6 +48,7 @@ jdom = { module = "org.jdom:jdom2", version = "2.0.6.1" }
jgrapht = { module = "org.jgrapht:jgrapht-core", version = "1.5.1" } jgrapht = { module = "org.jgrapht:jgrapht-core", version = "1.5.1" }
jimfs = { module = "com.google.jimfs:jimfs", version.ref = "jimfs" } jimfs = { module = "com.google.jimfs:jimfs", version.ref = "jimfs" }
jquery = { module = "org.webjars:jquery", version = "3.6.0" } jquery = { module = "org.webjars:jquery", version = "3.6.0" }
jnr = { module = "com.github.jnr:jnr-ffi", version = "2.2.11" }
jsoup = { module = "org.jsoup:jsoup", version = "1.14.3" } jsoup = { module = "org.jsoup:jsoup", version = "1.14.3" }
junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version = { strictly = "5.8.2" } } junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version = { strictly = "5.8.2" } }
junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } junit-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }

Loading…
Cancel
Save