Created more robust BitStream implementation

Pending validation, will deprecate and replace com.riiablo.codec.util.BitStream
This commit is contained in:
Collin Smith 2020-08-05 19:57:54 -07:00
parent 84748c56a3
commit cfb2850d92
2 changed files with 849 additions and 0 deletions

View File

@ -0,0 +1,553 @@
package com.riiablo.util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.ByteBuffer;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.Validate;
public class BitStream {
private static final BitStream EMPTY_BITSTREAM = new BitStream(new byte[0]);
public static BitStream emptyBitStream() {
return EMPTY_BITSTREAM;
}
public static BitStream wrap(byte[] bytes) {
return bytes == null ? emptyBitStream() : new BitStream(bytes);
}
/**
* Maximum number of bits that are safe to represent an unsigned value with.
*/
private static final int MAX_CACHE_SIZE = Long.SIZE - 1;
private static final long[] MASKS = new long[MAX_CACHE_SIZE + 1];
static {
for (int i = 1; i <= MAX_CACHE_SIZE; i++) {
MASKS[i] = (MASKS[i - 1] << 1) + 1;
}
}
/**
* Maximum number of bits a {@code long} can contain without overflowing when
* performing a bitwise {@code <<} by {@value Byte#SIZE}.
*/
private static final int MAX_SAFE_CACHE_SIZE = Long.SIZE - Byte.SIZE;
/**
* Expected size of the signature when calling {@link #skipUntil(byte[])}.
* This value is hard-coded and fixed due to limitations, however the impl
* may be changed in the future to support dynamic signature lengths. For
* now this is sufficient for any needs related to this project.
*/
// TODO: deprecate and support dynamic signature lengths
private static final int SIGNATURE_SIZE = 2;
/**
* Parent of this bit stream, or {@code null} if it has none.
*/
private final BitStream parent;
/**
* Buffer containing the byte stream to read bits from.
*/
private final ByteBuf buffer;
/**
* Total number of bits within this bit stream. This value may within the
* byte boundary of a byte.
*/
private final long numBits;
/**
* Number of bits read from the underlying byte stream, not including
* {@link #bitsCached}
*/
private long bitsRead;
/**
* Number of bits within {@link #cache} that have been read from the
* underlying byte stream.
*/
private int bitsCached;
/**
* Sequence of bits from the underlying byte stream used to create a number
* with the specified bits and store the overflow for the next read
* operation.
*/
private long cache;
private BitStream(byte[] b) {
parent = null;
buffer = Unpooled.wrappedBuffer(b);
numBits = (long) b.length * Byte.SIZE;
}
// TODO: how to manage skipping the subview?
// drop cache and skip to last byte
// set cache and bitsCached to last byte, or 0 if on boundary
/**
* Contains the logic of constructing a new bit stream as a slice of an
* existing bit stream. This exists because it ensures that when the new bit
* stream is constructed, any modifications to the parent bit stream will
* have completed, rather than the alternative, where {@link #readSlice}
* must construct the child and then move it's read position.
*
* @see #readSlice
*/
private BitStream(BitStream parent, long numBits) {
assert bitsCached < Byte.SIZE : "Expected bitsCached to be at most 7 bits, was: " + bitsCached;
assert numBits > 0 : "Empty bit streams should use emptyBitStream() instead";
assert parent.bitsRead + numBits <= parent.numBits : "numBits cannot exceed the number of bits remaining within the parent bit stream!";
this.parent = parent;
// length should include the last byte that bits belong (round to ceil)
final long length = (numBits - parent.bitsCached + Byte.SIZE - 1) / Byte.SIZE;
assert length <= Integer.MAX_VALUE : "ByteBuf only supports int";
this.buffer = parent.buffer.slice(parent.buffer.readerIndex(), (int) length);
this.numBits = numBits;
cache = parent.cache;
bitsCached = parent.bitsCached;
}
/**
* Returns a slice of this bit stream's sub-region starting at the current
* bit position.
*
* @see #readSlice
*/
private BitStream slice(long numBits) {
if (numBits <= 0) return emptyBitStream();
Validate.isTrue(bitsRead + numBits <= this.numBits,
"numBits cannot exceed the number of bits remaining within this bit stream! %d >= %d",
bitsRead + numBits,
this.numBits);
return new BitStream(this, numBits);
}
/**
* Returns a slice of this bit stream's sub-region starting at the current
* bit position and increases the bit position of this bit stream by the size
* of the new slice.
*
* @see #slice
*/
public BitStream readSlice(long numBits) {
BitStream slice = slice(numBits);
if (numBits <= 0) return slice;
// length should not include last byte in case of overflow -- skip will need to read this byte
// in order to set cache properly (round to floor)
final long length = (numBits - bitsCached) / Byte.SIZE;
assert length <= Integer.MAX_VALUE : "ByteBuf only supports int";
buffer.skipBytes((int) length);
final int overflowBits = (int) ((bitsRead + numBits) % Byte.SIZE);
bitsRead += (numBits - overflowBits);
clearCache();
skip(overflowBits);
return slice;
}
/**
* Returns a copy of the bytes backing this bit stream. This method is unsafe
* and only provided temporarily.
*/
@Deprecated
public byte[] copyBytes() {
if (!buffer.isReadable()) return ArrayUtils.EMPTY_BYTE_ARRAY;
byte[] copy = new byte[buffer.capacity()];
buffer.getBytes(0, copy);
return copy;
}
/**
* Clears the overflow bits of the previously read byte.
*/
public void clearCache() {
bitsCached = 0;
cache = 0L;
}
/**
* Returns the number of bits that have been read from the stream and stored
* within {@link #cache()}.
*/
public int bitsCached() {
return bitsCached;
}
/**
* Returns the contents of the cache used to construct numbers. After a read
* operation completes, this will be filled with at most 7 bits.
*/
public long cache() {
return cache;
}
public int bytePosition() {
return buffer.readerIndex() + (parent != null ? parent.bytePosition() : 0);
}
public int bytesAvailable() {
return buffer.readableBytes();
}
/**
* Returns the absolute bit position of this bit stream. This value includes
* any offsets of any parent bit streams.
*
* @see #bitsRead()
*/
public long bitPosition() {
return bitsRead + (parent != null ? parent.bitPosition() : 0L);
}
public long bitsAvailable() {
return bitsCached + ((long) bytesAvailable() * Byte.SIZE);
}
/**
* Returns the number of bits read by this bit stream.
*
* @see #bitPosition
*/
public long bitsRead() {
return bitsRead;
}
/**
* Returns the number of bits in this bit stream.
*/
public long numBits() {
return numBits;
}
/**
* Skips up to 64 bits.
*/
public BitStream skip(int bits) {
readRaw(bits);
return this;
}
/**
* Skips bits remaining in currently processed byte.
*
* @see #skip
*/
public BitStream alignToByte() {
int bits = bitsCached % Byte.SIZE;
if (bits > 0) skip(bits);
return this;
}
/**
* Consumes bits until the specified sequence of bytes are encountered.
* After this function executes, position will be such that the next read
* operation is at the first byte in the signature, or at the end of the
* byte stream. This function will align the byte stream at the byte
* boundary.
* <p/>
* NOTE: Only supports signatures of exactly {@value #SIGNATURE_SIZE} bytes.
*
* @see #alignToByte
* @see #skip
*/
// FIXME: is it expected behavior to allow throwing EndOfStream if no signature found?
public BitStream skipUntil(byte[] signature) {
Validate.isTrue(signature.length == SIGNATURE_SIZE, "Only supports signature length of " + SIGNATURE_SIZE);
alignToByte();
final byte fb0 = signature[0];
final byte fb1 = signature[1];
byte b0, b1;
b1 = (byte) readUnsigned(Byte.SIZE);
for (;;) {
b0 = b1;
b1 = (byte) readUnsigned(Byte.SIZE);
if (b0 == fb0 && b1 == fb1) {
buffer.readerIndex(buffer.readerIndex() - signature.length);
break;
}
}
// TODO: support dynamic signature lengths
// create a byte[] of size signature.length
// use as a circular buffer with each read byte incrementing index and then going back to
// 0 when signature.length is reached. Comparisons will need to be index..length
// and 0..index (and 0..length in case where index == 0)
return this;
}
/**
* Reads up to 63 bits as unsigned and casts the result into a {@code long}.
* {@link #readRaw} should be used if 64 bits need to be read, or the value
* that is being read represents raw memory (i.e., flags).
* <p/>
* <p>{@code bits} should be between [0, {@value #MAX_CACHE_SIZE}].
* <p>Reading {@code 0} bits will always return {@code 0}.
*
* @see #readRaw
* @see #readSigned
* @see #readU7
* @see #readU15
* @see #readU31
* @see #readU63
*/
public long readUnsigned(int bits) {
Validate.inclusiveBetween(0, MAX_CACHE_SIZE, bits);
if (bits == 0) return 0;
if ((bitsRead += bits) > numBits) {
bitsRead = numBits;
throw new EndOfStream();
}
ensureCache(bits);
return bitsCached < bits
? readCacheSafe(bits)
: readCacheUnsafe(bits);
}
/**
* Reads up to 7 bits as unsigned and casts the result into a {@code byte}.
*/
public byte readU7(int bits) {
Validate.isTrue(bits < Byte.SIZE, "only 7 bits can fit into byte and be unsigned. bits: " + bits);
return (byte) readUnsigned(bits);
}
/**
* Reads up to 15 bits as unsigned and casts the result into a {@code short}.
*/
public short readU15(int bits) {
Validate.isTrue(bits < Short.SIZE, "only 15 bits can fit into short and be unsigned. bits: " + bits);
return (short) readUnsigned(bits);
}
/**
* Reads up to 31 bits as unsigned and casts the result into a {@code int}.
*/
public int readU31(int bits) {
Validate.isTrue(bits < Integer.SIZE, "only 31 bits can fit into int and be unsigned. bits: " + bits);
return (int) readUnsigned(bits);
}
/**
* Reads up to 63 bits as unsigned and casts the result into a {@code long}.
*/
public long readU63(int bits) {
Validate.isTrue(bits < Long.SIZE, "only 63 bits can fit into long and be unsigned. bits: " + bits);
return readUnsigned(bits);
}
/**
* Reads up to 64 bits as a {@code long}. This function behaves identically
* to {@link #readUnsigned}, with the exception that it is intended to be
* used to read raw memory and support reading a 64 bit long, since it is
* impossible to encode an unsigned 64-bit number as a {@code long}.
*/
// TODO: there may be a better way to do this, but this is simple.
public long readRaw(int bits) {
Validate.inclusiveBetween(0, Long.SIZE, bits);
long lo = readUnsigned(bits > Integer.SIZE ? Integer.SIZE : bits);
long hi = readUnsigned(bits > Integer.SIZE ? bits - Integer.SIZE : 0);
return (hi << Integer.SIZE) | lo;
}
/**
* Reads up to 64 bits from the underlying byte stream, sign extending the
* result as a {@code long}.
*
* @see #readUnsigned
* @see #readRaw
*/
public long readSigned(int bits) {
if (bits == Long.SIZE) return readRaw(Long.SIZE);
Validate.inclusiveBetween(0, MAX_CACHE_SIZE, bits);
final int shift = Long.SIZE - bits;
return readUnsigned(bits) << shift >> shift;
}
/**
* Reads a single bit and casts the result into a {@code boolean}.
*/
public boolean readBoolean() {
return readUnsigned(1) == 1L;
}
/**
* Reads a single bit and casts the result into a {@code byte}.
*/
public byte readBit() {
return (byte) readUnsigned(1);
}
/**
* Reads <i>n</i> bytes from the underlying byte stream into the specified
* array. This function will align the byte stream at the byte boundary
* and clear the cache.
*
* @see #alignToByte
* @see #clearCache
* @see #read(byte[], int, int)
*/
public void read(byte[] dst) {
read(dst, 0, dst.length);
}
/**
* Reads <i>n</i> bytes from the underlying byte stream into the specified
* array. This function will align the byte stream at the byte boundary
* and clear the cache.
*
* @see #alignToByte
* @see #clearCache
* @see #read(byte[])
*/
public void read(byte[] dst, int dstOffset, int len) {
alignToByte();
clearCache();
buffer.readBytes(dst, dstOffset, len);
}
/**
* Reads <i>n</i> bytes from the underlying byte stream into a created byte
* array. This function will align the byte stream at the byte boundary
* and clear the cache.
*
* @see #alignToByte
* @see #clearCache
* @see #read(byte[])
* @see #read(byte[], int, int)
*/
public byte[] read(int len) {
byte[] dst = new byte[len];
read(dst);
return dst;
}
/**
* Reads <i>n</i> characters from the underlying byte stream, assuming each
* character contains {@value Byte#SIZE} bits per character. This function
* is guaranteed to read {@code len} characters.
*
* @see #readString(int, int)
*/
public String readString(int len) {
return readString(len, Byte.SIZE);
}
/**
* Reads <i>n</i> characters from the underlying byte stream, assuming each
* character contains {@code bitsPerChar} bits. This function is guaranteed
* to read {@code len} characters.
* <p/>
* Note: This function does not support multi-byte character encoding,
* therefore <pre>bitsPerChar <= {@value Byte#SIZE}</pre>
*
* @see #readString(int)
*/
public String readString(int len, int bitsPerChar) {
Validate.isTrue(len >= 0, "len must be positive!");
Validate.inclusiveBetween(1, Byte.SIZE, bitsPerChar);
return _readString(len, bitsPerChar, false);
}
/**
* Reads up to <i>n</i> characters from the underlying byte stream, assuming
* each character contains {@code bitsPerChar} bits. This function will stop
* reading characters when a null-termination is encountered.
* <p/>
* Note: This function does not support multi-byte character encoding,
* therefore <pre>bitsPerChar <= {@value Byte#SIZE}</pre>
*
* @see #readString(int, int)
*/
public String readString0(int len, int bitsPerChar) {
Validate.isTrue(len >= 0, "len must be positive!");
Validate.inclusiveBetween(1, Byte.SIZE, bitsPerChar);
return _readString(len, bitsPerChar, true);
}
private String _readString(int len, int bitsPerChar, boolean nullTerminate) {
assert len >= 0 : "len must be positive!";
assert bitsPerChar >= 1 && bitsPerChar <= Byte.SIZE : "bitsPerChar should be in (0,8]";
byte[] b = new byte[len];
for (int i = 0; i < len; i++) {
b[i] = (byte) readUnsigned(bitsPerChar);
if (nullTerminate && b[i] == '\0') break;
}
// TODO: Why is this done this roundabout way?
return BufferUtils.readString(ByteBuffer.wrap(b), len);
}
private int readByte() {
try {
return buffer.readByte() & 0xFF;
} catch (IndexOutOfBoundsException t) {
throw new EndOfStream();
}
}
/**
* Ensures {@link #cache} contains at least <i>n</i> bits, up to
* {@value #MAX_SAFE_CACHE_SIZE} bits due to overflow.
*
* @throws EndOfStream if the underlying byte stream did not contain at least
* <i>n</i> bits.
*/
private int ensureCache(int bits) {
while (bitsCached < bits && bitsCached <= MAX_SAFE_CACHE_SIZE) {
final long nextByte = readByte();
cache |= (nextByte << bitsCached);
bitsCached += Byte.SIZE;
}
return bitsCached;
}
/**
* Reads <i>n</i> bits from {@link #cache}, consuming the next byte in the
* underlying byte stream.
* <p/>
* This function asserts that {@link #cache} would have overflowed if
* <i>n</i> bits were read from the underlying byte stream and thus reads
* the next byte accounting for this case.
*/
private long readCacheSafe(int bits) {
final int bitsToAddCount = bits - bitsCached;
final int overflowBits = Byte.SIZE - bitsToAddCount;
final long nextByte = readByte();
long bitsToAdd = nextByte & MASKS[bitsToAddCount];
cache |= (bitsToAdd << bitsCached);
final long overflow = (nextByte >>> bitsToAddCount) & MASKS[overflowBits];
final long bitsOut = bitsCached & MASKS[bits];
cache = overflow;
bitsCached = overflowBits;
return bitsOut;
}
/**
* Reads <i>n</i> bits from {@link #cache}.
* <p/>
* This function asserts {@link #cache} contains at least <i>n</i> bits.
*/
private long readCacheUnsafe(int bits) {
final long bitsOut = cache & MASKS[bits];
cache >>>= bits;
bitsCached -= bits;
return bitsOut;
}
public static class EndOfStream extends RuntimeException {
EndOfStream() {
super("The end of the stream has been reached!");
}
}
}

View File

@ -0,0 +1,296 @@
package com.riiablo.util;
import org.junit.Assert;
import org.junit.Test;
public class BitStreamTest {
@Test
public void no_bits_available_in_empty_stream() {
BitStream b = BitStream.emptyBitStream();
Assert.assertEquals(b.bitsAvailable(), 0L);
}
@Test
public void read_0_bits_from_empty_stream() {
BitStream b = BitStream.emptyBitStream();
b.readUnsigned(0);
}
@Test(expected = BitStream.EndOfStream.class)
public void read_bits_from_empty_stream_throws_EndOfStream_exception() {
BitStream b = BitStream.emptyBitStream();
b.readUnsigned(1);
}
@Test
public void align_byte_when_already_aligned() {
BitStream b = BitStream.wrap(new byte[] {0x00});
long before = b.bitsAvailable();
b.alignToByte();
long after = b.bitsAvailable();
Assert.assertEquals(before, after);
}
@Test
public void align_byte_when_unaligned() {
BitStream b = BitStream.wrap(new byte[]{0x00});
b.skip(1).alignToByte();
Assert.assertEquals(0, b.bitsAvailable());
}
@Test
public void read_hunters_bow_of_blight() {
byte[] bytes = new byte[]{
0x4A, 0x4D, 0x10, 0x00, (byte) 0x80, 0x00, 0x65, 0x00, 0x04,
(byte) 0x82, 0x26, 0x76, 0x07, (byte) 0x82, 0x09, (byte) 0xD4,
(byte) 0xAA, 0x12, 0x03, 0x01, (byte) 0x80, 0x70, 0x01, 0x01,
(byte) 0x91, 0x03, 0x01, 0x04, 0x64, (byte) 0xFC, 0x07};
BitStream b = BitStream.wrap(bytes);
Assert.assertEquals("JM", b.readString(2)); // signature
Assert.assertEquals(0x00800010, b.readUnsigned(Integer.SIZE)); // flags
Assert.assertEquals(101, b.readUnsigned(8)); // version
b.skip(2); // unknown
Assert.assertEquals(0, b.readUnsigned(3)); // location
Assert.assertEquals(0, b.readUnsigned(4)); // body location
Assert.assertEquals(2, b.readUnsigned(4)); // grid x
Assert.assertEquals(0, b.readUnsigned(4)); // grid y
Assert.assertEquals(1, b.readUnsigned(3)); // store location
Assert.assertEquals("hbw ", b.readString(4)); // code
Assert.assertEquals(0, b.readUnsigned(3)); // sockets filled
Assert.assertEquals(0x2555A813, b.readUnsigned(Integer.SIZE)); // id
Assert.assertEquals(6, b.readUnsigned(7)); // ilvl
Assert.assertEquals(4, b.readUnsigned(4)); // quality
Assert.assertEquals(false, b.readBoolean()); // picture id
Assert.assertEquals(false, b.readBoolean()); // class only
Assert.assertEquals(0, b.readUnsigned(11)); // magic prefix
Assert.assertEquals(737, b.readUnsigned(11)); // magic suffix
b.skip(1); // unknown
Assert.assertEquals(32, b.readUnsigned(8)); // max durability
Assert.assertEquals(32, b.readUnsigned(9)); // durability
Assert.assertEquals(57, b.readUnsigned(9)); // poisonmindam
Assert.assertEquals(0, b.readUnsigned(0)); // poisonmindam param bits
Assert.assertEquals(8, b.readUnsigned(10)); // poisonmindam value
Assert.assertEquals(0, b.readUnsigned(0)); // poisonmaxdam param bits
Assert.assertEquals(8, b.readUnsigned(10)); // poisonmaxdam value
Assert.assertEquals(0, b.readUnsigned(0)); // poisonlength param bits
Assert.assertEquals(50, b.readUnsigned(9)); // poisonlength value
Assert.assertEquals(0x1ff, b.readUnsigned(9)); // stat list finished
Assert.assertEquals(5, b.bitsAvailable()); // tail end of stream
}
@Test(expected = IllegalArgumentException.class)
public void read_u64_throws_IllegalArgumentException() {
BitStream b = BitStream.emptyBitStream();
b.readUnsigned(64);
}
@Test
public void read_raw_64_bits() {
BitStream b = BitStream.wrap(new byte[]{
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
Assert.assertEquals(0xEFCDAB89_67452301L, b.readRaw(Long.SIZE));
}
@Test
public void read_raw_64_bits_partial_byte_order() {
BitStream b = BitStream.wrap(new byte[]{
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
Assert.assertEquals(0x0000AB89_67452301L, b.readRaw(48));
Assert.assertEquals(0x00000000_0000EFCDL, b.readRaw(16));
}
@Test
public void read_raw_64_bits_partial_bit_order() {
BitStream b = BitStream.wrap(new byte[]{
0x01, 0x23, 0x45, 0x67, (byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
Assert.assertEquals(0x000DAB89_67452301L, b.readRaw(52));
Assert.assertEquals(0x00000000_00000EFCL, b.readRaw(12));
}
@Test
public void read_npc_data() {
BitStream b = BitStream.wrap(new byte[] {
0x01, 0x77,
0x34, 0x00,
(byte) 0xAC, (byte) 0xAE, (byte) 0xA5, (byte) 0x89, 0x02, 0x00, 0x00, 0x00,
(byte) 0xAC, (byte) 0xBE, (byte) 0xA4, (byte) 0x89, 0x02, 0x00, 0x00, 0x00,
(byte) 0xAE, (byte) 0xAE, (byte) 0xA4, (byte) 0xC9, 0x02, 0x00, 0x00, 0x00,
(byte) 0xFA, (byte) 0x7B, (byte) 0xE7, (byte) 0x18, 0x00, 0x00, 0x00, 0x00,
(byte) 0xDA, (byte) 0x79, (byte) 0xC7, (byte) 0x18, 0x00, 0x00, 0x00, 0x00,
(byte) 0x10, (byte) 0x51, (byte) 0xE3, (byte) 0x18, 0x00, 0x00, 0x00, 0x00});
Assert.assertArrayEquals(new byte[] {0x01, 0x77}, b.read(2)); // signature
Assert.assertEquals(52, b.readUnsigned(16)); // size
Assert.assertEquals(0x00000002_89A5AEACL, b.readRaw(64));
Assert.assertEquals(0x00000002_89A4BEACL, b.readRaw(64));
Assert.assertEquals(0x00000002_C9A4AEAEL, b.readRaw(64));
Assert.assertEquals(0x00000000_18E77BFAL, b.readRaw(64));
Assert.assertEquals(0x00000000_18C779DAL, b.readRaw(64));
Assert.assertEquals(0x00000000_18E35110L, b.readRaw(64));
}
@Test
public void read_dcc_stream_sizes() {
// TODO...
}
@Test
public void read_byte_array() {
BitStream b = BitStream.wrap(new byte[]{
0x01, 0x77,
0x34, 0x00,
(byte) 0xAC, (byte) 0xAE, (byte) 0xA5, (byte) 0x89, 0x02, 0x00, 0x00, 0x00,
(byte) 0xAC, (byte) 0xBE, (byte) 0xA4, (byte) 0x89, 0x02, 0x00, 0x00, 0x00,
(byte) 0xAE, (byte) 0xAE, (byte) 0xA4, (byte) 0xC9, 0x02, 0x00, 0x00, 0x00,
(byte) 0xFA, (byte) 0x7B, (byte) 0xE7, (byte) 0x18, 0x00, 0x00, 0x00, 0x00,
(byte) 0xDA, (byte) 0x79, (byte) 0xC7, (byte) 0x18, 0x00, 0x00, 0x00, 0x00,
(byte) 0x10, (byte) 0x51, (byte) 0xE3, (byte) 0x18, 0x00, 0x00, 0x00, 0x00});
byte[] expected = new byte[] {0x01, 0x77, 0x34, 0x00};
byte[] actual = b.read(expected.length);
Assert.assertArrayEquals(expected, actual);
}
@Test
public void read_byte_array_when_unaligned() {
BitStream b = BitStream.wrap(new byte[]{
0x01, 0x77,
0x34, 0x00,
(byte) 0xAC, (byte) 0xAE, (byte) 0xA5, (byte) 0x89, 0x02, 0x00, 0x00, 0x00,
(byte) 0xAC, (byte) 0xBE, (byte) 0xA4, (byte) 0x89, 0x02, 0x00, 0x00, 0x00,
(byte) 0xAE, (byte) 0xAE, (byte) 0xA4, (byte) 0xC9, 0x02, 0x00, 0x00, 0x00,
(byte) 0xFA, (byte) 0x7B, (byte) 0xE7, (byte) 0x18, 0x00, 0x00, 0x00, 0x00,
(byte) 0xDA, (byte) 0x79, (byte) 0xC7, (byte) 0x18, 0x00, 0x00, 0x00, 0x00,
(byte) 0x10, (byte) 0x51, (byte) 0xE3, (byte) 0x18, 0x00, 0x00, 0x00, 0x00});
b.skip(1);
byte[] expected = new byte[] {0x77, 0x34, 0x00, (byte) 0xAC};
byte[] actual = b.read(expected.length);
Assert.assertArrayEquals(expected, actual);
}
@Test
public void read_u63() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80});
Assert.assertEquals(0, b.readU63(63));
}
@Test
public void read_s64() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
(byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x80});
Assert.assertEquals(Long.MIN_VALUE, b.readSigned(Long.SIZE));
}
@Test
public void bits_read_increments() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67,
(byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
Assert.assertEquals(0, b.bitsRead());
b.readUnsigned(4);
Assert.assertEquals(4, b.bitsRead());
b.readUnsigned(8);
Assert.assertEquals(12, b.bitsRead());
b.readUnsigned(16);
Assert.assertEquals(28, b.bitsRead());
}
@Test
public void read_slice_seeks() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67,
(byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
b.readSlice(31);
Assert.assertEquals(31, b.bitsRead());
}
@Test(expected = BitStream.EndOfStream.class)
public void read_slice_throws_EndOfStream() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0x01, (byte) 0x23, (byte) 0x45, (byte) 0x67,
(byte) 0x89, (byte) 0xAB, (byte) 0xCD, (byte) 0xEF});
BitStream slice = b.readSlice(31);
slice.readUnsigned(32);
}
@Test
public void read_slice_align_to_align() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0xEF, (byte) 0xBE, (byte) 0xAD, (byte) 0xDE,
(byte) 0x00, (byte) 0xAD, (byte) 0xBB, (byte) 0xDA});
BitStream slice = b.readSlice(32);
Assert.assertEquals(0, slice.cache());
Assert.assertEquals(0, slice.bitsCached());
Assert.assertEquals(0, b.cache());
Assert.assertEquals(0, b.bitsCached());
Assert.assertEquals(0xDEADBEEFL, slice.readUnsigned(32));
Assert.assertEquals(0xDABBAD00L, b.readUnsigned(32));
}
@Test
public void read_slice_align_to_unalign() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0xEF, (byte) 0xBE, (byte) 0xAD, (byte) 0xDE,
(byte) 0x00, (byte) 0xAD, (byte) 0xBB, (byte) 0xDA});
BitStream slice = b.readSlice(28);
Assert.assertEquals(0, slice.cache());
Assert.assertEquals(0, slice.bitsCached());
Assert.assertEquals(0xD, b.cache());
Assert.assertEquals(4, b.bitsCached());
Assert.assertEquals(0xDABBAD00DL, b.readUnsigned(36));
Assert.assertEquals(0xEADBEEFL, slice.readUnsigned(28));
}
@Test
public void read_slice_unalign_to_align() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0xEF, (byte) 0xBE, (byte) 0xAD, (byte) 0xDE,
(byte) 0x00, (byte) 0xAD, (byte) 0xBB, (byte) 0xDA});
BitStream slice = b.skip(4).readSlice(28);
Assert.assertEquals(0xE, slice.cache());
Assert.assertEquals(4, slice.bitsCached());
Assert.assertEquals(0, b.cache());
Assert.assertEquals(0, b.bitsCached());
Assert.assertEquals(0xDABBAD00L, b.readUnsigned(32));
Assert.assertEquals(0xDEADBEEL, slice.readUnsigned(28));
}
@Test
public void read_slice_unalign_to_unalign() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0xEF, (byte) 0xBE, (byte) 0xAD, (byte) 0xDE,
(byte) 0x00, (byte) 0xAD, (byte) 0xBB, (byte) 0xDA});
BitStream slice = b.skip(4).readSlice(32);
Assert.assertEquals(0xE, slice.cache());
Assert.assertEquals(4, slice.bitsCached());
Assert.assertEquals(0, b.cache());
Assert.assertEquals(4, b.bitsCached());
Assert.assertEquals(0xDABBAD0L, b.readUnsigned(28));
Assert.assertEquals(0x0DEADBEEL, slice.readUnsigned(32));
}
@Test
public void read_slice_unalign_to_unalign_complex_single_byte() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0xEF, (byte) 0xBE, (byte) 0xAD, (byte) 0xDE,
(byte) 0x00, (byte) 0xAD, (byte) 0xBB, (byte) 0xDA});
BitStream slice = b.skip(3).readSlice(4);
Assert.assertEquals(0b11101, slice.cache());
Assert.assertEquals(5, slice.bitsCached());
Assert.assertEquals(0b1, b.cache());
Assert.assertEquals(1, b.bitsCached());
}
@Test
public void read_slice_unalign_to_unalign_complex_multi_byte() {
BitStream b = BitStream.wrap(new byte[]{
(byte) 0xEF, (byte) 0xBE, (byte) 0xAD, (byte) 0xDE,
(byte) 0x00, (byte) 0xAD, (byte) 0xBB, (byte) 0xDA});
BitStream slice = b.skip(3).readSlice(22);
Assert.assertEquals(0b11101, slice.cache());
Assert.assertEquals(5, slice.bitsCached());
Assert.assertEquals(0b1101111, b.cache());
Assert.assertEquals(7, b.bitsCached());
}
}