MagicNumber.java
package emissary.util.magic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Objects;
import javax.annotation.Nullable;
public class MagicNumber {
private static final Logger log = LoggerFactory.getLogger(MagicNumber.class);
/** The default charset used when loading the config file and when sampling data */
public static final Charset DEFAULT_CHARSET = StandardCharsets.ISO_8859_1;
/** Byte data type */
public static final String TYPE_KEY_BYTE = "BYTE"; //
/** Short data type */
public static final String TYPE_KEY_SHORT = "SHORT"; //
/** Long data type */
public static final String TYPE_KEY_LONG = "LONG"; //
/** String data type */
public static final String TYPE_KEY_STRING = "STRING"; //
/** Date data type */
public static final String TYPE_KEY_DATE = "DATE"; // long integer - seconds since epoch
/** Big endian short data type */
public static final String TYPE_KEY_BESHORT = "BESHORT"; // big-endian 16-bit
/** Big endian long data type */
public static final String TYPE_KEY_BELONG = "BELONG"; // big-endian 32-bit
/** Big endian date data type */
public static final String TYPE_KEY_BEDATE = "BEDATE"; // big-endian 32-bit date
/** Little endian short data type */
public static final String TYPE_KEY_LESHORT = "LESHORT"; // little-end 16-bit
/** Little endian long data type */
public static final String TYPE_KEY_LELONG = "LELONG"; // little-end 32-bit
/** Little endian long data type */
public static final String TYPE_KEY_LEDATE = "LEDATE"; // little-end 32-bit date
/** Unknown data type id */
public static final int TYPE_UNKNOWN = -1;
/** Byte data type id */
public static final int TYPE_BYTE = 0;
/** Short data type id */
public static final int TYPE_SHORT = 1;
/** Long data type id */
public static final int TYPE_LONG = 2;
/** String data type id */
public static final int TYPE_STRING = 3;
/** Date data type id */
public static final int TYPE_DATE = 4;
/** Big endian short data type id */
public static final int TYPE_BESHORT = 5;
/** Big endian long data type id */
public static final int TYPE_BELONG = 6;
/** Big endian date data type id */
public static final int TYPE_BEDATE = 7;
/** Little endian short data type id */
public static final int TYPE_LESHORT = 8;
/** Little endian long data type id */
public static final int TYPE_LELONG = 9;
/** Little endian date data type id */
public static final int TYPE_LEDATE = 10;
/** Empty string constant */
public static final String EMPTYSTRING = "";
/** Unary Operator: Equals */
public static final char MAGICOPERATOR_AND = '=';
/** Unary Operator: Greater than */
public static final char MAGICOPERATOR_GTHAN = '>';
/** Unary Operator: Less than */
public static final char MAGICOPERATOR_LTHAN = '<';
/** Unary Operator: At least one bit matches */
public static final char MAGICOPERATOR_OR = 'x';
/** Unary Operator: All bits match */
public static final char MAGICOPERATOR_BWAND = '&';
/** Unary Operator: None or some bits match */
public static final char MAGICOPERATOR_BWNOT = '^';
/** Unary Operator: Default Operator (AND) */
public static final char MAGICOPERATOR_NOT = '!';
/** Unary Operator: Greater than or equal to */
public static final char MAGICOPERATOR_EQUAL_GTHAN = ']';
/** Unary operator: Less than or equal to */
public static final char MAGICOPERATOR_EQUAL_LTHAN = '[';
/** Default Unary Operator - and */
public static final char MAGICOPERATOR_DEFAULT = MAGICOPERATOR_AND;
// Column A Properties
protected int depth;
protected int offset = -1;
protected char offsetUnary = 0;
// Column B Properties
protected int dataType = -1;
protected int dataTypeLength = 0;
protected byte[] mask;
protected boolean signedValue;
// Column C Properties
protected char unaryOperator;
@Nullable
protected byte[] value = null;
protected boolean substitute = false;
// Column D Properties
@Nullable
protected String description = null;
// Magic Number Properties
protected List<MagicNumber[]> dependencies;
/**
* Recreates the string entry for this magic number plus its child continuations under new lines preceded by a '>'
* character at the appropriate depth.
*
* @return String
*/
public String toStringAll() {
return toString(null, 0);
}
/**
* Tests the sample and if successful provides the description
*/
@Nullable
public String describe(byte[] data) {
String desc = describeSelf(data);
if (desc == null) {
return null;
}
StringBuilder sb = new StringBuilder(desc);
return escapeBackspace(describeDependents(data, sb, 0));
}
/**
* Private formatting method for escaping backspaces
*/
private static String escapeBackspace(String desc) {
StringBuilder s = new StringBuilder();
for (int i = 0; i < desc.length(); i++) {
if (desc.charAt(i) == '\\' && (i + 1) < desc.length() && desc.charAt(i + 1) == 'b') {
s = new StringBuilder(s.substring(0, s.length() - 1));
i++;
continue;
}
s.append(desc.charAt(i));
}
return s.toString();
}
/**
* Describe this instance only
*/
@Nullable
private String describeSelf(byte[] data) {
if (!test(data)) {
return null;
}
return format(description, data);
}
/**
* Private method to format output - mainly for description substitutions
*/
private String format(String desc, byte[] data) {
if (!substitute) {
return desc;
}
Deque<Character> chars = new ArrayDeque<>();
for (int i = (desc.length() - 1); i >= 0; --i) {
chars.push(desc.charAt(i));
}
StringBuilder sb = new StringBuilder();
while (!chars.isEmpty()) {
Character next = chars.pop();
if (!chars.isEmpty() && next == '%') {
char subType = chars.pop();
if (dataType == TYPE_STRING) {
if (offset < (data.length - 2)) {
String sub = new String(Objects.requireNonNull(getElement(data, offset, 1)), DEFAULT_CHARSET);
sb.append(sub);
}
} else if (subType == 'c' || subType == 's') {
byte[] subData = getElement(data, offset, dataTypeLength);
if (subData != null) {
String sub = new String(subData, DEFAULT_CHARSET);
sb.append(sub);
}
} else {
byte[] subData = getElement(data, offset, dataTypeLength);
if (subData != null) {
String sub = MagicMath.byteArrayToString(subData, 10);
sb.append(sub);
}
}
if (subType == 'l' && !chars.isEmpty() && chars.peek() == 'd') {
chars.pop();
}
continue;
}
sb.append(next.charValue());
}
return sb.toString();
}
/**
* Tests dependent children
*/
private String describeDependents(byte[] data, StringBuilder sb, int layer) {
log.debug("DESCRIBING DEPENDENTS at layer {}", layer);
if (dependencies == null || layer >= dependencies.size()) {
log.debug("Not enough dependents for layer {}", layer);
return sb.toString();
}
boolean shouldContinue = false;
MagicNumber[] dependentItems = dependencies.get(layer);
log.debug("Found {} items at layer {}", dependentItems.length, layer);
for (MagicNumber dependentItem : dependentItems) {
String s = dependentItem.describeSelf(data);
if (s != null) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(s);
shouldContinue = true;
}
}
if (!shouldContinue) {
return sb.toString();
}
return describeDependents(data, sb, layer + 1);
}
/**
* Tests this magic number against the given data
*/
public boolean test(byte[] data) {
byte[] subject = getElement(data, offset, dataTypeLength);
if (subject == null) {
return false;
}
return testNumeric(subject);
}
/**
* Tests numeric byte data only
*/
private boolean testNumeric(byte[] data) {
if (substitute) {
return true;
}
byte[] mValues = value;
log.debug("Unary Operator: {}", unaryOperator);
int end = mValues.length;
switch (unaryOperator) {
case MAGICOPERATOR_AND:
case MAGICOPERATOR_BWAND:
for (int i = 0; i < end; i++) {
if (data[i] != mValues[i]) {
return false;
}
}
return true;
case MAGICOPERATOR_GTHAN:
for (int i = 0; i < end; i++) {
if ((data[i] & 0xFF) < (mValues[i] & 0xFF)) {
return false;
}
if (i == end - 1 && data[i] == mValues[i]) {
return false;
}
}
return true;
case MAGICOPERATOR_LTHAN:
for (int i = 0; i < end; i++) {
if ((data[i] & 0xFF) > (mValues[i] & 0xFF)) {
return false;
}
if (i == end - 1 && data[i] == mValues[i]) {
return false;
}
}
return true;
case MAGICOPERATOR_OR:
for (int i = 0; i < end; i++) {
if (data[i] == mValues[i]) {
return true;
}
}
return false;
case MAGICOPERATOR_BWNOT:
case MAGICOPERATOR_NOT:
for (int i = 0; i < end; i++) {
if (data[i] != mValues[i]) {
return true;
}
}
return false;
case MAGICOPERATOR_EQUAL_GTHAN:
for (int i = 0; i < end; i++) {
if ((data[i] & 0xFF) < (mValues[i] & 0xFF)) {
return false;
}
}
return true;
case MAGICOPERATOR_EQUAL_LTHAN:
for (int i = 0; i < end; i++) {
if ((data[i] & 0xFF) > (mValues[i] & 0xFF)) {
return false;
}
}
return true;
default:
throw new IllegalStateException(
"This MagicNumber instance is configured incorrectly. The unary operator is set to an unknown or unconfigured value.");
}
}
/**
* Retrieves the data sample
*/
@Nullable
private static byte[] getElement(@Nullable byte[] data, int offset, int length) {
if (data == null) {
return null;
}
if (data.length < (offset + length)) {
return null;
}
byte[] subject = new byte[length];
System.arraycopy(data, offset, subject, 0, subject.length);
return subject;
}
/**
* Add child continuations
*/
@SuppressWarnings("AvoidObjectArrays")
public void addDependencyLayer(MagicNumber[] dependencyLayer) {
if (dependencies == null) {
dependencies = new ArrayList<>();
}
dependencies.add(dependencyLayer);
}
/**
* Re-creates the string magic number entry for this number only
*
* @return a String represention of the entry
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(">".repeat(Math.max(0, depth)));
if (offsetUnary > 0) {
sb.append(offsetUnary);
}
if (offset == 0) {
sb.append("0");
} else {
sb.append(MagicMath.HEX_PREFIX);
sb.append(Integer.toHexString(offset));
}
sb.append('\t');
sb.append(MagicNumberFactory.resolveReverseDataType(dataType));
if (mask != null && mask.length > 0) {
sb.append('&');
sb.append(MagicMath.byteArrayToHexString(mask));
}
sb.append('\t');
if (unaryOperator == MAGICOPERATOR_EQUAL_LTHAN) {
sb.append("<=");
} else if (unaryOperator == MAGICOPERATOR_EQUAL_GTHAN) {
sb.append(">=");
} else {
sb.append(unaryOperator);
}
sb.append(MagicMath.byteArrayToHexString(value));
sb.append('\t');
sb.append(description);
return sb.toString();
}
/**
* Private method to create the string plus continuations
*/
private String toString(@Nullable StringBuilder sbuf, int depth) {
StringBuilder sb = sbuf;
int d = depth;
if (sb == null) {
sb = new StringBuilder(description);
}
if (dependencies == null || d >= dependencies.size()) {
return sb.toString();
}
MagicNumber[] dependentItems = dependencies.get(d);
for (MagicNumber dependentItem : dependentItems) {
sb.append('\n');
sb.append(dependentItem.toString());
}
return toString(sb, d + 1);
}
}