import java.util.*; //TODO: Create tests for constructors /** *

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. Provides * auto-mapping of KLV elements within a payload to the * java.util.Map interface.

* *

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.

* *

The underlying byte array is always king. If you change the key * length ({@link #setKeyLength}) or change the length encoding * ({@link #setLengthEncoding}), you only change how the underlying * byte array is interpreted on subsequent calls.

* *

Everything in KLV is Big Endian.

* *

All getValue... methods will return up to the number * of bytes specified in the length fields ({@link #getDeclaredValueLength}) unless * there are fewer bytes actually given than are intended in which * case {@link #getActualValueLength} bytes will be used. This is to make * the code more robust for reading corrupted data.

* *

This code is released into the Public Domain. Enjoy.

* * @author Robert Harder * @author rharder # users.sourceforge.net * @version 0.3 */ public class KLV { /* ******** E N U M ******** */ /** * 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 static 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 BER */ public int value(){ return this.value; } /** * Returns the LengthEncoding matching value * with zero mapping to BER or null if no match. * @param value the matching length encoding */ public static LengthEncoding valueOf( int value ){ switch( value ){ case 1 : return OneByte; case 2 : return TwoBytes; case 4 : return FourBytes; case 0 : return BER; default: return null; } // end switch } // end valueOf } // end enum LengthEncoding /** * The number of bytes in the key field can be * one byte, two bytes, four bytes, or sixteen bytes. */ public static 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; } /** * Returns the KeyLength matching value * or null if no match is found. * @param value the matching key length */ public static KeyLength valueOf( int value ){ switch( value ){ case 1 : return OneByte; case 2 : return TwoBytes; case 4 : return FourBytes; case 16 : return SixteenBytes; default : return null; } // end switch } // end valueOf } // end enum KeyLength // These are left over from before I switched to enums // although enums require a more modern JVM. -Rob /** Indicates length field is one byte. Equal to decimal 1. */ //public final static int LENGTH_FIELD_ONE_BYTE = 1; /** Indicates length field is two bytes. Equal to decimal 2. */ //public final static int LENGTH_FIELD_TWO_BYTES = 2; /** Indicates length field is four bytes. Equal to decimal 4. */ //public final static int LENGTH_FIELD_FOUR_BYTES = 4; /** Indicates length field uses basic encoding rules (BER). Equal to decimal 8. */ //public final static int LENGTH_FIELD_BER = 8; /** Indicates key length of one byte. Equal to decimal 1. */ //public final static int KEY_LENGTH_ONE_BYTE = 1; /** Indicates key length of two bytes. Equal to decimal 2. */ //public final static int KEY_LENGTH_TWO_BYTES = 2; /** Indicates key length of four bytes. Equal to decimal 4. */ //public final static int KEY_LENGTH_FOUR_BYTES = 4; /** Indicates key length of 16 bytes. Equal to decimal 16. */ //public final static int KEY_LENGTH_SIXTEEN_BYTES = 16; /* ******** S T A T I C F I E L D S ******** */ /** * Default KeyLength value (four bytes) when * not otherwise specified. */ public final static KeyLength DEFAULT_KEY_LENGTH = KeyLength.FourBytes; /** * Default LengthEncoding value (BER) when * not otherwise specified. */ public final static LengthEncoding DEFAULT_LENGTH_ENCODING = LengthEncoding.BER; /** Default character set encoding to use is UTF-8. */ public final static String DEFAULT_CHARSET_NAME = "UTF-8"; /* ******** I N S T A N C E F I E L D S ******** */ /** * 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 kind of length encoding used. */ private LengthEncoding lengthEncoding; /** * 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; /* ******** C O N S T R U C T O R S ******** */ /** * Creates a KLV set with default key length (four bytes), * default length encoding (BER), a length of zero, and no value. * Other constructors in sub classes are not required to call this constructor. */ public KLV(){ this.keyLength = DEFAULT_KEY_LENGTH; this.lengthEncoding = DEFAULT_LENGTH_ENCODING; this.value = new byte[0]; } /** *

Creates a KLV set from the given byte array, the specified key length, * and the specified length field encoding.

* *

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 keyLength The number of bytes in the key. * @param lengthEncoding The length field encoding type. * @throws NullPointerException If any parameters are null. * @throws IllegalArgumentException If there are not enough bytes in the array * to cover at least the key and the length field. */ public KLV( byte[] theBytes, KeyLength keyLength, LengthEncoding lengthEncoding ){ this( theBytes, 0, keyLength, lengthEncoding ); } /** *

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.

* *

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 NullPointerException If any parameters are null. * @throws IllegalArgumentException If there are not enough bytes in the array * to cover at least the key and the length field. * @throws ArrayIndexOutOfBoundsException If offset is out of range of the byte array. */ public KLV( byte[] theBytes, int offset, KeyLength keyLength, LengthEncoding lengthEncoding ){ // Check for null and bad offset if( theBytes == null ) throw new NullPointerException( "KLV byte array must not be null." ); if( keyLength == null ) throw new NullPointerException( "Key length must not be null." ); if( lengthEncoding == null ) throw new NullPointerException( "Length encoding must not be null." ); if( offset < 0 || offset >= theBytes.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, theBytes.length ) ); // These public 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. int valueOffset = setLength( theBytes, offset + keyLength.value(), lengthEncoding ); int remaining = theBytes.length - valueOffset; if( remaining < this.value.length ) throw new ArrayIndexOutOfBoundsException( 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; } // end constructor /** * Create a KLV set with the given key, key length, length field encoding, * and provided value in a byte array. If value is null, * then a zero-length value is assumed. */ public KLV( int shortKey, KeyLength keyLength, LengthEncoding lengthFieldEncoding, byte[] value ){ this( shortKey, keyLength, lengthFieldEncoding, value, 0, value.length ); } /** * Create a KLV set with the given key, key length, length field encoding, * and provided value in a byte array. If value is null, * then a zero-length value is assumed. */ public KLV( int shortKey, KeyLength keyLength, LengthEncoding lengthEncoding, byte[] value, int offset, int length ){ // Check for bad parameters if( keyLength == null ) throw new NullPointerException( "Key length must not be null." ); if( lengthEncoding == null ) throw new NullPointerException( "Length encoding must not be null." ); if( value != null ){ if( offset < 0 ) throw new ArrayIndexOutOfBoundsException( "Offset must not be negative: " + offset ); if( value.length > 0 && offset >= value.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, value.length ) ); if( length - offset < value.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes in array (%d) for declared length (%d).", value.length, length ) ); } // end if: value not null // Key this.keyLength = keyLength; this.keyIfShort = shortKey; // Length & value this.lengthEncoding = lengthEncoding; if( value == null ){ this.value = new byte[0]; } else { switch( lengthEncoding ){ case OneByte: if( length > (1<<8)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", lengthEncoding, length ) ); this.value = new byte[length]; System.arraycopy(value,offset, this.value,0,length); break; case TwoBytes: if( length > (1<<16)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", lengthEncoding, length ) ); this.value = new byte[length]; System.arraycopy(value,offset, this.value,0,length); break; case FourBytes: case BER: // Any Java length is allowed. this.value = new byte[ length ]; System.arraycopy(value,offset, this.value,0,length); break; default: assert false : lengthEncoding; // We've accounted for all types } } // end else: value not null } // end constructor /** * Create a KLV set with the given key, key length based on the length of the array, * length field encoding, * and provided value in a byte array. If value is null, * then a zero-length value is assumed. */ public KLV( byte[] key, LengthEncoding lengthEncoding, byte[] value, int offset, int length ){ // Check for bad parameters if( key == null ) throw new NullPointerException( "Key must not be null." ); if( lengthEncoding == null ) throw new NullPointerException( "Length encoding must not be null." ); if( !(key.length==1 || key.length==2 || key.length==4 || key.length==16) ) throw new IllegalArgumentException( "Key length must be 1, 2, 4, or 16 bytes, not " + key.length ); if( value != null ){ if( offset < 0 || offset >= value.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, value.length ) ); if( offset + length >= value.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes in array for declared length of %d.", length ) ); } // end if: value not null // Key this.setKey( key, 0, KeyLength.valueOf(key.length) ); // Length & value this.lengthEncoding = lengthEncoding; if( value == null ){ this.value = new byte[0]; } else { switch( lengthEncoding ){ case OneByte: if( length > (1<<8)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", lengthEncoding, length ) ); this.value = new byte[length]; System.arraycopy(value,offset, this.value,0,length); case TwoBytes: if( length > (1<<16)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", lengthEncoding, length ) ); this.value = new byte[length]; System.arraycopy(value,offset, this.value,0,length); break; case FourBytes: case BER: // Any Java length is allowed. this.value = new byte[ length ]; System.arraycopy(value,offset, this.value,0,length); break; default: assert false : lengthEncoding; // We've accounted for all types } } // end else: value not null } // end constructor /** * Return the KLV as a byte array. * The array is copied from the original underlying byte array. */ public byte[] toBytes(){ byte[] key = this.getFullKey(); byte[] lengthField = KLV.makeLengthField(this.lengthEncoding, this.value.length); byte[] bytes = new byte[ key.length + lengthField.length + this.value.length ]; System.arraycopy(key,0, bytes,0,key.length); System.arraycopy(lengthField,0, bytes,key.length,lengthField.length); System.arraycopy(this.value,0, bytes,key.length+lengthField.length,this.value.length); return bytes; } public static void main(String[] args){ KLV klv; // Add one-byte subKLV for( int i = 0; i < 255; i++ ){ klv = new KLV(); klv.addSubKLV(42, (byte)i); klv.addSubKLV(23, (byte)((i+10)%255)); KLV k42 = klv.getSubKLVMap().get(42); KLV k23 = klv.getSubKLVMap().get(23); } } /* ******** P U B L I C G E T M E T H O D S ******** */ /** * Returns a list of all KLV sets in this payload (value field) * assuming the existing key length and length field encoding. */ public List getSubKLVList(){ return this.getSubKLVList(this.keyLength, this.lengthEncoding); } /** * Returns a list of all KLV sets in this payload (value field) * assuming the given key length and length field encoding. */ public List getSubKLVList( KeyLength keyLength, LengthEncoding lengthEncoding ){ return KLV.bytesToList( this.value,0,this.value.length, keyLength, lengthEncoding ); } /** * Return a mapping of keys (up to four bytes long) to KLV sets * from this KLV's payload (value field) based on the existing * key length and length field encoding. * If two KLV subsets are in the payload, and they each have the * same key value, then the latter one will overwrite the earlier one. * */ public Map getSubKLVMap(){ return this.getSubKLVMap(this.keyLength, this.lengthEncoding); } /** * Return a mapping of keys (up to four bytes long) to KLV sets * from this KLV's payload (value field) based on an assumed key * length and length field encoding scheme. * If two KLV subsets are in the payload, and they each have the * same key value, then the latter one will overwrite the earlier one. * */ public Map getSubKLVMap( KeyLength keyLength, LengthEncoding lengthEncoding ){ return KLV.bytesToMap( this.value,0,this.value.length, keyLength, lengthEncoding ); } /** * Returns the length of the key * (not necessarily of the payload within, * if the payload is more KLV data). * * @return length of key. */ public KeyLength getKeyLength(){ return this.keyLength; } // end getKeyLength /** * Returns up to four bytes of the key as an int. * If the key is sixteen bytes long, then the lowest * four bytes are used to determine this short key * (which is officially meaningless). * * @return the key */ public int getShortKey(){ switch( this.keyLength ){ case OneByte: return this.keyIfShort & 0xFF; case TwoBytes: return this.keyIfShort & 0xFFFF; case FourBytes: return this.keyIfShort; case SixteenBytes: assert this.keyIfLong != null; assert 16 == this.keyIfLong.length : this.keyIfLong.length; int key = 0; for( int i = 0; i < 4; i++ ){ key |= (this.keyIfLong[13+i] & 0xFF) << (4-i)*8; } // end for: four bytes return key; default: assert false : this.keyLength; return 0; } // end switch } // end getShortKey /** * 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(){ int length = this.keyLength.value; 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: assert this.keyIfLong != null; assert 16 == this.keyIfLong.length : this.keyIfLong.length; System.arraycopy(this.keyIfLong,0, key,0,16); break; default: assert false : this.keyLength; } // end switch return key; } // end getFullKey /** * Returns the length encoding flag * (not necessarily of the payload within, * if the payload is more KLV data). * * @return length field encoding flag */ public LengthEncoding getLengthEncoding(){ return this.lengthEncoding; } /** * Returns the length of the value in this KLV set. * * @return length of value */ public int getLength(){ return this.value.length; } /** * Returns the value of this KLV set. * This is the actual byte array, so changes to the byte array * change the KLV's actual value. * * @return the value */ public byte[] getValue(){ return this.value; } /** * 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(){ byte[] bytes = getValue(); byte value = 0; if( bytes.length > 0 ) value = bytes[0]; return value; } // end getValueAs8bitSignedInt /** * 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(){ byte[] bytes = getValue(); int value = 0; if( bytes.length > 0 ) value = bytes[0] & 0xFF; return value; } // end getValueAs8bitSignedInt /** * 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(){ byte[] bytes = getValue(); short value = 0; int length = bytes.length; int shortLen = length < 2 ? length : 2; for( int i = 0; i < shortLen; i++ ) value |= (bytes[i] & 0xFF) << (shortLen*8 - i*8 - 8); return value; } // end getValueAs16bitSignedInt /** * 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(){ byte[] bytes = getValue(); int value = 0; int length = bytes.length; int shortLen = length < 2 ? length : 2; for( int i = 0; i < shortLen; i++ ) value |= (bytes[i] & 0xFF) << (shortLen*8 - i*8 - 8); return value; } // end getValueAs16bitUnsignedInt /** * 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(){ byte[] bytes = getValue(); int value = 0; int length = bytes.length; int shortLen = length < 4 ? length : 4; for( int i = 0; i < shortLen; i++ ) value |= (bytes[i] & 0xFF) << (shortLen*8 - i*8 - 8); return value; } // end getValueAs32bitSignedInt /** * Returns up to the first eight bytes of the value as a 64-bit signed long. * Note if you expect a 32-bit unsigned 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(){ byte[] bytes = getValue(); long value = 0; int length = bytes.length; int shortLen = length < 8 ? length : 8; for( int i = 0; i < shortLen; i++ ) value |= (long)(bytes[i] & 0xFF) << (shortLen*8 - i*8 - 8); return value; } // end getValueAs64bitLong /** * 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 Float.intBitsToFloat 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 Float.NaN is returned. * * @return the value as a float */ public float getValueAsFloat(){ return this.getValue().length < 4 ? Float.NaN : Float.intBitsToFloat(getValueAs32bitInt()); } // end getValueAsFloat /** * 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 Double.longBitsToDouble 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 Double.NaN is returned. * * @return the value as a float */ public double getValueAsDouble(){ return this.getValue().length < 8 ? Double.NaN : Double.longBitsToDouble(getValueAs64bitLong()); } // end getValueAsDouble /** * Returns the value as a String using KLV's default character set * as defined by {@link #DEFAULT_CHARSET_NAME} or the computer's default * charset if that is not available. * * @return value as a string */ public String getValueAsString(){ try{ return getValueAsString( DEFAULT_CHARSET_NAME ); } catch( java.io.UnsupportedEncodingException exc ){ return new String( getValue() ); } // end catch } // end getValueAsString /** * Return the value as a String, interpreted with given encoding. * * @return value as String. */ public String getValueAsString( String charsetName ) throws java.io.UnsupportedEncodingException{ return new String( getValue(), charsetName ); } /* ******** S E T K E Y M E T H O D S ******** */ /** * Sets the key length and discards any leftover bytes. * If sizing up, key is preserved. For instance, a one-byte * key of 42, when changed to a four-byte key, will still * be 42. When jumping to or from a sixteen byte key however, * the previous value of the key is discarded. * * @param keyLength The new key length * @return this to aid in stringing together commands */ public KLV setKeyLength( KeyLength keyLength ){ // No change? Bail out. if( this.keyLength == keyLength ) return this; // Expanding to sixteen? if( keyLength == KeyLength.SixteenBytes ){ this.keyIfShort = 0; this.keyIfLong = new byte[16]; } // end if: expanding to sixteen // Shrinking from sixteen? else if( this.keyLength == KeyLength.SixteenBytes ){ this.keyIfShort = 0; this.keyIfLong = null; } // end else if: shrinking from sixteen // Else, 1, 2, 4 switch-a-roos are no matter // Whoopie. this.keyLength = keyLength; return this; } public KLV setKey( byte[] key ){ if( key == null ) throw new NullPointerException( "Key must not be null." ); switch( key.length ){ case 1: case 2: case 4: case 16: return this.setKey( key, 0, KeyLength.valueOf(key.length)); default: throw new IllegalArgumentException("Invalid key size: " + key.length ); } } /** * Sets the key according to the key found in the byte array * and of the given length. If keyLength 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 this to aid in stringing together commands * @throws NullPointException If any parameter is null * @throws ArrayIndexOutOfBoundsException If offset is invalid */ public KLV setKey( byte[] inTheseBytes, int offset, KeyLength keyLength ){ // Check for null and bad offset if( inTheseBytes == null ) throw new NullPointerException( "Byte array must not be null." ); if( keyLength == null ) throw new NullPointerException( "Key length must not be null." ); if( offset < 0 || offset >= inTheseBytes.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, inTheseBytes.length ) ); if( inTheseBytes.length - offset < keyLength.value() ) throw new ArrayIndexOutOfBoundsException( 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; default: throw new IllegalArgumentException("Unknown key length: " + keyLength ); } return this; } // end setKey /** * Sets the key according to the existing key length. * * @param shortKey the key of one, two, or four bytes * @return this to aid in stringing commands together */ public KLV setKey( int shortKey ){ return setKey( shortKey, this.keyLength ); } /** * Sets the key according to the given key length. * If you specify a sixteen-byte key, then the lowest four * bytes will be set to the four bytes in the int shortKey. * * @param shortKey the key of one, two, or four bytes * @param keyLength the length of the key * @return this to aid in stringing commands together */ public KLV setKey( int shortKey, KeyLength keyLength ){ switch( keyLength ){ case OneByte: case TwoBytes: case FourBytes: this.keyIfShort = shortKey; this.keyIfLong = null; this.keyLength = keyLength; break; case SixteenBytes: byte[] key = new byte[16]; for( int i = 0; i < 4; i++ ){ key[13+i] = (byte)(shortKey >> (3-i)*8); } // end for: four bytes this.keyLength = keyLength; break; default: assert false : keyLength; } // end switch return this; } // end setKey /* ******** S E T L E N G T H M E T H O D S ******** */ /** * Sets the length encoding used in this KLV set. * If necessary, the current value will be truncated if * the new length encoding cannot support the size of the value. * This would only be the case for one- and two-byte length encodings. * * @param lengthEncoding The new length encoding * @return this to aid in stringing together commands */ public KLV setLengthEncoding( LengthEncoding lengthEncoding ){ switch( lengthEncoding ){ case OneByte: if( this.value.length > (2<<8)-1 ){ byte[] bytes = new byte[(2<<8)-1]; System.arraycopy(this.value,0, bytes,0,bytes.length); this.value = bytes; } // end if: need to truncate this.lengthEncoding = lengthEncoding; break; case TwoBytes: if( this.value.length > (2<<16)-1 ){ byte[] bytes = new byte[(2<<16)-1]; System.arraycopy(this.value,0, bytes,0,bytes.length); this.value = bytes; } // end if: need to truncate this.lengthEncoding = lengthEncoding; break; case FourBytes: case BER: this.lengthEncoding = lengthEncoding; break; default: assert false : lengthEncoding; } // end switch return this; } /** * Sets the length according to the length found in the byte array * and of the given length encoding. * If lengthEncoding 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 NullPointException If any parameter is null * @throws ArrayIndexOutOfBoundsException If offset is invalid */ public int setLength( byte[] inTheseBytes, int offset, LengthEncoding lengthEncoding ){ // Check for null and bad offset if( inTheseBytes == null ) throw new NullPointerException( "Byte array must not be null." ); if( lengthEncoding == null ) throw new NullPointerException( "Length encoding must not be null." ); if( offset < 0 || offset >= inTheseBytes.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, inTheseBytes.length ) ); int length = 0; int valueOffset = 0; switch( lengthEncoding ){ case OneByte: if( inTheseBytes.length - offset < 1 ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes for %s length encoding.", lengthEncoding ) ); length = inTheseBytes[offset] & 0xFF; setLength( length, lengthEncoding ); valueOffset = offset + 1; break; case TwoBytes: if( inTheseBytes.length - offset < 2 ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes for %s length encoding.", lengthEncoding ) ); length = (inTheseBytes[offset] & 0xFF) << 8; length |= inTheseBytes[offset+1] & 0xFF; setLength( length, lengthEncoding ); valueOffset = offset + 2; break; case FourBytes: if( inTheseBytes.length - offset < 4 ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes for %s length encoding.", lengthEncoding ) ); 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. if( inTheseBytes.length - offset < 1 ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes for %s length encoding.", lengthEncoding ) ); int ber = inTheseBytes[offset] & 0xFF; // Easy case: low seven bits is length if( (ber & 0x80) == 0 ){ setLength( ber, lengthEncoding ); valueOffset = offset + 1; } // Else, use following bytes to determine length else{ int following = ber & 0x7F; // Low seven bits if( inTheseBytes.length - offset < following+1 ) throw new ArrayIndexOutOfBoundsException( String.format( "Not enough bytes for %s length encoding.", lengthEncoding ) ); 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; default: assert false : lengthEncoding; } // end switch return valueOffset; } /** * Sets the length of the Value, copying or truncating the old value * as appropriate for the new length and using the existing * length encoding. * * @param length The new number of bytes in the Value * @return this to aid in stringing commands together */ public KLV setLength( int length ){ return this.setLength( length, this.lengthEncoding ); } /** * 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 this to aid in stringing commands together */ public KLV setLength( int length, LengthEncoding lengthEncoding ){ if( length < 0 ) throw new IllegalArgumentException( "Length must not be negative: " + length ); // Check errors based on length encoding switch( lengthEncoding ){ case OneByte: if( length > (1<<8)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", lengthEncoding, length ) ); break; case TwoBytes: if( length > (1<<16)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", lengthEncoding, length ) ); break; case FourBytes: // Any Java length is allowed. case BER: // Any Java length is allowed. break; default: assert false : lengthEncoding; // We've accounted for all types } // end switch // Copy old value byte[] bytes = new byte[length]; if( this.value != null ){ System.arraycopy(value,0, bytes,0,(int)Math.min(length,this.value.length)); } // end if: value exists this.value = bytes; return this; } /* ******** S E T V A L U E M E T H O D S ******** */ /** * Sets the value of the KLV set, throwing an * IllegalArgumentException if newValue * is too long for the existing length encoding. * * @param newValue New value for the KLV set * @return this to aid in stringing commands together. */ public KLV setValue( byte[] newValue ){ return setValue( newValue, 0, newValue.length ); } /** * Sets the value of the KLV set and adjusts the length encoding as specified. * IllegalArgumentException if newValue * is too long for the existing length encoding. */ public KLV setValue( byte[] newValue, int offset, int length ){ // Check for null and bad offset if( newValue == null ) throw new NullPointerException( "Byte array must not be null." ); if( offset < 0 ) throw new ArrayIndexOutOfBoundsException( "Offset must not be negative: " + offset ); if( value.length > 0 && offset >= value.length ) // Empty arrays are OK throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, value.length ) ); if( newValue.length - offset < length ){ throw new IllegalArgumentException(String.format( "Number of bytes (%d) and offset (%d) not sufficient for declared length (%d).", newValue.length, offset, length )); } // Check errors based on length encoding switch( this.lengthEncoding ){ case OneByte: if( length > (1<<8)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", this.lengthEncoding, length ) ); break; case TwoBytes: if( length > (1<<16)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", this.lengthEncoding, length ) ); break; case FourBytes: // Any Java length is allowed. case BER: // Any Java length is allowed. break; default: assert false : this.lengthEncoding; // We've accounted for all types } // end switch // Copy old value byte[] bytes = new byte[length]; System.arraycopy(newValue,offset, bytes,0,length); this.value = bytes; return this; } /* ******** A D D M E T H O D S ******** */ /** * Adds a sub KLV set with the given key and the * single byte of data as the payload * using the parent's key length * and parent's length field encoding. * * @param key The key for the data * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int key, byte subValue ){ return addSubKLV( key, new byte[]{ subValue } ); } // end addSubKLV /** * Adds a sub KLV set with the given key and the * single short (two bytes) of data as the payload * using the parent's key length * and parent's length field encoding. * * @param key The key for the data * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int key, short subValue ){ return addSubKLV( key, new byte[]{ (byte)(subValue >> 8), (byte)subValue } ); } // end addSubKLV /** * Adds a sub KLV set with the given key and the * single int (four bytes) of data as the payload * using the parent's key length * and parent's length field encoding. * * @param key The key for the data * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int key, int subValue ){ return addSubKLV( key, new byte[]{ (byte)(subValue >> 24), (byte)(subValue >> 16), (byte)(subValue >> 8), (byte)subValue } ); } // end addSubKLV /** * Adds a sub KLV set with the given key and the * single long (eight bytes) of data as the payload * using the parent's key length * and parent's length field encoding. * * @param key The key for the data * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int key, long subValue ){ return addSubKLV( key, new byte[]{ (byte)(subValue >> 56), (byte)(subValue >> 48), (byte)(subValue >> 40), (byte)(subValue >> 32), (byte)(subValue >> 24), (byte)(subValue >> 16), (byte)(subValue >> 8), (byte)subValue } ); } // end addSubKLV /** * Adds a sub KLV set with the given key and the * string of data as the payload * using the parent's key length * and parent's length field encoding. * If data is null, then the corresponding * payload length will be zero. * The default charset (UTF-8) will be used unless * that is not supported in which case the current * computer's default charset will be used. * * @param key The key for the data * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int key, String subValue ){ if( subValue == null ){ return addSubKLV( key, new byte[0] ); } // end if: null else { try{ return addSubKLV( key, subValue.getBytes(KLV.DEFAULT_CHARSET_NAME) ); } catch( java.io.UnsupportedEncodingException exc ){ return addSubKLV( key, subValue.getBytes() ); } // end catch } // end else: not null } // end addSubKLV /** * Adds a KLV set to the overall payload using the given * key, parent's key length, parent's length encoding, and the provided data. * Underlying byte array is copied and replaced. * * @param key The key for the data * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int key, byte[] subValue ){ return addSubKLV( key, this.keyLength, this.lengthEncoding, subValue ); } // end addSubKLV /** * Adds a KLV set to the overall payload using the given * key, given sub key length, given length encoding, and the provided data. * Underlying byte array is copied and replaced. * * @param subKey The key for the data * @param subKeyLength Length of key in sub KLV * @param subLengthEncoding Length field encoding in sub KLV * @param subValue The data in the payload * @return this, to aid in stringing commands together. */ public KLV addSubKLV( int subKey, KeyLength subKeyLength, LengthEncoding subLengthEncoding, byte[] subValue ){ return addSubKLV( new KLV( subKey, subKeyLength, subLengthEncoding, subValue, 0, subValue.length ) ); } // end addSubKLV /** * Adds the given KLV set to the payload by calling * addPaylaod( sub.toBytes() ). * * @param sub the KLV set to add. * @return this, to aid in stringing commands together. */ public KLV addSubKLV( KLV sub ){ return addPayload( sub.toBytes() ); } /** * Adds the provided bytes to the payload and adjusts the length field. * * @param extraBytes New bytes to add * @return this, to aid in stringing commands together. */ public KLV addPayload( byte[] extraBytes ){ addPayload( extraBytes, 0, extraBytes.length ); return this; } /** * Adds the provided bytes to the payload and adjusts the length field. * If the length field encoding does not support payloads as large * as would result from adding extraBytes, * then an IllegalArgumentException is thrown. * * @param extraBytes new bytes to add * @param extraOffset offset within extraBytes * @param extraLength length of extraBytes to use * @return this, to aid in stringing commands together. */ public KLV addPayload( byte[] bytes, int offset, int length ){ if( bytes == null ) throw new NullPointerException( "Byte array must not be null." ); if( offset < 0 || offset >= bytes.length ) throw new ArrayIndexOutOfBoundsException( String.format( "Offset %d is out of range (byte array length: %d).", offset, bytes.length ) ); if( bytes.length - offset < length ){ throw new IllegalArgumentException(String.format( "Number of bytes (%d) and offset (%d) not sufficient for declared length (%d).", bytes.length, offset, length )); } int newLength = this.value.length + length; // Check errors based on length encoding switch( this.lengthEncoding ){ case OneByte: if( newLength > (1<<8)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", this.lengthEncoding, newLength ) ); break; case TwoBytes: if( newLength > (1<<16)-1 ) throw new IllegalArgumentException(String.format( "%s encoding cannot support a %d-byte value.", this.lengthEncoding, newLength ) ); break; case FourBytes: // Any Java length is allowed. case BER: // Any Java length is allowed. break; default: assert false : this.lengthEncoding; // We've accounted for all types } // end switch byte[] newValue = new byte[ newLength ]; System.arraycopy(this.value,0, newValue,0,this.value.length); System.arraycopy(bytes,offset, newValue,this.value.length,length); this.value = newValue; return this; } /* ******** O B J E C T O V E R R I D E ******** */ public String toString(){ StringBuilder sb = new StringBuilder(); sb.append('['); // Key sb.append("Key="); if( this.keyLength.value() <= 4 ) sb.append( getShortKey() ); else{ sb.append('['); byte[] longKey = getFullKey(); for( byte b : longKey ) sb.append(Long.toHexString(b & 0xFF)).append(' '); sb.append(']'); } // Length sb.append(", Length="); sb.append( getLength() ); // Value sb.append(", Value=["); byte[] value = getValue(); for( byte b : value ) sb.append(Long.toHexString(b & 0xFF)).append(' '); sb.append(']'); sb.append(']'); return sb.toString(); } /* ******** S T A T I C M E T H O D S ********* */ /** * 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 java.util.List bytesToList( byte[] bytes, int offset, int length, KeyLength keyLength, LengthEncoding lengthEncoding ){ LinkedList list = new LinkedList(); int currentPos = offset; // Keep track of where we are while( currentPos < offset + length ){ try{ KLV klv = new KLV( bytes, currentPos, keyLength, lengthEncoding ); currentPos = klv.offsetAfterInstantiation; // private access list.add( klv ); } catch( Exception exc ){ // Stop trying for more? System.err.println("Stopped parsing with exception: " + exc.getMessage() ); break; } // end catch } // end while return list; } // end parseBytes /** * Return a mapping of keys (up to four bytes) to KLV * sets based on an assumed key length and length field * encoding scheme. If two KLV subsets are in the payload, * and they each have the same key value, then the latter * one will overwrite the earlier one. * * @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 Map of keys to KLVs */ public static Map bytesToMap( byte[] bytes, int offset, int length, KeyLength keyLength, LengthEncoding lengthEncoding ){ Map map = new HashMap(); for( KLV klv : KLV.bytesToList(bytes, offset, length, keyLength, lengthEncoding) ){ map.put( klv.getShortKey(),klv ); } return map; } // end parseBytes /** * Make a byte array that represents the length field necessary to * indicate the given payload length. Most useful when using BER encoding. * * @param lengthEncoding field encoding flag * @param payloadLength number of bytes in value * @return byte array with appropriate length field bytes */ protected static byte[] makeLengthField( LengthEncoding lengthEncoding, int payloadLength ){ // Bytes for length encoding byte[] bytes = null; switch( lengthEncoding ){ // Unsigned integer, one byte long. case OneByte: if( payloadLength > 255 ) throw new IllegalArgumentException( String.format("Too much data (%d bytes) for one-byte length field encoding.", payloadLength) ); bytes = new byte[]{ (byte)payloadLength }; break; // Unsigned integer, two bytes long, big endian. case TwoBytes: if( payloadLength > 65535 ) throw new IllegalArgumentException( String.format("Too much data (%d bytes) for two-byte length field encoding.", payloadLength) ); bytes = new byte[]{ (byte)(payloadLength >> 8), (byte)payloadLength }; break; // (Un?)signed integer, four bytes long, big endian. case FourBytes: bytes = new byte[]{ (byte)(payloadLength >> 24), (byte)(payloadLength >> 16), (byte)(payloadLength >> 8), (byte)payloadLength }; break; // 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. case BER: if( payloadLength <= 127 ){ bytes = new byte[]{ (byte)payloadLength }; } // end if: short form else { if( payloadLength <= 255 ){ // One byte bytes = new byte[]{ (byte)0x81, (byte)payloadLength }; } else if( payloadLength <= 65535 ){ // Two bytes bytes = new byte[]{ (byte)0x82, (byte)(payloadLength >> 8), (byte)payloadLength }; } else { // Four bytes bytes = new byte[]{ (byte)0x84, (byte)(payloadLength >> 24), (byte)(payloadLength >> 16), (byte)(payloadLength >> 8), (byte)payloadLength }; } } // end else: long form break; default: throw new IllegalStateException( "Unknown length field encoding flag: " + lengthEncoding ); } // end switch return bytes; } // end makeLengthField } // end class KLV