blob: 32e9d0b406a8abebdc124e2a75d88b2791c05d77 [file] [log] [blame]
/*
* Copyright 2016-present Open Networking Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onlab.util;
import com.google.common.base.Objects;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.lang.String.format;
import static org.apache.commons.lang3.ArrayUtils.reverse;
/**
* Immutable sequence of bytes, assumed to represent a value in {@link
* ByteOrder#BIG_ENDIAN BIG_ENDIAN} order.
* <p>
* Sequences can be created copying from an already existing representation of a
* sequence of bytes, such as {@link ByteBuffer} or {@code byte[]}; or by
* copying bytes from a primitive data type, such as {@code long}, {@code int}
* or {@code short}. In the first case, bytes are assumed to be already given in
* big-endian order, while in the second case big-endianness is enforced by this
* class.
*/
public final class ImmutableByteSequence {
private enum BitwiseOp {
AND,
OR,
XOR
}
/*
Actual bytes are backed by a byte buffer.
The order of a newly-created byte buffer is always BIG_ENDIAN.
*/
private ByteBuffer value;
private boolean isAscii = false;
/**
* Private constructor. Creates a new byte sequence object backed by the
* passed ByteBuffer.
*
* @param value a byte buffer
*/
private ImmutableByteSequence(ByteBuffer value) {
this.value = value;
// Rewind buffer so it's ready to be read.
// No write operation should be performed on it from now on.
this.value.rewind();
}
private ImmutableByteSequence(ByteBuffer value, boolean isAscii) {
this(value);
this.isAscii = isAscii;
}
/**
* Creates a new immutable byte sequence with the same content and order of
* the passed byte array.
*
* @param original a byte array value
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence copyFrom(byte[] original) {
checkArgument(original != null && original.length > 0,
"Cannot copy from an empty or null array");
return new ImmutableByteSequence(
ByteBuffer.allocate(original.length).put(original));
}
/**
* Creates a new immutable byte sequence with the same content and order of
* the passed byte array, from/to the given indexes (inclusive).
*
* @param original a byte array value
* @param fromIdx starting index
* @param toIdx ending index
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence copyFrom(byte[] original, int fromIdx, int toIdx) {
checkArgument(original != null && original.length > 0,
"Cannot copy from an empty or null array");
checkArgument(toIdx >= fromIdx && toIdx < original.length, "invalid indexes");
ByteBuffer buffer = ByteBuffer.allocate((toIdx - fromIdx) + 1);
for (int i = fromIdx; i <= toIdx; i++) {
buffer.put(original[i]);
}
return new ImmutableByteSequence(buffer);
}
/**
* Creates a new immutable byte sequence copying bytes from the given
* ByteBuffer {@link ByteBuffer}. If the byte buffer order is not big-endian
* bytes will be copied in reverse order.
*
* @param original a byte buffer
* @return a new byte buffer object
*/
public static ImmutableByteSequence copyFrom(ByteBuffer original) {
checkArgument(original != null && original.capacity() > 0,
"Cannot copy from an empty or null byte buffer");
byte[] bytes = new byte[original.capacity()];
// copy bytes from original buffer
original.rewind();
original.get(bytes);
if (original.order() == ByteOrder.LITTLE_ENDIAN) {
// FIXME: this can be improved, e.g. read bytes in reverse order from original
reverse(bytes);
}
return new ImmutableByteSequence(ByteBuffer.wrap(bytes));
}
/**
* Creates a new immutable byte sequence from the given string.
*
* @param original a string
* @return a new byte buffer object
*/
public static ImmutableByteSequence copyFrom(String original) {
checkArgument(original != null && original.length() > 0,
"Cannot copy from an empty or null string");
return new ImmutableByteSequence(ByteBuffer.allocate(original.length())
.put(original.getBytes()),
true);
}
/**
* Creates a new byte sequence of 8 bytes containing the given long value.
*
* @param original a long value
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence copyFrom(long original) {
return new ImmutableByteSequence(
ByteBuffer.allocate(Long.BYTES).putLong(original));
}
/**
* Creates a new byte sequence of 4 bytes containing the given int value.
*
* @param original an int value
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence copyFrom(int original) {
return new ImmutableByteSequence(
ByteBuffer.allocate(Integer.BYTES).putInt(original));
}
/**
* Creates a new byte sequence of 2 bytes containing the given short value.
*
* @param original a short value
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence copyFrom(short original) {
return new ImmutableByteSequence(
ByteBuffer.allocate(Short.BYTES).putShort(original));
}
/**
* Creates a new byte sequence of 1 byte containing the given value.
*
* @param original a byte value
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence copyFrom(byte original) {
return new ImmutableByteSequence(
ByteBuffer.allocate(Byte.BYTES).put(original));
}
/**
* Creates a new immutable byte sequence while trimming or expanding the
* content of the given byte buffer to fit the given bit-width. Calling this
* method has the same behavior as
* {@code ImmutableByteSequence.copyFrom(original).fit(bitWidth)}.
*
* @param original a byte buffer value
* @param bitWidth a non-zero positive integer
* @return a new immutable byte sequence
* @throws ByteSequenceTrimException if the byte buffer cannot be fitted
*/
public static ImmutableByteSequence copyAndFit(ByteBuffer original, int bitWidth)
throws ByteSequenceTrimException {
checkArgument(original != null && original.capacity() > 0,
"Cannot copy from an empty or null byte buffer");
checkArgument(bitWidth > 0,
"bit-width must be a non-zero positive integer");
if (original.order() == ByteOrder.LITTLE_ENDIAN) {
// FIXME: this can be improved, e.g. read bytes in reverse order from original
byte[] newBytes = new byte[original.capacity()];
original.get(newBytes);
reverse(newBytes);
return internalCopyAndFit(ByteBuffer.wrap(newBytes), bitWidth);
} else {
return internalCopyAndFit(original.duplicate(), bitWidth);
}
}
private static ImmutableByteSequence internalCopyAndFit(ByteBuffer byteBuf, int bitWidth)
throws ByteSequenceTrimException {
final int byteWidth = (bitWidth + 7) / 8;
final ByteBuffer newByteBuffer = ByteBuffer.allocate(byteWidth);
byteBuf.rewind();
if (byteWidth >= byteBuf.capacity()) {
newByteBuffer.position(byteWidth - byteBuf.capacity());
newByteBuffer.put(byteBuf);
} else {
for (int i = 0; i < byteBuf.capacity() - byteWidth; i++) {
if (byteBuf.get(i) != 0) {
throw new ByteSequenceTrimException(byteBuf, bitWidth);
}
}
newByteBuffer.put(byteBuf.position(byteBuf.capacity() - byteWidth));
}
return new ImmutableByteSequence(newByteBuffer);
}
/**
* Creates a new byte sequence of the given size where all bits are 0.
*
* @param size number of bytes
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence ofZeros(int size) {
// array is initialized to all 0's by default
return new ImmutableByteSequence(ByteBuffer.wrap(new byte[size]));
}
/**
* Creates a new byte sequence of the given size where all bits are 1.
*
* @param size number of bytes
* @return a new immutable byte sequence
*/
public static ImmutableByteSequence ofOnes(int size) {
byte[] bytes = new byte[size];
Arrays.fill(bytes, (byte) 0xFF);
return new ImmutableByteSequence(ByteBuffer.wrap(bytes));
}
/**
* Creates a new byte sequence that is prefixed with specified number of
* zeros if val = 0 or ones if val = 0xff.
*
* @param size number of total bytes
* @param prefixBits number of bits in prefix
* @param val 0 for prefix of zeros; 0xff for prefix of ones
* @return new immutable byte sequence
*/
static ImmutableByteSequence prefix(int size, long prefixBits, byte val) {
checkArgument(val == 0 || val == (byte) 0xff, "Val must be 0 or 0xff");
byte[] bytes = new byte[size];
int prefixBytes = (int) (prefixBits / Byte.SIZE);
Arrays.fill(bytes, 0, prefixBytes, val);
Arrays.fill(bytes, prefixBytes, bytes.length, (byte) ~val);
int partialBits = (int) (prefixBits % Byte.SIZE);
if (partialBits != 0) {
bytes[prefixBytes] = val == 0 ?
(byte) (0xff >> partialBits) : (byte) (0xff << Byte.SIZE - partialBits);
}
return new ImmutableByteSequence(ByteBuffer.wrap(bytes));
}
/**
* Creates a new byte sequence that is prefixed with specified number of
* zeros.
*
* @param size number of total bytes
* @param prefixBits number of bits in prefix
* @return new immutable byte sequence
*/
public static ImmutableByteSequence prefixZeros(int size, long prefixBits) {
return prefix(size, prefixBits, (byte) 0);
}
/**
* Creates a new byte sequence that is prefixed with specified number of
* ones.
*
* @param size number of total bytes
* @param prefixBits number of bits in prefix
* @return new immutable byte sequence
*/
public static ImmutableByteSequence prefixOnes(int size, long prefixBits) {
return prefix(size, prefixBits, (byte) 0xff);
}
/**
* Returns a view of this sequence as a read-only {@link ByteBuffer}.
* <p>
* The returned buffer will have position 0, while limit and capacity will
* be set to this sequence {@link #size()}. The buffer order will be
* big-endian.
*
* @return a read-only byte buffer
*/
public ByteBuffer asReadOnlyBuffer() {
// position, limit and capacity set rewind at constructor
return value.asReadOnlyBuffer();
}
/**
* Gets the number of bytes in this sequence.
*
* @return an integer value
*/
public int size() {
return this.value.capacity();
}
/**
* Creates a new byte array view of this sequence.
*
* @return a new byte array
*/
public byte[] asArray() {
ByteBuffer bb = asReadOnlyBuffer();
byte[] bytes = new byte[size()];
bb.get(bytes);
return bytes;
}
private ImmutableByteSequence doBitwiseOp(ImmutableByteSequence other, BitwiseOp op) {
checkArgument(other != null && this.size() == other.size(),
"Other sequence must be non null and with same size as this");
byte[] newBytes = new byte[this.size()];
byte[] thisBytes = this.asArray();
byte[] otherBytes = other.asArray();
for (int i = 0; i < this.size(); i++) {
switch (op) {
case AND:
newBytes[i] = (byte) (thisBytes[i] & otherBytes[i]);
break;
case OR:
newBytes[i] = (byte) (thisBytes[i] | otherBytes[i]);
break;
case XOR:
newBytes[i] = (byte) (thisBytes[i] ^ otherBytes[i]);
break;
default:
throw new IllegalArgumentException(
"Unknown bitwise operator " + op.name());
}
}
return ImmutableByteSequence.copyFrom(newBytes);
}
/**
* Returns a new byte sequence corresponding to the result of a bitwise AND
* operation between this sequence and the given other, i.e. {@code this &
* other}.
*
* @param other other byte sequence
* @return new byte sequence
* @throws IllegalArgumentException if other sequence is null or its size is
* different than this sequence size
*/
public ImmutableByteSequence bitwiseAnd(ImmutableByteSequence other) {
return doBitwiseOp(other, BitwiseOp.AND);
}
/**
* Returns a new byte sequence corresponding to the result of a bitwise OR
* operation between this sequence and the given other, i.e. {@code this |
* other}.
*
* @param other other byte sequence
* @return new byte sequence
* @throws IllegalArgumentException if other sequence is null or its size is
* different than this sequence size
*/
public ImmutableByteSequence bitwiseOr(ImmutableByteSequence other) {
return doBitwiseOp(other, BitwiseOp.OR);
}
/**
* Returns a new byte sequence corresponding to the result of a bitwise XOR
* operation between this sequence and the given other, i.e. {@code this ^
* other}.
*
* @param other other byte sequence
* @return new byte sequence
* @throws IllegalArgumentException if other sequence is null or its size is
* different than this sequence size
*/
public ImmutableByteSequence bitwiseXor(ImmutableByteSequence other) {
return doBitwiseOp(other, BitwiseOp.XOR);
}
@Override
public int hashCode() {
return value.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final ImmutableByteSequence other = (ImmutableByteSequence) obj;
return Objects.equal(this.value, other.value);
}
/**
* Returns the index of the most significant bit (MSB), assuming a bit
* numbering scheme of type "LSB 0", i.e. the bit numbering starts at zero
* for the least significant bit (LSB). The MSB index of a byte sequence of
* zeros will be -1.
* <p>
* As an example, the following conditions always hold true: {@code
* ImmutableByteSequence.copyFrom(0).msbIndex() == -1
* ImmutableByteSequence.copyFrom(1).msbIndex() == 0
* ImmutableByteSequence.copyFrom(2).msbIndex() == 1
* ImmutableByteSequence.copyFrom(3).msbIndex() == 1
* ImmutableByteSequence.copyFrom(4).msbIndex() == 2
* ImmutableByteSequence.copyFrom(512).msbIndex() == 9 }
*
* @return index of the MSB, -1 if the sequence has all bytes set to 0
*/
public int msbIndex() {
int index = (size() * 8) - 1;
byteLoop:
for (int i = 0; i < size(); i++) {
byte b = value.get(i);
if (b != 0) {
for (int j = 7; j >= 0; j--) {
byte mask = (byte) ((1 << j) - 1);
if ((b & ~mask) != 0) {
break byteLoop;
}
index--;
}
}
index -= 8;
}
return index;
}
/**
* Returns the ASCII representation of the byte sequence if the content can
* be interpreted as an ASCII string, otherwise returns the hexadecimal
* representation of this byte sequence, e.g.0xbeef. The length of the
* returned string is not representative of the length of the byte sequence,
* as all padding zeros are removed.
*
* @return hexadecimal representation
*/
@Override
public String toString() {
if (this.isAscii()) {
return new String(value.array());
} else {
return "0x" + HexString
.toHexString(value.array(), "")
// Remove leading zeros, but leave one if string is all zeros.
.replaceFirst("^0+(?!$)", "");
}
}
/**
* Checks if the content can be interpreted as an ASCII printable string.
*
* @return True if the content can be interpreted as an ASCII printable
* string, false otherwise
*/
public boolean isAscii() {
return isAscii;
}
/**
* Trims or expands a copy of this byte sequence so to fit the given
* bit-width. When trimming, the operations is deemed to be safe only if the
* trimmed bits are zero, i.e. it is safe to trim only when {@code bitWidth
* > msbIndex()}, otherwise an exception will be thrown. When expanding, the
* sequence will be padded with zeros. The returned byte sequence will have
* minimum size to contain the given bit-width.
*
* @param bitWidth a non-zero positive integer
* @return a new byte sequence
* @throws ByteSequenceTrimException if the byte sequence cannot be fitted
*/
public ImmutableByteSequence fit(int bitWidth) throws ByteSequenceTrimException {
return doFit(this, bitWidth);
}
private static ImmutableByteSequence doFit(ImmutableByteSequence original,
int bitWidth)
throws ByteSequenceTrimException {
checkNotNull(original, "byte sequence cannot be null");
checkArgument(bitWidth > 0, "bit-width must be a non-zero positive integer");
int newByteWidth = (bitWidth + 7) / 8;
if (bitWidth == original.size() * 8) {
// No need to fit.
return original;
}
ByteBuffer newBuffer = ByteBuffer.allocate(newByteWidth);
if (newByteWidth > original.size()) {
// Pad extra bytes with 0's.
int numPadBytes = newByteWidth - original.size();
for (int i = 0; i < numPadBytes; i++) {
newBuffer.put((byte) 0x00);
}
newBuffer.put(original.asReadOnlyBuffer());
} else {
// Trim sequence.
if (bitWidth > original.msbIndex()) {
int diff = original.size() - newByteWidth;
ByteBuffer originalBuffer = original.asReadOnlyBuffer();
for (int i = diff; i < original.size(); i++) {
newBuffer.put(originalBuffer.get(i));
}
} else {
throw new ByteSequenceTrimException(original, bitWidth);
}
}
return new ImmutableByteSequence(newBuffer);
}
/**
* Signals a trim exception during byte sequence creation.
*/
public static class ByteSequenceTrimException extends Exception {
ByteSequenceTrimException(ImmutableByteSequence original, int bitWidth) {
super(format("cannot trim %s into a %d bits long value",
original, bitWidth));
}
ByteSequenceTrimException(ByteBuffer original, int bitWidth) {
super(format("cannot trim %s (ByteBuffer) into a %d bits long value",
original, bitWidth));
}
}
}