Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ plugins {

allprojects {
group = "in.dragonbra"
version = "1.8.0"
version = "1.8.1-SNAPSHOT"
}

repositories {
Expand Down
14 changes: 2 additions & 12 deletions src/main/java/in/dragonbra/javasteam/util/VZipUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import `in`.dragonbra.javasteam.util.stream.BinaryReader
import `in`.dragonbra.javasteam.util.stream.MemoryStream
import `in`.dragonbra.javasteam.util.stream.SeekOrigin
import org.tukaani.xz.LZMAInputStream
import java.util.zip.*
import java.util.zip.DataFormatException
import kotlin.math.max

@Suppress("SpellCheckingInspection", "unused")
Expand All @@ -21,11 +21,6 @@ object VZipUtil {

private const val VERSION: Byte = 'a'.code.toByte()

// Thread-local window buffer pool to avoid repeated allocations
private val windowBufferPool = ThreadLocal.withInitial {
ByteArray(1 shl 23) // 8MB max size
}

@JvmStatic
fun decompress(ms: MemoryStream, destination: ByteArray, verifyChecksum: Boolean = true): Int {
try {
Expand Down Expand Up @@ -67,12 +62,7 @@ object VZipUtil {

// If the value of dictionary size in properties is smaller than (1 << 12),
// the LZMA decoder must set the dictionary size variable to (1 << 12).
val windowSize = max(1 shl 12, dictionarySize)
val windowBuffer = if (windowSize <= (1 shl 23)) {
windowBufferPool.get() // Reuse thread-local buffer
} else {
ByteArray(windowSize) // Fallback for unusually large windows
}
val windowBuffer = ByteArray(max(1 shl 12, dictionarySize))
val bytesRead = LZMAInputStream(
ms,
sizeDecompressed.toLong(),
Expand Down
32 changes: 26 additions & 6 deletions src/main/java/in/dragonbra/javasteam/util/VZstdUtil.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package `in`.dragonbra.javasteam.util

import com.github.luben.zstd.Zstd
import com.github.luben.zstd.ZstdInputStream
import `in`.dragonbra.javasteam.util.log.LogManager
import java.io.ByteArrayInputStream
import java.io.IOException
import java.nio.ByteBuffer
import java.nio.ByteOrder

object VZstdUtil {

private const val VZSTD_HEADER: Int = 0x615A5356
private const val STREAM_CHUNK_SIZE = 64 * 1024 // 64KB chunks
private const val HEADER_SIZE = 8
private const val FOOTER_SIZE = 15

Expand Down Expand Up @@ -53,13 +55,31 @@ object VZstdUtil {
throw IllegalArgumentException("The destination buffer is smaller than the decompressed data size.")
}

val compressedData = buffer.copyOfRange(HEADER_SIZE, buffer.size - FOOTER_SIZE) // :( allocations

try {
val bytesDecompressed = Zstd.decompress(destination, compressedData)
// Use streaming decompression to avoid long JNI critical locks
var totalDecompressed = 0

// Use direct ByteArrayInputStream with offset to avoid copying compressed data
ByteArrayInputStream(buffer, HEADER_SIZE, buffer.size - FOOTER_SIZE).use { byteStream ->
ZstdInputStream(byteStream).use { zstdStream ->
var bytesRead: Int
var offset = 0

// Read in chunks to break up JNI locks
while (offset < sizeDecompressed) {
val toRead = minOf(STREAM_CHUNK_SIZE, sizeDecompressed - offset)
bytesRead = zstdStream.read(destination, offset, toRead)

if (bytesRead == -1) break

offset += bytesRead
totalDecompressed += bytesRead
}
}
}

if (bytesDecompressed != sizeDecompressed.toLong()) {
throw IOException("Failed to decompress Zstd (expected $sizeDecompressed bytes, got $bytesDecompressed).")
if (totalDecompressed != sizeDecompressed) {
throw IOException("Failed to decompress Zstd (expected $sizeDecompressed bytes, got $totalDecompressed).")
}

if (verifyChecksum) {
Expand Down