io.snice.buffer.impl.DefaultImmutableBuffer Maven / Gradle / Ivy
package io.snice.buffer.impl;
import com.google.polo.pairing.HexDump;
import io.snice.buffer.Buffer;
import io.snice.buffer.ByteNotFoundException;
import io.snice.buffer.ReadableBuffer;
import io.snice.buffer.WritableBuffer;
import io.snice.net.IPv4;
import io.snice.preconditions.PreConditions;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import static io.snice.preconditions.PreConditions.assertArgument;
import static io.snice.preconditions.PreConditions.assertArray;
/**
* The default implementation of our immutable buffer and is truly 100% immutable.
*/
public class DefaultImmutableBuffer implements Buffer {
public static Buffer of(final byte[] buffer) {
assertArray(buffer);
if (buffer.length == 0) {
return EmptyBuffer.EMPTY;
}
return new DefaultImmutableBuffer(buffer, 0, buffer.length);
}
public static Buffer of(final byte[] buffer, final int offset, final int length) {
assertArray(buffer, offset, length);
if (buffer.length == 0) {
return EmptyBuffer.EMPTY;
}
return new DefaultImmutableBuffer(buffer, offset, offset + length);
}
/**
* The actual buffer
*/
private final byte[] buffer;
/**
* Used when slicing out portions of the buffer.
* The lower boundary is inclusive.
*/
private final int lowerBoundary;
/**
* The upper boundary of the "window" of data this buffer is allowed to "see".
*/
private final int upperBoundary;
private DefaultImmutableBuffer(final byte[] buffer, final int lowerBoundary, final int upperBoundary) {
this.buffer = buffer;
this.lowerBoundary = lowerBoundary;
this.upperBoundary = upperBoundary;
}
@Override
public int countWhiteSpace(final int startIndex) {
checkIndex(lowerBoundary + startIndex);
int count = 0;
for (int i = lowerBoundary + startIndex; i < upperBoundary; ++i) {
if (buffer[i] != SP && buffer[i] != HTAB) {
break;
}
++count;
}
return count;
}
@Override
public Buffer toBuffer() {
return this;
}
@Override
public byte[] getContent() {
final int capacity = capacity();
final var content = new byte[capacity];
System.arraycopy(buffer, lowerBoundary, content, 0, capacity);
return content;
}
@Override
public ReadableBuffer toReadableBuffer() {
return DefaultReadableBuffer.of(this);
}
@Override
public WritableBuffer toWritableBuffer() {
final int capacity = capacity();
final byte[] copy = new byte[capacity];
System.arraycopy(buffer, lowerBoundary, copy, 0, capacity);
return WritableBuffer.of(copy);
}
@Override
public int indexOfSingleCRLF() {
throw new RuntimeException("Not implemented yet");
}
@Override
public Buffer indexOfDoubleCRLF() {
throw new RuntimeException("Not implemented yet");
}
@Override
public boolean isEmpty() {
return capacity() == 0;
}
@Override
public int capacity() {
return this.upperBoundary - this.lowerBoundary;
}
@Override
public int indexdOfSafe(final int maxBytes, final byte... bytes) throws IllegalArgumentException {
throw new RuntimeException("Not implemented yet");
}
/**
* Just because the underlying buffer is of a certain size doesn't mean that all of those
* bytes are available to this particular slice.
*
* @return the total readable bytes, which is the number of bytes in our "window"
*/
private int getReadableBytes() {
return upperBoundary - lowerBoundary;
}
public static boolean hasReadableBytes() {
// when creating this buffer we will check if the passed in
// array is empty and if so, we'll actually return an empty buffer
// instead so this is safe.
return true;
}
@Override
public int indexOf(final int maxBytes, final byte... bytes) throws ByteNotFoundException, IllegalArgumentException {
return indexOf(0, maxBytes, bytes);
}
@Override
public int indexOf(final int startIndex, final int maxBytes, final byte... bytes) throws ByteNotFoundException, IllegalArgumentException {
checkIndex(lowerBoundary + startIndex);
assertArgument(maxBytes > 0, "The max bytes must be at least 1");
assertArgument(bytes.length > 0, "No bytes specified. Not sure what you want me to look for");
final int capacity = capacity();
// remember that the start index is zero based off of the lower boundary.
// however, the getByte method takes this into account, which is why
// we don't do it here.
int index = startIndex;
while ((index < capacity) && (maxBytes > index)) {
if (isByteInArray(getByte(index), bytes)) {
return index;
}
++index;
}
if (index - startIndex >= maxBytes) {
throw new ByteNotFoundException(capacity, bytes);
}
return -1;
}
@Override
public void writeTo(final OutputStream out) throws IOException {
final int length = getReadableBytes();
out.write(buffer, lowerBoundary, length);
}
@Override
public void writeTo(final WritableBuffer out) {
final int length = getReadableBytes();
out.write(buffer, lowerBoundary, length);
}
@Override
public int indexOf(final byte b) throws ByteNotFoundException, IllegalArgumentException {
return indexOf(0, 4096, b);
}
@Override
public int countOccurences(final int startIndex, final int maxBytes, final byte b) throws IllegalArgumentException {
checkIndex(lowerBoundary + startIndex);
final int capacity = capacity();
final int stop = Math.min(lowerBoundary + startIndex + maxBytes, lowerBoundary + capacity);
int count = 0;
for (int i = lowerBoundary + startIndex; i < stop; ++i) {
if (buffer[i] == b) {
++count;
}
}
return count;
}
@Override
public Buffer slice(final int start, final int stop) throws IndexOutOfBoundsException, IllegalArgumentException {
PreConditions.assertArgument(start >= 0, "The start index must be greater than zero");
PreConditions.assertArgument(stop >= start, "The stop index (" + stop + ") must be greater or equal " +
"to the start (" + start + ") index");
if (start == stop) {
return EmptyBuffer.EMPTY;
}
checkIndex(lowerBoundary + start);
checkIndex(lowerBoundary + stop - 1);
final int upperBoundary = lowerBoundary + stop;
return new DefaultImmutableBuffer(buffer, lowerBoundary + start, upperBoundary);
}
@Override
public Buffer slice(final int stop) {
return slice(0, stop);
}
@Override
public Buffer slice() {
return this;
}
@Override
public byte getByte(final int index) throws IndexOutOfBoundsException {
checkIndex(lowerBoundary + index);
return buffer[lowerBoundary + index];
}
@Override
public int getInt(final int index) throws IndexOutOfBoundsException {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 3);
return Buffer.signedInt(buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3]);
}
@Override
public String toIPv4String(final int index) {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 3);
return IPv4.convertToStringIP(buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3]);
}
@Override
public long getLong(final int index) throws IndexOutOfBoundsException {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 7);
return Buffer.signedLong(buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3], buffer[i + 4], buffer[i + 5], buffer[i + 6], buffer[i + 7]);
}
public long getLongFromFiveOctets(int index) throws IndexOutOfBoundsException {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 4);
return Buffer.signedLong(buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3], buffer[i + 4]);
}
@Override
public int getIntFromThreeOctets(final int index) throws IndexOutOfBoundsException {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 2);
return Buffer.signedInt(buffer[i], buffer[i + 1], buffer[i + 2]);
}
@Override
public long getUnsignedInt(final int index) throws IndexOutOfBoundsException {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 3);
return Buffer.unsignedInt(buffer[i], buffer[i + 1], buffer[i + 2], buffer[i + 3]);
}
@Override
public short getShort(final int index) throws IndexOutOfBoundsException {
final int i = lowerBoundary + index;
checkIndex(i);
checkIndex(i + 1);
// big endian
return (short) (buffer[i] << 8 | buffer[i + 1] & 0xFF);
// little endian
// return (short) (this.buffer[i] & 0xFF | this.buffer[i + 1] << 8);
}
@Override
public int getUnsignedShort(final int index) throws IndexOutOfBoundsException {
return getShort(index) & 0xFFFF;
}
@Override
public short getUnsignedByte(final int index) throws IndexOutOfBoundsException {
return (short) (getByte(index) & 0xFF);
}
@Override
public int parseToInt() throws NumberFormatException {
return parseToInt(10);
}
@Override
public boolean endsWith(final byte[] content) throws IllegalArgumentException {
assertArray(content);
assertArgument(content.length > 0, "The byte-array cannot be empty");
if (content.length > capacity()) {
return false;
}
final int length = content.length;
for (int i = 0; i < length; ++i) {
if (content[i] != buffer[upperBoundary - length + i]) {
return false;
}
}
return true;
}
@Override
public boolean endsWith(final byte b) throws IllegalArgumentException {
return buffer[upperBoundary - 1] == b;
}
@Override
public boolean endsWith(final byte b1, final byte b2) throws IllegalArgumentException {
if (capacity() < 2) {
return false;
}
return buffer[upperBoundary - 2] == b1
&& buffer[upperBoundary - 1] == b2;
}
@Override
public boolean endsWith(final byte b1, final byte b2, final byte b3) throws IllegalArgumentException {
if (capacity() < 3) {
return false;
}
return buffer[upperBoundary - 3] == b1
&& buffer[upperBoundary - 2] == b2
&& buffer[upperBoundary - 1] == b3;
}
@Override
public boolean endsWith(final byte b1, final byte b2, final byte b3, final byte b4) throws IllegalArgumentException {
if (capacity() < 4) {
return false;
}
return buffer[upperBoundary - 4] == b1
&& buffer[upperBoundary - 3] == b2
&& buffer[upperBoundary - 2] == b3
&& buffer[upperBoundary - 1] == b4;
}
@Override
public String dumpAsHex() {
return HexDump.dumpHexString(buffer, lowerBoundary, upperBoundary - lowerBoundary);
}
@Override
public String toHexString(boolean prefix) {
return HexDump.toHexString(prefix, buffer, lowerBoundary, upperBoundary - lowerBoundary);
}
@Override
public Buffer clone() {
return this;
}
@Override
public boolean equalsIgnoreCase(final Object other) {
return internalEquals(true, other);
}
@Override
public String toString() {
return toUTF8String();
}
@Override
public String toUTF8String() {
try {
final int length = getReadableBytes();
return new String(buffer, lowerBoundary, length, "UTF-8");
} catch (final UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
int result = 1;
for (int i = lowerBoundary; i < upperBoundary; ++i) {
result = 31 * result + buffer[i];
}
return result;
}
@Override
public boolean equals(final Object other) {
return internalEquals(false, other);
}
/**
* Due to the decision to not implement the default buffer vs the readable version using
* inheritance but rather through delegation we end up in some unfortunate situations like
* the one below. Perhaps I should have stuck with inheritance instead even though I do think
* it overall may potentially, possibly maybe be a more robust solution by doing it through
* delegation... but I may be wrong...
*
* @param other
* @return
*/
private static DefaultImmutableBuffer ensureBuffer(final Object other) throws ClassCastException {
// if this is already an immutable buffer, as opposed to a readable etc, then this is cheap.
// if it isn't, for a readable buffer it is cheap as well, for a writable, it will force us
// to make a clone of the underlying byte-array...
final Buffer buffer = (Buffer)other;
return (DefaultImmutableBuffer)buffer.toBuffer();
}
private boolean internalEquals(final boolean ignoreCase, final Object other) {
try {
if (this == other) {
return true;
}
final DefaultImmutableBuffer b = ensureBuffer(other);
if (getReadableBytes() != b.getReadableBytes()) {
return false;
}
final int length = getReadableBytes();
for (int i = 0; i < length; ++i) {
final byte a1 = buffer[lowerBoundary + i];
final byte b1 = b.buffer[b.lowerBoundary + i];
// Do a UTF-8-aware, possibly case-insensitive character match. Only considers
// case of 7-bit ASCII characters 'a'-'z'. In UTF-8, all bytes of multi-byte
// characters have thier most signifcant bit set, so they won't be erroneously
// considered by this algorithm since they won't fall in the range 0x41-0x5a/
// 0x61-0x7a.
// This algorithm won't work with UTF-16, and could misfire on malformed UTF-8,
// e.g. the first byte of a UTF-8 sequence marks the beginning of a multi-byte
// sequence but the second byte does not have the two high-order bits set to 10.
// For 7-bit ascii leters, upper and lower-case only differ by one bit,
// i.e. 'A' is 0x41, and 'a' is 0x61. We need only compare the 5 least
// signifcant bits.
if (a1 != b1) {
if (ignoreCase &&
((a1 >= 'A' && a1 <= 'Z') || (a1 >= 'a' && a1 <= 'z')) &&
((b1 >= 'A' && b1 <= 'Z') || (b1 >= 'a' && b1 <= 'z')) &&
(a1 & 0x1f) == (b1 & 0x1f)) {
continue;
}
return false;
}
}
return true;
} catch (final NullPointerException | ClassCastException e) {
return false;
}
}
/**
* (Copied from the Integer class and slightly altered to read from this
* buffer instead of a String)
*
* Parses the string argument as a signed integer in the radix specified by
* the second argument. The characters in the string must all be digits of
* the specified radix (as determined by whether
* {@link java.lang.Character#digit(char, int)} returns a nonnegative
* value), except that the first character may be an ASCII minus sign
* '-'
('\u002D'
) to indicate a negative
* value. The resulting integer value is returned.
*
* An exception of type NumberFormatException
is thrown if any
* of the following situations occurs:
*
* - The first argument is
null
or is a string of length
* zero.
* - The radix is either smaller than
* {@link java.lang.Character#MIN_RADIX} or larger than
* {@link java.lang.Character#MAX_RADIX}.
*
- Any character of the string is not a digit of the specified radix,
* except that the first character may be a minus sign
'-'
(
* '\u002D'
) provided that the string is longer than length
* 1.
* - The value represented by the string is not a value of type
*
int
.
*
*
* Examples:
*
*
* parseInt("0", 10) returns 0
* parseInt("473", 10) returns 473
* parseInt("-0", 10) returns 0
* parseInt("-FF", 16) returns -255
* parseInt("1100110", 2) returns 102
* parseInt("2147483647", 10) returns 2147483647
* parseInt("-2147483648", 10) returns -2147483648
* parseInt("2147483648", 10) throws a NumberFormatException
* parseInt("99", 8) throws a NumberFormatException
* parseInt("Kona", 10) throws a NumberFormatException
* parseInt("Kona", 27) returns 411787
*
*
*
*
* @param radix
* the radix to be used while parsing s
.
* @return the integer represented by the string argument in the specified
* radix.
* @exception NumberFormatException
* if the String
does not contain a parsable
* int
.
*/
@Override
public final int parseToInt(final int radix) throws NumberFormatException {
if (isEmpty()) {
throw new NumberFormatException("Buffer is empty, cannot convert it to an integer");
}
if (radix < Character.MIN_RADIX) {
throw new NumberFormatException("radix " + radix + " less than Character.MIN_RADIX");
}
if (radix > Character.MAX_RADIX) {
throw new NumberFormatException("radix " + radix + " greater than Character.MAX_RADIX");
}
int result = 0;
boolean negative = false;
int i = 0;
final int max = getReadableBytes();
final int limit;
final int multmin;
int digit;
if (max > 0) {
if (getByte(i) == (byte) '-') {
negative = true;
limit = Integer.MIN_VALUE;
i++;
} else {
limit = -Integer.MAX_VALUE;
}
multmin = limit / radix;
if (i < max) {
digit = Character.digit((char) getByte(i++), radix);
if (digit < 0) {
throw new NumberFormatException("For input string: \"" + this + "\"");
} else {
result = -digit;
}
}
while (i < max) {
// Accumulating negatively avoids surprises near MAX_VALUE
digit = Character.digit((char) getByte(i++), radix);
if (digit < 0) {
throw new NumberFormatException("For input string: \"" + this + "\"");
}
if (result < multmin) {
throw new NumberFormatException("For input string: \"" + this + "\"");
}
result *= radix;
if (result < limit + digit) {
throw new NumberFormatException("For input string: \"" + this + "\"");
}
result -= digit;
}
} else {
throw new NumberFormatException("For input string: \"" + this + "\"");
}
if (negative) {
if (i > 1) {
return result;
} else { /* Only got "-" */
throw new NumberFormatException("For input string: \"" + this + "\"");
}
} else {
return -result;
}
}
protected static boolean isByteInArray(final byte b, final byte[] bytes) {
for (final byte x : bytes) {
if (x == b) {
return true;
}
}
return false;
}
/**
* Convenience method for checking if we can read at the index
*
* @param index
* @throws IndexOutOfBoundsException
*/
protected void checkIndex(final int index) throws IndexOutOfBoundsException {
final int upperBoundary = lowerBoundary + capacity();
if (index >= upperBoundary) {
throw new IndexOutOfBoundsException("Trying to access index " + index + " in a buffer whose upper bound is (exclusive) " + upperBoundary);
}
}
}