package de.persosim.simulator.tlv;
import static org.globaltester.logging.BasicLogger.DEBUG;
import static org.globaltester.logging.BasicLogger.logException;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import de.persosim.simulator.exception.ISO7816Exception;
import de.persosim.simulator.platform.Iso7816;
/**
* This class implements basic functionality for the use within TLV data
* objects. Any TLV data object consists of objects that represent the tag
* field, followed by a length field and finally a value field, that must be
* present but may be empty. Checks for the validity of values and conformity of
* encoding are provided by the elements themselves. Further on the object
* itself only provides methods to check for the interaction of the elements.
* Constructors and set methods will reject any invalid or malformed element if
* not explicitly told otherwise. If so the object accordingly is marked as
* exempted from checks as a whole.
*
* In order to prevent the object from being presented valid tag fields that are
* modified by direct access via reference later on, e.g. a primitive encoded
* TLV data object being converted to a constructed one, a tag must not be
* changed in any way without being noticed. Any change of this element hence
* must be committed through methods provided by this object. In order for this
* to happen the element is not set directly but as a clone of the respective
* element, eliminating any outside access by reference. Accordingly any get
* method also returns a clone of this element. On the contrary, these
* precautions are unnecessary for the length field. As e.g. for constructed TLV
* data objects the length field may change through changes to sub-objects, the
* length field, when requested, can not be taken for being up-to-date. Hence
* any length encoding that may be present must be checked. If no length
* encoding is present or the length encoding does not match the actual value
* field, the length field is re-calculated based upon the actual value field.
* Access to value fields also is not restricted. Validity of these fields is
* checked on access. While value fields of primitive encoded TLV data objects
* are always valid, validity of value fields of constructed TLV data objects
* needs to be checked recursively.
*
* @author slutters
*
*/
public abstract class TlvDataObject extends TlvElement implements Iso7816, ValidityChecks {
/* The tag component of any TLV data object
*
* The tag does not have to be immutable. However only methods from within this class hierarchy are allowed
* to perform changes as long as they fulfill the following conditions
* 1) No construct/set/get method is allowed to permit outside references to the tag.
* 2) Tags may only be set or modified if the result matches the tag-class of its class
* (e.g. tags within PrimitiveTlvDataObject must always be primitive).
*/
protected TlvTag tlvTag;
/*
* The length component of any TLV data object.
*
* The length field for any TLV data object by default is computed based on the length of the
* value field. As in BER encoding allow for more than one different but equally valid and hence
* no unique representation of the same indicated length the length field for TLV data
* objects can be explicitly stated with this variable. This also allows for an explicit
* manipulation of this field e.g. for debug or testing purposes.
*
* Variable only needs to be set if default DER-encoded length is explicitly to be overridden,
* either by a non-DER BER encoding or an intentionally damaged field.
*/
protected TlvLength tlvLength;
/* The value component will be set in the various sub classes */
//TODO define value field within this class, that reduces code duplication within subclasses (esp. constructor redundancy) and requires only a little caution when casting the valuefield within those subclasses
protected boolean performValidityChecks;
/*--------------------------------------------------------------------------------*/
/**
* Basic constructor for TLV objects
* @param performValidityChecks true: perform validity checks, false: do not perform validity checks
*/
protected TlvDataObject(boolean performValidityChecksInput) {
performValidityChecks = performValidityChecksInput;
}
/**
* Constructor for TLV objects
* @param dataField the byte array that in a certain range contains the TLV data object
* @param minOffset the first offset of the range to contain the TLV data object (inclusive)
* @param maxOffset the first offset not to be part of the range to contain the TLV data object (exclusive).
*/
public TlvDataObject(byte[] dataField, int minOffset, int maxOffset) {
if(dataField == null) {throw new NullPointerException();}
if(minOffset < 0) {throw new IllegalArgumentException("min offset must not be less than 0");}
if(maxOffset < minOffset) {throw new IllegalArgumentException("max offset must not be smaller than min offset");}
if(maxOffset > dataField.length) {throw new IllegalArgumentException("selected array area must not lie outside of data array");}
if(minOffset == maxOffset) {throw new IllegalArgumentException("selected part of data field must be greater than 0");}
performValidityChecks = PERFORM_VALIDITY_CHECKS;
/*
* Determine Tag
*/
int currentOffset = minOffset;
tlvTag = new TlvTag(dataField, currentOffset, maxOffset);
/*
* Determine Length
*/
currentOffset += getNoOfTagBytes();
tlvLength = new TlvLength(dataField, currentOffset, maxOffset);
int indicatedLength = tlvLength.getIndicatedLength();
/*
* Determine Value
*/
currentOffset += tlvLength.getLength();
if((currentOffset + indicatedLength) > maxOffset) {
/* error, length indicated by TLV would exceed expected length */
ISO7816Exception.throwIt(SW_6A85_NC_INCONSISTENT_WITH_TLV_STRUCTURE, "offset outside data array");
}
/* The actual value is set by the sub-class constructors */
}
/*--------------------------------------------------------------------------------*/
/**
* Set the tag of this object.
* If validity checks are to be skipped the tag may be set to any arbitrary value.
* In any case the tag's indicated class must match the class of the TLV data object.
* If the indicated class of a TLV object is to be changed the object must be rebuild as object of the respective class.
* @param tlvTag the tag
* @param performValidityChecksInput true: perform validity checks, false: do not perform validity checks
*/
public abstract void setTag(TlvTag tlvTag, boolean performValidityChecksInput);
/**
* Set the tag of this object.
* @param tlvTag the tag
*/
public void setTag(TlvTag tlvTag) {
setTag(tlvTag, PERFORM_VALIDITY_CHECKS);
}
/**
* Set the length of this object
* @param tlvLengthInput the length
* @param performValidityChecksInput true: perform validity checks, false: do not perform validity checks
*/
public void setLength(TlvLength tlvLengthInput, boolean performValidityChecksInput) {
if(tlvLengthInput == null) {throw new NullPointerException("length must not be null");}
performValidityChecks = performValidityChecksInput;
if(performValidityChecks) {
if(!tlvLengthInput.isValidBerEncoding()) {throw new IllegalArgumentException("new length must be valid BER encoding");}
if(tlvLengthInput.getIndicatedLength() != this.getNoOfValueBytes()) {throw new IllegalArgumentException("new length must match length of present value field");}
}
this.tlvLength = tlvLengthInput;
}
/**
* Set the length of this object
* @param tlvLengthInput the length
*/
public void setLength(TlvLength tlvLengthInput) {
setLength(tlvLengthInput, PERFORM_VALIDITY_CHECKS);
}
/*--------------------------------------------------------------------------------*/
/**
* @return the noOfTagBytes
*/
public int getNoOfTagBytes() {
return tlvTag.getLength();
}
/**
* @return the noOfLengthBytes
*/
public int getNoOfLengthBytes() {
return getTlvLength().getLength();
}
/**
* @return the no of bytes occupied by the value field
*/
public abstract int getNoOfValueBytes();
@Override
public int getLength() {
return getNoOfTagBytes() + getNoOfLengthBytes() + getNoOfValueBytes();
}
/*--------------------------------------------------------------------------------*/
@Override
public byte[] toByteArray() {
ByteArrayOutputStream outputStream;
outputStream = new ByteArrayOutputStream();
try {
/* tag can be accessed directly */
outputStream.write(tlvTag.toByteArray());
/* length must be accessed by getter in case there is a valid override */
outputStream.write(getTlvLength().toByteArray());
/* value must be accessed by getter as values are only specified by sub classes */
outputStream.write(getTlvValue().toByteArray());
} catch (IOException e) {
logException(this.getClass(), e, DEBUG);
}
return outputStream.toByteArray();
}
/**
* Returns the tag number encoded within the tag field
* @return the tag number encoded within the tag field
*/
public int getTagNo() {
return tlvTag.getIndicatedTagNo();
}
/**
* Returns the length of the value field as indicated by the length field
* @return the length of the value field as indicated by the length field
*/
public int getLengthValue() {
return getTlvLength().getIndicatedLength();
}
/**
* @return the valueField
*/
public abstract byte[] getValueField();
/**
* The tag returned is a clone of the original object.
* This is necessary for the following reasons:
* The type of this object allows changes in general.
* These changes may be valid for themselves while being
* invalid as part of the TLV data object represented
* by this class.
*
* @return the tlvTag
*/
public TlvTag getTlvTag() {
return tlvTag.clone();
}
/**
* Returns the length field for this TLV data object. If not explicitly set
* the length field is always returned as a live computation based on the
* actual length of the current value field encoded according to DER
* encoding rules by default. If explicitly set the length field will only
* be returned as is if either the encoded value matches the actual length
* of the value field or if validity checks have been explicitly disabled.
* Otherwise the method will discard the length field and deal with it in
* the same way as if it had never been set.
*
* @return the tlvLength
*/
public TlvLength getTlvLength() {
if(tlvLength == null) {
/* A TLV length field has NOT been explicitly set */
return new TlvLength(getTlvValue().getLength());
} else{
/* A TLV length field has been explicitly set */
int indicatedLength = tlvLength.getIndicatedLength();
if(indicatedLength == getTlvValue().getLength()) {
/* The length indicated by the length field matches the actual length of the value field */
return tlvLength;
} else{
/* The length indicated by the length field does NOT match the actual length of the value field */
if(performValidityChecks) {
/* discard invalid length field */
tlvLength = null;
return new TlvLength(getTlvValue().getLength());
} else{
return tlvLength;
}
}
}
}
/**
* @return the tlvValue
*/
public abstract TlvValue getTlvValue();
@Override
public boolean isValidBerEncoding() {
/* first all T-L-V elements must be of valid encoding for themselves */
if(!(getTlvTag().isValidBerEncoding() && getTlvLength().isValidBerEncoding() && getTlvValue().isValidBerEncoding())) {return false;}
/* then encoded length must match actual length */
if(getTlvLength().getIndicatedLength() != getTlvValue().getLength()) {return false;}
return true;
}
@Override
public boolean isValidDerEncoding() {
if(!isValidBerEncoding()) {return false;}
//TODO provide a negative testcase for this method that checks behavior for a mismatch between length encoded in getTlvLEngth() and actual length of value
return getTlvTag().isValidDerEncoding() && getTlvLength().isValidDerEncoding() && getTlvValue().isValidDerEncoding();
}
/**
* This method matches this tag against a provided tag
* @param otherTlvTag the tlv tag to compare with
* @return whether this tag matches the provided tag
*/
public boolean matches(TlvTag otherTlvTag) {
return tlvTag.matches(otherTlvTag);
}
/**
* This method matches this tag against the tag of a provided TLVDataObject
* @param the TLVDataObject with the tag to be matched against
* @return whether this tag matches the provided tag
*/
public boolean matches(TlvDataObject otherTLVDataObject) {
return matches(otherTLVDataObject.getTlvTag());
}
@Override
public String toString() {
StringBuilder sb;
TlvValue tlvValue;
sb = new StringBuilder();
sb.append(this.tlvTag.toString());
sb.append("|");
sb.append(this.getTlvLength().toString());
tlvValue = this.getTlvValue();
if(!tlvValue.isEmpty()) {
sb.append("|");
sb.append(this.getTlvValue().toString());
}
return sb.toString();
}
@Override
public boolean equals(Object anotherTlvDataObject) {
if(anotherTlvDataObject == null) {return false;}
if (!(anotherTlvDataObject instanceof TlvDataObject)) {
return false;
}
//TlvDataObjects are considered equal iff they encode the same T-L-V combination in the same way
return Arrays.equals(toByteArray(), ((TlvDataObject) anotherTlvDataObject).toByteArray());
}
@Override
public int hashCode() {
int hash = 1;
byte[] byteArray = toByteArray();
for (int i = 0; i < byteArray.length; i++) {
hash *= byteArray[i];
}
return hash;
}
public void setPerformValidityChecksTo(boolean performValidityChecksInput) {
performValidityChecks = performValidityChecksInput;
}
/**
* Returns a neatly indented and line wrapped version of the provided {@link TlvDataObject}
* @param obj
*/
public static String dumpTlvObject(TlvDataObject obj){
if (obj == null) return "";
String inputStr = obj.toString();
StringBuilder sb = new StringBuilder();
String indent = "";
for (int i = 0, n = inputStr.length(); i < n; i++) {
char curChar = inputStr.charAt(i);
switch (curChar) {
case '(':
indent += " ";
break;
case ')':
indent = indent.substring(4);
break;
case '[':
sb.append("\n");
sb.append(indent);
break;
case ']':
break;
default:
sb.append(curChar);
break;
}
}
return sb.toString();
}
}