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
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,29 @@ internal class DeflateLevel(
val chainLength: Int,
// Look ahead one byte when the current match is shorter than this.
val maxLazyLength: Int,
// Reduce the lookahead chain search once the current match reaches this length.
val goodMatchLength: Int,
// Cap the hash table so low levels retain their memory and cache advantage.
val maxHashBits: Int,
// Use bounded dynamic programming instead of greedy or one-byte lazy parsing.
val usesCostAwareParsing: Boolean = false,
)

internal val DEFLATE_LEVELS = arrayOf(
DeflateLevel(niceLength = 0, chainLength = 0, maxLazyLength = 0, maxHashBits = 12),
DeflateLevel(niceLength = 8, chainLength = 4, maxLazyLength = 0, maxHashBits = 12),
DeflateLevel(niceLength = 16, chainLength = 8, maxLazyLength = 0, maxHashBits = 13),
DeflateLevel(niceLength = 16, chainLength = 16, maxLazyLength = 0, maxHashBits = 13),
DeflateLevel(niceLength = 16, chainLength = 32, maxLazyLength = 4, maxHashBits = 14),
DeflateLevel(niceLength = 32, chainLength = 32, maxLazyLength = 16, maxHashBits = 14),
DeflateLevel(niceLength = 128, chainLength = 128, maxLazyLength = 16, maxHashBits = 15),
DeflateLevel(niceLength = 128, chainLength = 256, maxLazyLength = 32, maxHashBits = 15),
DeflateLevel(niceLength = 258, chainLength = 1_024, maxLazyLength = 128, maxHashBits = 16),
DeflateLevel(niceLength = 0, chainLength = 0, maxLazyLength = 0, goodMatchLength = 0, maxHashBits = 12),
DeflateLevel(niceLength = 8, chainLength = 4, maxLazyLength = 0, goodMatchLength = 0, maxHashBits = 12),
DeflateLevel(niceLength = 16, chainLength = 8, maxLazyLength = 0, goodMatchLength = 0, maxHashBits = 13),
DeflateLevel(niceLength = 16, chainLength = 16, maxLazyLength = 0, goodMatchLength = 0, maxHashBits = 13),
DeflateLevel(niceLength = 16, chainLength = 32, maxLazyLength = 4, goodMatchLength = 4, maxHashBits = 14),
DeflateLevel(niceLength = 32, chainLength = 32, maxLazyLength = 16, goodMatchLength = 8, maxHashBits = 14),
DeflateLevel(niceLength = 128, chainLength = 128, maxLazyLength = 16, goodMatchLength = 8, maxHashBits = 15),
DeflateLevel(niceLength = 128, chainLength = 256, maxLazyLength = 32, goodMatchLength = 8, maxHashBits = 15),
DeflateLevel(niceLength = 258, chainLength = 1_024, maxLazyLength = 128, goodMatchLength = 32, maxHashBits = 16),
DeflateLevel(
niceLength = 258,
chainLength = 4_096,
maxLazyLength = 0,
goodMatchLength = 0,
maxHashBits = 20,
usesCostAwareParsing = true,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import com.rafambn.kflate.util.readTwoBytes
import com.rafambn.kflate.util.shiftToNextByte
import com.rafambn.kflate.util.writeBlock
import com.rafambn.kflate.util.writeFixedBlock
import kotlin.math.ceil

internal fun inflate(
inputData: ByteArray,
Expand Down Expand Up @@ -322,7 +321,12 @@ internal fun inflate(

when {
symbol < 256 -> {
ensureCapacity(1, bytesWrittenToOutput)
// The normal unbounded path has spare capacity almost all the time.
// Keep output-limit validation on the bounded path, but avoid doing
// long arithmetic and a function call for every literal otherwise.
if (maxOutputSize != null || bytesWrittenToOutput >= workingBuffer.size) {
ensureCapacity(1, bytesWrittenToOutput)
}
workingBuffer[bytesWrittenToOutput++] = symbol.toByte()
lastBitPosition = currentBitPosition
}
Expand Down Expand Up @@ -380,7 +384,9 @@ internal fun inflate(
currentBitPosition += extraBits
}

ensureCapacity(matchLength, bytesWrittenToOutput)
if (maxOutputSize != null || matchLength > workingBuffer.size - bytesWrittenToOutput) {
ensureCapacity(matchLength, bytesWrittenToOutput)
}

val copyEndIndex = bytesWrittenToOutput + matchLength
val buffer = workingBuffer
Expand Down Expand Up @@ -497,8 +503,7 @@ internal fun deflate(
val prevSize = if (isLastBlock) minOf(MATCH_DISTANCE_MASK + 1, dataSize) else MATCH_DISTANCE_MASK + 1
val prev = state.prev ?: ShortArray(prevSize)
val head = state.head ?: ShortArray(mask + 1)
val baseShift1 = ceil(hashBits / 3.0).toInt()
val baseShift2 = 2 * baseShift1
val hashShift = (hashBits + 2) / 3

val symbols = IntArray(minOf(65536, dataSize))
val literalFrequencies = IntArray(288)
Expand All @@ -513,7 +518,7 @@ internal fun deflate(

if (levelOptions.usesCostAwareParsing) {
while (i < waitIndex && i + 2 < dataSize) {
val hashValue = deflateHash(data, i, baseShift1, baseShift2, mask)
val hashValue = deflateHash(data, i, hashShift, mask)
val iMod = i and MATCH_DISTANCE_MASK
prev[iMod] = head[hashValue]
head[hashValue] = iMod.toShort()
Expand All @@ -531,7 +536,7 @@ internal fun deflate(
while (i < dataSize) {
val hashEnd = minOf(i, dataSize - 2)
while (hashedUntil < hashEnd) {
val hashValue = deflateHash(data, hashedUntil, baseShift1, baseShift2, mask)
val hashValue = deflateHash(data, hashedUntil, hashShift, mask)
val hashIndex = hashedUntil and MATCH_DISTANCE_MASK
prev[hashIndex] = head[hashValue]
head[hashValue] = hashIndex.toShort()
Expand All @@ -546,7 +551,7 @@ internal fun deflate(
var scanIndex = windowStart
while (scanIndex < windowEnd) {
if (scanIndex + 2 >= dataSize) break
val hashValue = deflateHash(data, scanIndex, baseShift1, baseShift2, mask)
val hashValue = deflateHash(data, scanIndex, hashShift, mask)
val scanIndexMod = scanIndex and MATCH_DISTANCE_MASK
val previousIndex = head[hashValue].toInt() and 0xFFFF
prev[scanIndexMod] = previousIndex.toShort()
Expand Down Expand Up @@ -599,8 +604,9 @@ internal fun deflate(
}
waitIndex = i
} else {
var hashWindow = if (i + 2 < dataSize) deflateHashWindow(data, i) else 0
while (i + 2 < dataSize) {
val hashValue = deflateHash(data, i, baseShift1, baseShift2, mask)
val hashValue = deflateHash(hashWindow, hashShift, mask)
val iMod = i and MATCH_DISTANCE_MASK
val pIMod = head[hashValue].toInt() and 0xFFFF
prev[iMod] = pIMod.toShort()
Expand Down Expand Up @@ -631,8 +637,17 @@ internal fun deflate(

val nextMatch = if (shouldSearchLazyMatch(length, levelOptions.maxLazyLength, remaining)) {
val nextIndex = i + 1
val nextHash = deflateHash(data, nextIndex, baseShift1, baseShift2, mask)
findLongestMatch(data, dataSize, nextIndex, head[nextHash].toInt() and 0xFFFF, prev, levelOptions)
val nextHashWindow = updateDeflateHashWindow(hashWindow, data[i + 3])
val nextHash = deflateHash(nextHashWindow, hashShift, mask)
findLongestMatch(
data,
dataSize,
nextIndex,
head[nextHash].toInt() and 0xFFFF,
prev,
levelOptions,
minimumLength = length,
)
} else {
0
}
Expand Down Expand Up @@ -660,6 +675,9 @@ internal fun deflate(
++literalFrequencies[data[i].toInt() and 0xFF]
}
}
if (i + 3 < dataSize) {
hashWindow = updateDeflateHashWindow(hashWindow, data[i + 3])
}
i++
}

Expand Down Expand Up @@ -816,10 +834,26 @@ internal fun shouldSearchLazyMatch(length: Int, maxLazyLength: Int, remaining: I
return maxLazyLength > 0 && length in 3 until maxLazyLength && remaining > length + 1
}

private fun deflateHash(data: ByteArray, index: Int, shift1: Int, shift2: Int, mask: Int): Int {
private fun deflateHash(data: ByteArray, index: Int, hashShift: Int, mask: Int): Int {
return ((data[index].toInt() and 0xFF) xor
((data[index + 1].toInt() and 0xFF) shl shift1) xor
((data[index + 2].toInt() and 0xFF) shl shift2)) and mask
((data[index + 1].toInt() and 0xFF) shl hashShift) xor
((data[index + 2].toInt() and 0xFF) shl (2 * hashShift))) and mask
}

private fun deflateHashWindow(data: ByteArray, index: Int): Int {
return (data[index].toInt() and 0xFF) or
((data[index + 1].toInt() and 0xFF) shl 8) or
((data[index + 2].toInt() and 0xFF) shl 16)
}

private fun updateDeflateHashWindow(window: Int, nextByte: Byte): Int {
return (window ushr 8) or ((nextByte.toInt() and 0xFF) shl 16)
}

private fun deflateHash(window: Int, hashShift: Int, mask: Int): Int {
return ((window and 0xFF) xor
(((window ushr 8) and 0xFF) shl hashShift) xor
((window ushr 16) shl (2 * hashShift))) and mask
}

private fun findLongestMatch(
Expand All @@ -829,6 +863,7 @@ private fun findLongestMatch(
previousIndex: Int,
previous: ShortArray,
level: DeflateLevel,
minimumLength: Int = 2,
): Int {
val remaining = dataSize - index
var currentIndex = index and MATCH_DISTANCE_MASK
Expand All @@ -840,7 +875,10 @@ private fun findLongestMatch(
val maxDistance = minOf(MATCH_DISTANCE_MASK, index)
val maxLength = minOf(MAX_MATCH_LENGTH, remaining)
var remainingChain = level.chainLength
var bestLength = 2
if (level.goodMatchLength > 0 && minimumLength >= level.goodMatchLength) {
remainingChain = maxOf(1, remainingChain shr 2)
}
var bestLength = minimumLength
var bestDistance = 0

while (distance <= maxDistance && --remainingChain != 0 && currentIndex != candidateIndex) {
Expand Down
41 changes: 41 additions & 0 deletions kflate/src/jvmTest/kotlin/AlgorithmCoverageTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import com.rafambn.kflate.huffman.generateLengthCodes
import com.rafambn.kflate.huffman.validateHuffmanCodeLengths
import com.rafambn.kflate.streaming.DeflateState
import com.rafambn.kflate.streaming.InflateState
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.Inflater
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
Expand Down Expand Up @@ -238,6 +240,8 @@ class AlgorithmCoverageTest {
})
assertEquals(0, DEFLATE_LEVELS[3].maxLazyLength)
assertTrue(DEFLATE_LEVELS[4].maxLazyLength > 0)
assertEquals(8, DEFLATE_LEVELS[6].goodMatchLength)
assertEquals(32, DEFLATE_LEVELS[8].goodMatchLength)
assertTrue(!DEFLATE_LEVELS[8].usesCostAwareParsing)
assertTrue(DEFLATE_LEVELS[9].usesCostAwareParsing)

Expand All @@ -248,6 +252,21 @@ class AlgorithmCoverageTest {
assertTrue(!shouldSearchLazyMatch(length = 3, maxLazyLength = 4, remaining = 4))
}

@Test
fun lazyLevelsRoundTripThresholdAndWindowInputs() {
val inputs = listOf(
("abcdefgh".repeat(256) + "abcdefgX" + "abcdefgh".repeat(256)).encodeToByteArray(),
("0123456789abcdef".repeat(2_100) + "01234567").encodeToByteArray(),
)

for (level in 4..8) {
for (input in inputs) {
val compressed = KFlate.compress(input, CompressionRaw(level = level))
assertContentEquals(input, inflateWithJava(compressed))
}
}
}

@Test
fun costAwareParsingChoosesTheCheapestTokenPath() {
assertEquals(8, fixedLiteralBitCost(42))
Expand Down Expand Up @@ -380,4 +399,26 @@ class AlgorithmCoverageTest {
deflater.end()
}
}

private fun inflateWithJava(input: ByteArray): ByteArray {
val inflater = Inflater(true)
return try {
inflater.setInput(input)
val output = ByteArrayOutputStream()
val buffer = ByteArray(8_192)
while (!inflater.finished()) {
val count = inflater.inflate(buffer)
if (count == 0) {
check(!inflater.needsInput() && !inflater.needsDictionary()) {
"Java Inflater stopped before finishing"
}
} else {
output.write(buffer, 0, count)
}
}
output.toByteArray()
} finally {
inflater.end()
}
}
}
Loading