mirror of
https://github.com/collinsmith/riiablo.git
synced 2025-02-01 10:24:30 +07:00
Created more robust BitStream implementation
Pending validation, will deprecate and replace com.riiablo.codec.util.BitStream
This commit is contained in:
parent
84748c56a3
commit
cfb2850d92
553
core/src/com/riiablo/util/BitStream.java
Normal file
553
core/src/com/riiablo/util/BitStream.java
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
296
core/test/com/riiablo/util/BitStreamTest.java
Normal file
296
core/test/com/riiablo/util/BitStreamTest.java
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user