diff --git a/core/src/com/riiablo/util/BitStream.java b/core/src/com/riiablo/util/BitStream.java new file mode 100644 index 00000000..5f20dbe2 --- /dev/null +++ b/core/src/com/riiablo/util/BitStream.java @@ -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. + *

+ * 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). + *

+ *

{@code bits} should be between [0, {@value #MAX_CACHE_SIZE}]. + *

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 n 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 n 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 n 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 n 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 n characters from the underlying byte stream, assuming each + * character contains {@code bitsPerChar} bits. This function is guaranteed + * to read {@code len} characters. + *

+ * Note: This function does not support multi-byte character encoding, + * therefore

bitsPerChar <= {@value Byte#SIZE}
+ * + * @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 n 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. + *

+ * Note: This function does not support multi-byte character encoding, + * therefore

bitsPerChar <= {@value Byte#SIZE}
+ * + * @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 n bits, up to + * {@value #MAX_SAFE_CACHE_SIZE} bits due to overflow. + * + * @throws EndOfStream if the underlying byte stream did not contain at least + * n 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 n bits from {@link #cache}, consuming the next byte in the + * underlying byte stream. + *

+ * This function asserts that {@link #cache} would have overflowed if + * n 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 n bits from {@link #cache}. + *

+ * This function asserts {@link #cache} contains at least n 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!"); + } + } +} diff --git a/core/test/com/riiablo/util/BitStreamTest.java b/core/test/com/riiablo/util/BitStreamTest.java new file mode 100644 index 00000000..bd250608 --- /dev/null +++ b/core/test/com/riiablo/util/BitStreamTest.java @@ -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()); + } +} \ No newline at end of file