From 16fb6375acda7ed2ddfed6714ce2f05c6305e35a Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 23 Dec 2025 11:45:19 -0300 Subject: [PATCH 1/4] Create BitBuffer --- .../src/me/devnatan/kompress/BitBuffer.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 lib/common/src/me/devnatan/kompress/BitBuffer.kt diff --git a/lib/common/src/me/devnatan/kompress/BitBuffer.kt b/lib/common/src/me/devnatan/kompress/BitBuffer.kt new file mode 100644 index 0000000..d1ced53 --- /dev/null +++ b/lib/common/src/me/devnatan/kompress/BitBuffer.kt @@ -0,0 +1,74 @@ +package me.devnatan.kompress + +/** Internally manages a 32-bit buffer and tracks how many bits are currently stored. */ +internal class BitBuffer { + private var buffer: UInt = 0u + private var bitsInBuffer: Int = 0 + + fun writeBits(value: UInt, numBits: Int) { + require(numBits in 1..32) { "numBits must be between 1 and 32, got $numBits" } + + val masked = if (numBits == 32) { + value + } else { + value and ((1u shl numBits) - 1u) + } + + if (bitsInBuffer + numBits <= 32) { + buffer = buffer or (masked shl bitsInBuffer) + bitsInBuffer += numBits + } else { + // would overflow 32-bit buffer (this shouldn't happen?) + // but we handle it by only taking what fits + val bitsToWrite = 32 - bitsInBuffer + val partialMask = (1u shl bitsToWrite) - 1u + buffer = buffer or ((masked and partialMask) shl bitsInBuffer) + bitsInBuffer = 32 + } + } + + fun readBits(numBits: Int): UInt { + require(numBits in 1..32) { "numBits must be between 1 and 32, got $numBits" } + require(bitsInBuffer >= numBits) { "Not enough bits in buffer: need $numBits, have $bitsInBuffer" } + + val value = if (numBits == 32) + buffer + else + buffer and ((1u shl numBits) - 1u) + + buffer = if (numBits == 32) + 0u + else + buffer shr numBits + + bitsInBuffer -= numBits + + return value + } + + fun hasBits(numBits: Int): Boolean = bitsInBuffer >= numBits + + fun alignToByte() { + val bitsToSkip = bitsInBuffer % 8 + if (bitsToSkip <= 0) return + + buffer = buffer shr bitsToSkip + bitsInBuffer -= bitsToSkip + } + + fun reset() { + buffer = 0u + bitsInBuffer = 0 + } + + fun availableBytes(): Int = bitsInBuffer / 8 + + fun readByte(): UByte { + require(bitsInBuffer >= 8) { "Not enough bits for a byte. Given: $bitsInBuffer" } + return readBits(8).toUByte() + } + + fun writeByte(byte: UByte) { + writeBits(byte.toUInt(), 8) + } +} \ No newline at end of file From f0947d77d20f15e5e1ea8ec5cda465ed55b7f053 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 23 Dec 2025 11:49:38 -0300 Subject: [PATCH 2/4] Write test to read zero and single --- .../me/devnatan/kompress/BitBufferTest.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 lib/common/test/me/devnatan/kompress/BitBufferTest.kt diff --git a/lib/common/test/me/devnatan/kompress/BitBufferTest.kt b/lib/common/test/me/devnatan/kompress/BitBufferTest.kt new file mode 100644 index 0000000..6ae70a5 --- /dev/null +++ b/lib/common/test/me/devnatan/kompress/BitBufferTest.kt @@ -0,0 +1,27 @@ +package me.devnatan.kompress + +import kotlin.test.Test +import kotlin.test.assertEquals + +class BitBufferTest { + + @Test + fun `write and read zero`() { + val buffer = BitBuffer() + buffer.writeBits(0u, 1) + assertEquals( + expected = 0u, + actual = buffer.readBits(1) + ) + } + + @Test + fun `write and read single`() { + val buffer = BitBuffer() + buffer.writeBits(1u, 1) + assertEquals( + expected = 1u, + actual = buffer.readBits(1) + ) + } +} \ No newline at end of file From 3a9a3aac0ccd59480a3b7e6d0c1ce7e359b23739 Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 23 Dec 2025 11:58:43 -0300 Subject: [PATCH 3/4] Write test for 8, 16 and 32 bits compression and read --- .../me/devnatan/kompress/BitBufferTest.kt | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/common/test/me/devnatan/kompress/BitBufferTest.kt b/lib/common/test/me/devnatan/kompress/BitBufferTest.kt index 6ae70a5..259839d 100644 --- a/lib/common/test/me/devnatan/kompress/BitBufferTest.kt +++ b/lib/common/test/me/devnatan/kompress/BitBufferTest.kt @@ -24,4 +24,44 @@ class BitBufferTest { actual = buffer.readBits(1) ) } + + @Test + fun `write and read full`() { + val buffer = BitBuffer() + buffer.writeBits(0xFFu, 8) + assertEquals( + expected = 0xFFu, + actual = buffer.readBits(8) + ) + } + + @Test + fun `write and read 16`() { + val buffer = BitBuffer() + buffer.writeBits(0xABCDu, 16) + assertEquals( + expected = 0xABCDu, + actual = buffer.readBits(16) + ) + } + + @Test + fun `write and read 32`() { + val buffer = BitBuffer() + buffer.writeBits(0x12345678u, 32) + assertEquals( + expected = 0x12345678u, + actual = buffer.readBits(32) + ) + } + + @Test + fun `write and read 32 max`() { + val buffer = BitBuffer() + buffer.writeBits(0xFFFFFFFFu, 32) + assertEquals( + expected = 0xFFFFFFFFu, + actual = buffer.readBits(32) + ) + } } \ No newline at end of file From 5f68d72b490d462dd205ffb6852682ebf516972d Mon Sep 17 00:00:00 2001 From: Natan Date: Tue, 23 Dec 2025 12:00:35 -0300 Subject: [PATCH 4/4] Improve error messages --- lib/common/src/me/devnatan/kompress/BitBuffer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/common/src/me/devnatan/kompress/BitBuffer.kt b/lib/common/src/me/devnatan/kompress/BitBuffer.kt index d1ced53..7cc2586 100644 --- a/lib/common/src/me/devnatan/kompress/BitBuffer.kt +++ b/lib/common/src/me/devnatan/kompress/BitBuffer.kt @@ -6,7 +6,7 @@ internal class BitBuffer { private var bitsInBuffer: Int = 0 fun writeBits(value: UInt, numBits: Int) { - require(numBits in 1..32) { "numBits must be between 1 and 32, got $numBits" } + require(numBits in 1..32) { "`numBits` must be between 1 and 32. Given $numBits" } val masked = if (numBits == 32) { value @@ -28,8 +28,8 @@ internal class BitBuffer { } fun readBits(numBits: Int): UInt { - require(numBits in 1..32) { "numBits must be between 1 and 32, got $numBits" } - require(bitsInBuffer >= numBits) { "Not enough bits in buffer: need $numBits, have $bitsInBuffer" } + require(numBits in 1..32) { "`numBits` must be between 1 and 32. Given: $numBits" } + require(bitsInBuffer >= numBits) { "Not enough bits in buffer. Expected: $numBits, given: $bitsInBuffer" } val value = if (numBits == 32) buffer