/**
* Copyright (c) Codice Foundation
* <p>
* This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or any later version.
* <p>
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details. A copy of the GNU Lesser General Public License
* is distributed along with this program and can be found at
* <http://www.gnu.org/licenses/lgpl.html>.
*/
package org.codice.ddf.libs.klv.data;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import com.google.common.base.Preconditions;
/**
* A public domain class for working with Key-Length-Value (KLV)
* byte-packing and unpacking. Supports 1-, 2-, 4-byte, and BER-encoded
* length fields and 1-, 2-, 4-, and 16-byte key fields.
* <p>
* KLV has been used for years as a repeatable, no-guesswork technique
* for byte-packing data, that is, sending data in a binary format
* with two bytes for this integer, four bytes for that float, and
* so forth. KLV is used in broadcast television and is defined in
* SMPTE 336M-2001, but it also greatly eases the burden of non-TV-related
* applications for an easy, interchangeable binary format.
* <p>
* The underlying byte array is always king. If you change the key
* length or the length encoding, you only change how the underlying
* byte array is interpreted on subsequent calls.
* <p>
* Everything in KLV is Big Endian.
* <p>
* All <tt>getValue...</tt> methods will return up to the number
* of bytes specified in the length fields unless there are fewer
* bytes actually given. In that case, the number of bytes given
* will be used. This is to make the code more robust for reading
* corrupted data.
*
* @author Robert Harder
* @author rharder # users.sourceforge.net
* @version 0.3
*/
public class Klv {
/**
* The encoding style for the length field can be fixed at
* one byte, two bytes, four bytes, or variable with
* Basic Encoding Rules (BER).
*/
public enum LengthEncoding {
OneByte(1),
TwoBytes(2),
FourBytes(4),
BER(5); // Max bytes a BER field could take up
private int value;
LengthEncoding(int value) {
this.value = value;
}
/**
* Returns the number of bytes used to encode length,
* or zero if encoding is <code>BER</code>
*/
public int value() {
return this.value;
}
}
/**
* The number of bytes in the key field can be
* one byte, two bytes, four bytes, or sixteen bytes.
*/
public enum KeyLength {
OneByte(1),
TwoBytes(2),
FourBytes(4),
SixteenBytes(16);
private int value;
KeyLength(int value) {
this.value = value;
}
/**
* Returns the number of bytes used in the key.
*/
public int value() {
return this.value;
}
}
/**
* Number of bytes in key.
*/
private KeyLength keyLength;
/**
* The key if the key length is greater than four bytes.
*/
private byte[] keyIfLong;
/**
* The key if the key length is four bytes or fewer.
*/
private int keyIfShort;
/**
* The bytes from which the KLV set is made up.
* May include irrelevant bytes so that byte arrays
* with offset and length specified separately so arrays
* can be passed around with a minimum of copying.
*/
private byte[] value;
/**
* When instantiated by reading a byte array, this private
* field will record the offset of the next byte in the array
* where perhaps another KLV set begins. This is used by the
* {@link #bytesToList} method to create a list of KLV sets
* from a long byte array.
*/
private int offsetAfterInstantiation;
/**
* Creates a KLV set from the given byte array, the given offset in that array,
* the total length of the KLV set in the byte array, the specified key length,
* and the specified length field encoding.
* <p>
* If there are not as many bytes in the array as the length field
* suggests, as many bytes as possible will be stored as the value, and
* the length field will reflect the actual length.
*
* @param theBytes The bytes that make up the entire KLV set
* @param offset The offset from beginning of theBytes
* @param keyLength The number of bytes in the key.
* @param lengthEncoding The length field encoding type.
* @throws IndexOutOfBoundsException If offset is out of range of the byte array.
*/
private Klv(final byte[] theBytes, final int offset, final KeyLength keyLength,
final LengthEncoding lengthEncoding) {
Preconditions.checkElementIndex(offset,
theBytes.length,
String.format("Offset %d is out of range (byte array length: %d).",
offset,
theBytes.length));
// These methods will interpret the byte array
// and set the appropriate key length and length encoding flags.
// setLength returns the offset of where the length field ends
// and the value portion begins. It also initializes an array in
// this.value of the appropriate length.
setKey(theBytes, offset, keyLength);
// Set length and verify enough bytes exist
// setLength(..) also establishes a this.value array.
final int valueOffset = setLength(theBytes, offset + keyLength.value(), lengthEncoding);
final int remaining = theBytes.length - valueOffset;
checkEnoughBytesRemaining(remaining,
this.value.length,
String.format("Not enough bytes left in array (%d) for the declared length (%d).",
remaining,
this.value.length));
System.arraycopy(theBytes, valueOffset, this.value, 0, this.value.length);
// Private field used when creating a list of KLVs from a long array.
this.offsetAfterInstantiation = valueOffset + this.value.length;
}
/**
* Returns a byte array representing the key. This is a copy of the bytes
* from the original byte set.
*
* @return the key
*/
public byte[] getFullKey() {
final int length = this.keyLength.value;
final byte[] key = new byte[length];
switch (this.keyLength) {
case OneByte:
key[0] = (byte) this.keyIfShort;
break;
case TwoBytes:
key[0] = (byte) (this.keyIfShort >> 8);
key[1] = (byte) this.keyIfShort;
break;
case FourBytes:
key[0] = (byte) (this.keyIfShort >> 24);
key[1] = (byte) (this.keyIfShort >> 16);
key[2] = (byte) (this.keyIfShort >> 8);
key[3] = (byte) this.keyIfShort;
break;
case SixteenBytes:
System.arraycopy(this.keyIfLong, 0, key, 0, 16);
break;
}
return key;
}
/**
* Returns the value of this KLV set as a copy of the underlying byte array.
*
* @return the value
*/
public byte[] getValue() {
return Arrays.copyOf(this.value, this.value.length);
}
/**
* Returns up to the first byte of the value as an 8-bit signed integer.
*
* @return the value as an 8-bit signed integer
*/
public int getValueAs8bitSignedInt() {
final byte[] bytes = getValue();
byte value = 0;
if (bytes.length > 0) {
value = bytes[0];
}
return value;
}
/**
* Returns up to the first byte of the value as an 8-bit unsigned integer.
*
* @return the value as an 8-bit unsigned integer
*/
public int getValueAs8bitUnsignedInt() {
final byte[] bytes = getValue();
int value = 0;
if (bytes.length > 0) {
value = bytes[0] & 0xFF;
}
return value;
}
/**
* Returns up to the first two bytes of the value as a 16-bit signed integer.
*
* @return the value as a 16-bit signed integer
*/
public int getValueAs16bitSignedInt() {
final byte[] bytes = getValue();
final int length = bytes.length;
final int shortLen = length < 2 ? length : 2;
short value = 0;
for (int i = 0; i < shortLen; i++) {
value |= (bytes[i] & 0xFF) << (shortLen * 8 - i * 8 - 8);
}
return value;
}
/**
* Returns up to the first two bytes of the value as a 16-bit unsigned integer.
*
* @return the value as a 16-bit unsigned integer
*/
public int getValueAs16bitUnsignedInt() {
final byte[] bytes = getValue();
final int length = bytes.length;
final int shortLen = length < 2 ? length : 2;
int value = 0;
for (int i = 0; i < shortLen; i++) {
value |= (bytes[i] & 0xFF) << (shortLen * 8 - i * 8 - 8);
}
return value;
}
/**
* Returns up to the first four bytes of the value as a 32-bit int.
* Since all Java ints are signed, there is no signed/unsigned option.
* If you need a 32-bit unsigned int, try {@link #getValueAs64bitLong}.
*
* @return the value as an int
*/
public int getValueAs32bitInt() {
final byte[] bytes = getValue();
final int length = bytes.length;
final int shortLen = length < 4 ? length : 4;
int value = 0;
for (int i = 0; i < shortLen; i++) {
value |= (bytes[i] & 0xFF) << (shortLen * 8 - i * 8 - 8);
}
return value;
}
/**
* Returns up to the first eight bytes of the value as a 64-bit signed long.
* Note if you expect a 32-bit <b>unsigned</b> int, and since Java doesn't
* have such a thing, you could return a long instead and get the proper effect.
*
* @return the value as a long
*/
public long getValueAs64bitLong() {
final byte[] bytes = getValue();
final int length = bytes.length;
final int shortLen = length < 8 ? length : 8;
long value = 0;
for (int i = 0; i < shortLen; i++) {
value |= (long) (bytes[i] & 0xFF) << (shortLen * 8 - i * 8 - 8);
}
return value;
}
/**
* Returns the first four bytes of the value as a float according
* to IEEE 754 byte packing. See Java's Float class for details.
* This method calls <code>Float.intBitsToFloat</code> with
* {@link #getValueAs32bitInt} as the argument. However it does check
* to see that the value has at least four bytes. If it does not,
* then <tt>Float.NaN</tt> is returned.
*
* @return the value as a float
*/
public float getValueAsFloat() {
return this.getValue().length < 4 ? Float.NaN : Float.intBitsToFloat(getValueAs32bitInt());
}
/**
* Returns the first eight bytes of the value as a double according
* to IEEE 754 byte packing. See Java's Double class for details.
* This method calls <code>Double.longBitsToDouble</code> with
* {@link #getValueAs64bitLong} as the argument. However it does check
* to see that the value has at least eight bytes. If it does not,
* then <tt>Double.NaN</tt> is returned.
*
* @return the value as a double
*/
public double getValueAsDouble() {
return this.getValue().length < 8 ?
Double.NaN :
Double.longBitsToDouble(getValueAs64bitLong());
}
/**
* Return the value as a String interpreted with the given encoding.
*
* @param charsetName the character encoding
* @return value as String
* @throws UnsupportedEncodingException if the String value cannot be interpreted using the
* given encoding
*/
public String getValueAsString(final String charsetName) throws UnsupportedEncodingException {
return new String(getValue(), charsetName);
}
/**
* Sets the key according to the key found in the byte array
* and of the given length. If <tt>keyLength</tt> is different
* than what was previously set for this KLV, then this KLV's
* key length parameter will be updated.
*
* @param inTheseBytes The byte array containing the key (and other stuff)
* @param offset The offset where to look for the key
* @param keyLength The length of the key
* @return <tt>this</tt> to aid in stringing together commands
* @throws IndexOutOfBoundsException If offset is invalid
*/
private Klv setKey(final byte[] inTheseBytes, final int offset, final KeyLength keyLength) {
Preconditions.checkElementIndex(offset,
inTheseBytes.length,
String.format("Offset %d is out of range (byte array length: %d).",
offset,
inTheseBytes.length));
final int remaining = inTheseBytes.length - offset;
checkEnoughBytesRemaining(remaining,
keyLength.value(),
String.format("Not enough bytes for %d-byte key.", keyLength.value()));
// Set key according to length of key
this.keyLength = keyLength;
switch (keyLength) {
case OneByte:
this.keyIfShort = inTheseBytes[offset] & 0xFF;
this.keyIfLong = null;
break;
case TwoBytes:
this.keyIfShort = (inTheseBytes[offset] & 0xFF) << 8;
this.keyIfShort |= inTheseBytes[offset + 1] & 0xFF;
this.keyIfLong = null;
break;
case FourBytes:
this.keyIfShort = (inTheseBytes[offset] & 0xFF) << 24;
this.keyIfShort |= (inTheseBytes[offset + 1] & 0xFF) << 16;
this.keyIfShort |= (inTheseBytes[offset + 2] & 0xFF) << 8;
this.keyIfShort |= inTheseBytes[offset + 3] & 0xFF;
this.keyIfLong = null;
break;
case SixteenBytes:
this.keyIfLong = new byte[16];
System.arraycopy(inTheseBytes, offset, this.keyIfLong, 0, 16);
this.keyIfShort = 0;
break;
}
return this;
}
/**
* Sets the length according to the length found in the byte array
* and of the given length encoding.
* If <tt>lengthEncoding</tt> is different
* than what was previously set for this KLV, then this KLV's
* length encoding parameter will be updated.
* An array of the appropriate length will be initialized.
*
* @param inTheseBytes The byte array containing the key (and other stuff)
* @param offset The offset where to look for the key
* @param lengthEncoding The length of the key
* @return Offset where value field would begin after length
* @throws IndexOutOfBoundsException If offset is invalid
*/
private int setLength(final byte[] inTheseBytes, final int offset,
final LengthEncoding lengthEncoding) {
Preconditions.checkElementIndex(offset,
inTheseBytes.length,
String.format("Offset %d is out of range (byte array length: %d).",
offset,
inTheseBytes.length));
int length = 0;
int valueOffset = 0;
final int remaining = inTheseBytes.length - offset;
final String lengthEncodingErrorMessage = String.format(
"Not enough bytes for %s length encoding.",
lengthEncoding);
switch (lengthEncoding) {
case OneByte:
checkEnoughBytesRemaining(remaining, 1, lengthEncodingErrorMessage);
length = inTheseBytes[offset] & 0xFF;
setLength(length, lengthEncoding);
valueOffset = offset + 1;
break;
case TwoBytes:
checkEnoughBytesRemaining(remaining, 2, lengthEncodingErrorMessage);
length = (inTheseBytes[offset] & 0xFF) << 8;
length |= inTheseBytes[offset + 1] & 0xFF;
setLength(length, lengthEncoding);
valueOffset = offset + 2;
break;
case FourBytes:
checkEnoughBytesRemaining(remaining, 4, lengthEncodingErrorMessage);
length = (inTheseBytes[offset] & 0xFF) << 24;
length |= (inTheseBytes[offset + 1] & 0xFF) << 16;
length |= (inTheseBytes[offset + 2] & 0xFF) << 8;
length |= inTheseBytes[offset + 3] & 0xFF;
setLength(length, lengthEncoding);
valueOffset = offset + 4;
break;
case BER:
// Short BER form: If high bit is not set, then
// use the byte to determine length of payload.
// Long BER form: If high bit is set (0x80),
// then use low seven bits to determine how many
// bytes that follow are themselves an unsigned
// integer specifying the length of the payload.
// Using more than four bytes to specify the length
// is not supported in this code, though it's not
// exactly illegal KLV notation either.
checkEnoughBytesRemaining(remaining, 1, lengthEncodingErrorMessage);
final int ber = inTheseBytes[offset] & 0xFF;
// Easy case: low seven bits is length
if ((ber & 0x80) == 0) {
setLength(ber, lengthEncoding);
valueOffset = offset + 1;
} else {
final int following = ber & 0x7F; // Low seven bits
checkEnoughBytesRemaining(remaining, following + 1, lengthEncodingErrorMessage);
for (int i = 0; i < following; i++) {
length |= (inTheseBytes[offset + 1 + i] & 0xFF) << (following - 1 - i) * 8;
}
setLength(length, lengthEncoding);
valueOffset = offset + 1 + following;
}
break;
}
return valueOffset;
}
/**
* Sets the length of the value, copying or truncating the old value
* as appropriate for the new length.
*
* @param length The new number of bytes in the Value
* @param lengthEncoding The length encoding to use
* @return <tt>this</tt> to aid in stringing commands together
*/
private Klv setLength(final int length, final LengthEncoding lengthEncoding) {
// Copy old value
final byte[] bytes = new byte[length];
if (this.value != null) {
System.arraycopy(value, 0, bytes, 0, Math.min(length, this.value.length));
}
this.value = bytes;
return this;
}
/**
* Returns a list of KLV sets in the supplied byte array
* assuming the provided key length and length field encoding.
*
* @param bytes The byte array to parse
* @param offset Where to start parsing
* @param length How many bytes to parse
* @param keyLength Length of keys assumed in the KLV sets
* @param lengthEncoding Flag indicating encoding type
* @return List of KLVs
*/
public static List<Klv> bytesToList(final byte[] bytes, final int offset, final int length,
final KeyLength keyLength, LengthEncoding lengthEncoding) {
final List<Klv> list = new LinkedList<>();
int currentPos = offset;
while (currentPos < offset + length) {
final Klv klv = new Klv(bytes, currentPos, keyLength, lengthEncoding);
currentPos = klv.offsetAfterInstantiation;
list.add(klv);
}
return list;
}
private void checkEnoughBytesRemaining(final int actualNumberOfBytesRemaining,
final int minimumExpectedNumberOfBytesRemaining, final String message) {
if (actualNumberOfBytesRemaining < minimumExpectedNumberOfBytesRemaining) {
throw new IndexOutOfBoundsException(message);
}
}
}