Skip to content

Commit 69749ce

Browse files
committed
feat(bitpacker): implement BitPacker class for bit manipulation and packing
1 parent 2a8b474 commit 69749ce

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package com.example.bip39.bit;
2+
3+
import java.util.Objects;
4+
5+
public final class BitPacker {
6+
7+
private static final int ELEVEN_BIT_GROUP_SIZE = 11;
8+
9+
private BitPacker() {}
10+
11+
public static int readBits(byte[] source, int bitOffset, int bitLength) {
12+
Objects.requireNonNull(source, "source must not be null");
13+
validateBitAccess(source.length, bitOffset, bitLength);
14+
if (bitLength > Integer.SIZE - 1) {
15+
throw new IllegalArgumentException("bitLength must be at most 31");
16+
}
17+
18+
int value = 0;
19+
for (int bitIndex = 0; bitIndex < bitLength; bitIndex++) {
20+
int absoluteBitIndex = bitOffset + bitIndex;
21+
int byteIndex = absoluteBitIndex / Byte.SIZE;
22+
int bitIndexInByte = 7 - (absoluteBitIndex % Byte.SIZE);
23+
int bit = ((source[byteIndex] & 0xff) >> bitIndexInByte) & 0x01;
24+
value = (value << 1) | bit;
25+
}
26+
return value;
27+
}
28+
29+
public static void writeBits(byte[] target, int bitOffset, int bitLength, int value) {
30+
Objects.requireNonNull(target, "target must not be null");
31+
validateBitAccess(target.length, bitOffset, bitLength);
32+
if (bitLength > Integer.SIZE - 1) {
33+
throw new IllegalArgumentException("bitLength must be at most 31");
34+
}
35+
if (bitLength != 0 && value >>> bitLength != 0) {
36+
throw new IllegalArgumentException("value does not fit into the requested bit length");
37+
}
38+
39+
for (int bitIndex = 0; bitIndex < bitLength; bitIndex++) {
40+
int absoluteBitIndex = bitOffset + bitIndex;
41+
int byteIndex = absoluteBitIndex / Byte.SIZE;
42+
int bitIndexInByte = 7 - (absoluteBitIndex % Byte.SIZE);
43+
int bitMask = 1 << bitIndexInByte;
44+
int bit = (value >> (bitLength - 1 - bitIndex)) & 0x01;
45+
int clearedByte = target[byteIndex] & ~bitMask;
46+
target[byteIndex] = (byte) (clearedByte | (bit << bitIndexInByte));
47+
}
48+
}
49+
50+
public static int[] splitTo11BitValues(byte[] source, int totalBitLength) {
51+
Objects.requireNonNull(source, "source must not be null");
52+
validateTotalBitLength(source.length, totalBitLength);
53+
if (totalBitLength % ELEVEN_BIT_GROUP_SIZE != 0) {
54+
throw new IllegalArgumentException("totalBitLength must be divisible by 11");
55+
}
56+
57+
int[] values = new int[totalBitLength / ELEVEN_BIT_GROUP_SIZE];
58+
for (int valueIndex = 0; valueIndex < values.length; valueIndex++) {
59+
values[valueIndex] =
60+
readBits(source, valueIndex * ELEVEN_BIT_GROUP_SIZE, ELEVEN_BIT_GROUP_SIZE);
61+
}
62+
return values;
63+
}
64+
65+
public static byte[] pack11BitValues(int[] values) {
66+
Objects.requireNonNull(values, "values must not be null");
67+
byte[] packed = new byte[(values.length * ELEVEN_BIT_GROUP_SIZE + 7) / Byte.SIZE];
68+
for (int valueIndex = 0; valueIndex < values.length; valueIndex++) {
69+
int value = values[valueIndex];
70+
if (value < 0 || value > 0x7ff) {
71+
throw new IllegalArgumentException("11-bit value out of range: " + value);
72+
}
73+
writeBits(packed, valueIndex * ELEVEN_BIT_GROUP_SIZE, ELEVEN_BIT_GROUP_SIZE, value);
74+
}
75+
return packed;
76+
}
77+
78+
public static byte[] extractBytes(byte[] source, int bitOffset, int bitLength) {
79+
Objects.requireNonNull(source, "source must not be null");
80+
validateBitAccess(source.length, bitOffset, bitLength);
81+
if (bitLength % Byte.SIZE != 0) {
82+
throw new IllegalArgumentException("bitLength must be divisible by 8");
83+
}
84+
85+
byte[] bytes = new byte[bitLength / Byte.SIZE];
86+
for (int byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
87+
bytes[byteIndex] = (byte) readBits(source, bitOffset + byteIndex * Byte.SIZE, Byte.SIZE);
88+
}
89+
return bytes;
90+
}
91+
92+
private static void validateTotalBitLength(int sourceLengthBytes, int totalBitLength) {
93+
validateBitAccess(sourceLengthBytes, 0, totalBitLength);
94+
}
95+
96+
private static void validateBitAccess(int sourceLengthBytes, int bitOffset, int bitLength) {
97+
if (bitOffset < 0) {
98+
throw new IllegalArgumentException("bitOffset must not be negative");
99+
}
100+
if (bitLength < 0) {
101+
throw new IllegalArgumentException("bitLength must not be negative");
102+
}
103+
if (bitOffset + bitLength > sourceLengthBytes * Byte.SIZE) {
104+
throw new IllegalArgumentException("Requested bits exceed source length");
105+
}
106+
}
107+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.example.bip39.bit;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import org.junit.jupiter.api.Test;
8+
9+
class BitPackerTest {
10+
11+
@Test
12+
void readsBitsMostSignificantBitFirstFromSignedBytes() {
13+
byte[] source = {(byte) 0x80, (byte) 0xf1};
14+
15+
assertEquals(1, BitPacker.readBits(source, 0, 1));
16+
assertEquals(0, BitPacker.readBits(source, 1, 7));
17+
assertEquals(0xf1, BitPacker.readBits(source, 8, 8));
18+
assertEquals(0x01, BitPacker.readBits(source, 12, 4));
19+
}
20+
21+
@Test
22+
void writesBitsAndExtractsBytesWithoutLosingUnsignedValues() {
23+
byte[] target = new byte[3];
24+
25+
BitPacker.writeBits(target, 0, 8, 0x80);
26+
BitPacker.writeBits(target, 8, 8, 0x01);
27+
BitPacker.writeBits(target, 16, 8, 0xff);
28+
29+
assertArrayEquals(new byte[] {(byte) 0x80, 0x01, (byte) 0xff}, target);
30+
assertArrayEquals(target, BitPacker.extractBytes(target, 0, 24));
31+
}
32+
33+
@Test
34+
void extractsBytesAcrossByteBoundaries() {
35+
byte[] source = {0x12, 0x34, 0x56};
36+
37+
assertArrayEquals(new byte[] {0x23, 0x45}, BitPacker.extractBytes(source, 4, 16));
38+
}
39+
40+
@Test
41+
void splitsKnownBitSequenceIntoElevenBitValues() {
42+
byte[] source = {0x12, 0x34, 0x56};
43+
44+
assertArrayEquals(new int[] {145, 1301}, BitPacker.splitTo11BitValues(source, 22));
45+
}
46+
47+
@Test
48+
void packsElevenBitValuesInMostSignificantBitOrder() {
49+
assertArrayEquals(
50+
new byte[] {0x12, 0x34, 0x54}, BitPacker.pack11BitValues(new int[] {145, 1301}));
51+
}
52+
53+
@Test
54+
void elevenBitPackingAndSplittingRoundTrips() {
55+
int[] values = {0, 1, 1024, 2047, 42};
56+
57+
byte[] packed = BitPacker.pack11BitValues(values);
58+
59+
assertArrayEquals(values, BitPacker.splitTo11BitValues(packed, values.length * 11));
60+
}
61+
62+
@Test
63+
void rejectsInvalidBitAccessAndOutOfRangeValues() {
64+
byte[] source = new byte[2];
65+
66+
assertThrows(IllegalArgumentException.class, () -> BitPacker.readBits(source, -1, 1));
67+
assertThrows(IllegalArgumentException.class, () -> BitPacker.readBits(source, 0, 17));
68+
assertThrows(IllegalArgumentException.class, () -> BitPacker.writeBits(source, 0, 3, 0b1000));
69+
assertThrows(
70+
IllegalArgumentException.class, () -> BitPacker.splitTo11BitValues(source, Byte.SIZE));
71+
assertThrows(IllegalArgumentException.class, () -> BitPacker.pack11BitValues(new int[] {2048}));
72+
assertThrows(IllegalArgumentException.class, () -> BitPacker.extractBytes(source, 0, 7));
73+
}
74+
}

0 commit comments

Comments
 (0)