Implemented possibility to read parent object while writing custom binary field

This commit is contained in:
Ziver Koc 2023-03-09 23:11:52 +01:00
parent 3514a58c40
commit fcbb2ef227
8 changed files with 372 additions and 29 deletions

View file

@ -25,6 +25,7 @@
package zutil; package zutil;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* A utility class containing Array specific utility methods * A utility class containing Array specific utility methods
@ -45,15 +46,87 @@ public class ArrayUtil {
} }
/** /**
* Searches for a given object inside of an array. * Searches for a given object inside an array.
* The method uses reference comparison or {@link #equals(Object)} to check for equality. * The method uses reference comparison or {@link #equals(Object)} to check for equality.
* *
* @return True if the given Object is found inside the array, false otherwise. * @return True if the given Object is found inside the array, false otherwise.
*/ */
public static <T> boolean contains(T[] array, T obj) { public static <T> boolean contains(T[] array, T obj) {
for (final T element : array) for (final T element : array)
if (element == obj || obj != null && obj.equals(element)) if (Objects.equals(obj, element))
return true; return true;
return false; return false;
} }
/**
* Combines multiple arras into one.
*
* @param arrays the arrays to be combined.
* @return one array containing all the elements of the provided arrays.
* @param <T>
*/
public static <T> T[] combine(T[]... arrays) {
int totalLength = 0;
for (T[] array : arrays) {
totalLength += array.length;
}
int outputPos = 0;
Object[] output = new Object[totalLength];
for (T[] array : arrays) {
System.arraycopy(array, 0, output, outputPos, array.length);
outputPos += array.length;
}
return (T[]) output;
}
/**
* Combines multiple arras into one.
*
* @param arrays the arrays to be combined.
* @return one array containing all the elements of the provided arrays.
* @param <T>
*/
public static int[] combine(int[]... arrays) {
int totalLength = 0;
for (int[] array : arrays) {
totalLength += array.length;
}
int outputPos = 0;
int[] output = new int[totalLength];
for (int[] array : arrays) {
System.arraycopy(array, 0, output, outputPos, array.length);
outputPos += array.length;
}
return output;
}
/**
* Combines multiple arras into one.
*
* @param arrays the arrays to be combined.
* @return one array containing all the elements of the provided arrays.
* @param <T>
*/
public static byte[] combine(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
int outputPos = 0;
byte[] output = new byte[totalLength];
for (byte[] array : arrays) {
System.arraycopy(array, 0, output, outputPos, array.length);
outputPos += array.length;
}
return output;
}
} }

View file

@ -32,8 +32,8 @@ import zutil.parser.binary.BinaryStruct.BinaryField;
import zutil.parser.binary.BinaryStruct.CustomBinaryField; import zutil.parser.binary.BinaryStruct.CustomBinaryField;
import zutil.parser.binary.BinaryStruct.VariableLengthBinaryField; import zutil.parser.binary.BinaryStruct.VariableLengthBinaryField;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
@ -53,7 +53,7 @@ public class BinaryFieldData {
private Class<? extends BinaryFieldSerializer> serializerClass; private Class<? extends BinaryFieldSerializer> serializerClass;
protected static List<BinaryFieldData> getStructFieldList(Class<? extends BinaryStruct> clazz) { public static List<BinaryFieldData> getStructFieldList(Class<? extends BinaryStruct> clazz) {
if (!cache.containsKey(clazz)) { if (!cache.containsKey(clazz)) {
try { try {
ArrayList<BinaryFieldData> list = new ArrayList<>(); ArrayList<BinaryFieldData> list = new ArrayList<>();
@ -198,7 +198,9 @@ public class BinaryFieldData {
public BinaryFieldSerializer getSerializer() { public BinaryFieldSerializer getSerializer() {
try { try {
return serializerClass.getDeclaredConstructor().newInstance(); Constructor<? extends BinaryFieldSerializer> constructor = serializerClass.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (ReflectiveOperationException e) { } catch (ReflectiveOperationException e) {
throw new RuntimeException("Unable to instantiate class: " + serializerClass, e); throw new RuntimeException("Unable to instantiate class: " + serializerClass, e);
} }

View file

@ -31,17 +31,59 @@ import java.io.OutputStream;
/** /**
* An Interface defining a custom field parser and writer. * An Interface defining a custom field parser and writer.
* <p> * <p>
* One singleton instance of the serializer will be instantiated for the lifetime of the * A new instance of the serializer will be instantiated for every time serialization is required.
* {@link BinaryStructInputStream} and {@link BinaryStructOutputStream} objects. * {@link BinaryStructInputStream} and {@link BinaryStructOutputStream} objects.
* <p> * <p>
* NOTE: Partial octet serializing not supported. * NOTE: Partial octet serializing not supported.
*
*/ */
public interface BinaryFieldSerializer<T> { public interface BinaryFieldSerializer<T> {
/**
* Read the given field from the stream.
*
* @param in the stream where the data should be read from.
* @param field meta-data about the target field that will be assigned.
* @param parentObject the parent object that owns the field.
* @return the value that should be assigned to the field.
*/
default T read(InputStream in,
BinaryFieldData field,
Object parentObject) throws IOException {
return read(in, field);
}
/**
* Read the given field from the stream.
*
* @param in the stream where the data should be read from.
* @param field meta-data about the target field that will be assigned.
* @return the value that should be assigned to the field.
*/
T read(InputStream in, T read(InputStream in,
BinaryFieldData field) throws IOException; BinaryFieldData field) throws IOException;
/**
* Write the given field to the output stream.
*
* @param out the stream where the field data should be written to.
* @param obj the object that should be serialized and written to the stream.
* @param field meta-data about the source field that will be serialized.
* @param parentObject the parent object that owns the field.
*/
default void write(OutputStream out,
T obj,
BinaryFieldData field,
Object parentObject) throws IOException {
write(out, obj, field);
}
/**
* Write the given field to the output stream.
*
* @param out the stream where the field data should be written to.
* @param obj the object that should be serialized and written to the stream.
* @param field meta-data about the source field that will be serialized.
*/
void write(OutputStream out, void write(OutputStream out,
T obj, T obj,
BinaryFieldData field) throws IOException; BinaryFieldData field) throws IOException;

View file

@ -41,14 +41,12 @@ import java.util.Map;
* *
* @author Ziver * @author Ziver
*/ */
public class BinaryStructInputStream { public class BinaryStructInputStream extends InputStream{
private InputStream in; private InputStream in;
private byte data; private byte data;
private int dataBitIndex = -1; private int dataBitIndex = -1;
private Map<Class, BinaryFieldSerializer> serializerCache = new HashMap<>();
public BinaryStructInputStream(InputStream in) { public BinaryStructInputStream(InputStream in) {
this.in = in; this.in = in;
@ -85,13 +83,9 @@ public class BinaryStructInputStream {
int totalReadLength = 0; int totalReadLength = 0;
for (BinaryFieldData field : structDataList) { for (BinaryFieldData field : structDataList) {
if (field.hasSerializer()) { if (field.hasSerializer()) {
BinaryFieldSerializer serializer = serializerCache.get(field.getSerializerClass()); BinaryFieldSerializer<Object> serializer = field.getSerializer();
if (serializer == null) {
serializer = field.getSerializer();
serializerCache.put(serializer.getClass(), serializer);
}
Object value = serializer.read(in, field); Object value = serializer.read(in, field, struct);
field.setValue(struct, value); field.setValue(struct, value);
} else { } else {
byte[] valueData = new byte[(int) Math.ceil(field.getBitLength(struct) / 8.0)]; byte[] valueData = new byte[(int) Math.ceil(field.getBitLength(struct) / 8.0)];
@ -119,21 +113,35 @@ public class BinaryStructInputStream {
return totalReadLength; return totalReadLength;
} }
@Override
public int read() throws IOException {
return in.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
/** /**
* @see InputStream#markSupported() * @see InputStream#markSupported()
*/ */
@Override
public boolean markSupported() { public boolean markSupported() {
return in.markSupported(); return in.markSupported();
} }
/** /**
* @see InputStream#mark(int) * @see InputStream#mark(int)
*/ */
@Override
public void mark(int limit) { public void mark(int limit) {
in.mark(limit); in.mark(limit);
} }
/** /**
* @see InputStream#reset() * @see InputStream#reset()
*/ */
@Override
public void reset() throws IOException { public void reset() throws IOException {
in.reset(); in.reset();
} }

View file

@ -29,9 +29,7 @@ import zutil.ByteUtil;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
@ -42,14 +40,12 @@ import java.util.Map;
* *
* @author Ziver * @author Ziver
*/ */
public class BinaryStructOutputStream { public class BinaryStructOutputStream extends OutputStream {
private OutputStream out; private OutputStream out;
private byte rest; private byte rest;
private int restBitLength; // length from Most Significant Bit private int restBitLength; // length from Most Significant Bit
private Map<Class, BinaryFieldSerializer> serializerCache = new HashMap<>();
public BinaryStructOutputStream(OutputStream out) { public BinaryStructOutputStream(OutputStream out) {
this.out = out; this.out = out;
@ -71,16 +67,23 @@ public class BinaryStructOutputStream {
return buffer.toByteArray(); return buffer.toByteArray();
} }
/**
* @see OutputStream#write(int b)
*/
@Override
public void write(int b) throws IOException {
out.write(b);
}
/** /**
* @see OutputStream#write(byte[]) * @see OutputStream#write(byte[])
*/ */
public void write(byte b[]) throws IOException { public void write(byte[] b) throws IOException {
out.write(b); out.write(b);
} }
/** /**
* @see OutputStream#write(byte[], int, int) * @see OutputStream#write(byte[], int, int)
*/ */
public void write(byte b[], int off, int len) throws IOException { public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len); out.write(b, off, len);
} }
@ -93,14 +96,10 @@ public class BinaryStructOutputStream {
for (BinaryFieldData field : structDataList) { for (BinaryFieldData field : structDataList) {
if (field.hasSerializer()) { if (field.hasSerializer()) {
BinaryFieldSerializer serializer = serializerCache.get(field.getSerializerClass()); BinaryFieldSerializer<Object> serializer = field.getSerializer();
if (serializer == null) {
serializer = field.getSerializer();
serializerCache.put(serializer.getClass(), serializer);
}
localFlush(); localFlush();
serializer.write(out, field.getValue(struct), field); serializer.write(out, field.getValue(struct), field, struct);
} else { } else {
int fieldBitLength = field.getBitLength(struct); int fieldBitLength = field.getBitLength(struct);
byte[] data = field.getByteValue(struct); byte[] data = field.getByteValue(struct);

View file

@ -0,0 +1,84 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2020 Ziver Koc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package zutil.parser.binary.serializer;
import zutil.parser.binary.BinaryFieldData;
import zutil.parser.binary.BinaryFieldSerializer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StreamCorruptedException;
import java.nio.charset.StandardCharsets;
/**
* Serializer handles data that is prefixed by two byte length.
* <p>
* Currently only these types are supported:
* <ul>
* <li>byte[]</li>
* <li>String</li>
* </ul>
*/
public class TwoByteLengthPrefixedDataSerializer implements BinaryFieldSerializer<Object> {
@Override
public Object read(InputStream in, BinaryFieldData field) throws IOException {
int b = in.read();
if (b < 0)
throw new StreamCorruptedException("Stream ended prematurely when reading first length byte.");
int length = (b & 0xFF) << 8;
b = in.read();
if (b < 0)
throw new StreamCorruptedException("Stream ended prematurely when reading second length byte.");
length |= b & 0xFF;
byte[] payload = new byte[length];
in.read(payload);
if (field.getType().isAssignableFrom(String.class))
return new String(payload, StandardCharsets.UTF_8);
return payload;
}
@Override
public void write(OutputStream out, Object obj, BinaryFieldData field) throws IOException {
if (obj == null)
return;
byte[] payload;
if (obj instanceof String)
payload = ((String) obj).getBytes(StandardCharsets.UTF_8);
else
payload = (byte[]) obj;
int length = payload.length;
out.write((length & 0xFF00) >> 8);
out.write(length & 0xFF);
out.write(payload);
}
}

View file

@ -0,0 +1,39 @@
package zutil;
import org.junit.Test;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.Assert.*;
public class ArrayUtilTest {
@Test
public void toIntArray() {
assertArrayEquals(new int[]{}, ArrayUtil.toIntArray(Collections.emptyList()));
assertArrayEquals(new int[]{1, 2, 3}, ArrayUtil.toIntArray(Arrays.asList(1, 2, 3)));
}
@Test
public void contains() {
assertFalse(ArrayUtil.contains(new Integer[]{}, 1));
assertTrue(ArrayUtil.contains(new Integer[]{1}, 1));
assertTrue(ArrayUtil.contains(new Integer[]{2, 1, 3}, 1));
}
@Test
public void combine() {
assertArrayEquals(new Integer[]{}, ArrayUtil.combine(new Integer[]{}, new Integer[]{}));
assertArrayEquals(new Integer[]{1, 2}, ArrayUtil.combine(new Integer[]{1, 2}, new Integer[]{}));
assertArrayEquals(new Integer[]{1, 2, 3, 4}, ArrayUtil.combine(new Integer[]{1, 2}, new Integer[]{3, 4}));
assertArrayEquals(new int[]{}, ArrayUtil.combine(new int[]{}, new int[]{}));
assertArrayEquals(new int[]{1, 2}, ArrayUtil.combine(new int[]{1, 2}, new int[]{}));
assertArrayEquals(new int[]{1, 2, 3, 4}, ArrayUtil.combine(new int[]{1, 2}, new int[]{3, 4}));
assertArrayEquals(new byte[]{}, ArrayUtil.combine(new byte[]{}, new byte[]{}));
assertArrayEquals(new byte[]{1, 2}, ArrayUtil.combine(new byte[]{1, 2}, new byte[]{}));
assertArrayEquals(new byte[]{1, 2, 3, 4}, ArrayUtil.combine(new byte[]{1, 2}, new byte[]{3, 4}));
}
}

View file

@ -0,0 +1,96 @@
package zutil.parser.binary.serializer;
import org.junit.Test;
import zutil.ArrayUtil;
import zutil.ByteUtil;
import zutil.parser.binary.BinaryFieldData;
import zutil.parser.binary.BinaryStruct;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StreamCorruptedException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import static org.junit.Assert.*;
public class TwoByteLengthPrefixedDataSerializerTest implements BinaryStruct {
@BinaryField(index = 10, length = 1)
private String tmpStringField;
@BinaryField(index = 20, length = 1)
private byte[] tmpByteField;
@Test(expected = StreamCorruptedException.class)
public void readPrematureEnd0() throws IOException {
TwoByteLengthPrefixedDataSerializer serializer = new TwoByteLengthPrefixedDataSerializer();
// 0 length stream
ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[0]);
serializer.read(inputStream, null, this);
}
@Test(expected = StreamCorruptedException.class)
public void readPrematureEnd1() throws IOException {
TwoByteLengthPrefixedDataSerializer serializer = new TwoByteLengthPrefixedDataSerializer();
// 1 length stream
ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[]{0x00});
serializer.read(inputStream, null, this);
}
@Test
public void read() throws IOException, NoSuchFieldException {
TwoByteLengthPrefixedDataSerializer serializer = new TwoByteLengthPrefixedDataSerializer();
List<BinaryFieldData> fieldDataList = BinaryFieldData.getStructFieldList(this.getClass());
BinaryFieldData stringFieldData = fieldDataList.get(0);
BinaryFieldData byteFieldData = fieldDataList.get(1);
// 0 Length
ByteArrayInputStream inputStream = new ByteArrayInputStream(new byte[]{0x00, 0x00});
assertEquals("", serializer.read(inputStream, stringFieldData, this));
inputStream.reset();
assertArrayEquals(new byte[0], (byte[])serializer.read(inputStream, byteFieldData, this));
// String "1234"
inputStream = new ByteArrayInputStream(ArrayUtil.combine(new byte[]{0x00, 0x04}, "1234".getBytes(StandardCharsets.UTF_8)));
assertEquals("1234", serializer.read(inputStream, stringFieldData, this));
inputStream.reset();
assertArrayEquals("1234".getBytes(StandardCharsets.UTF_8), (byte[])serializer.read(inputStream, byteFieldData, this));
}
@Test
public void write() throws IOException {
TwoByteLengthPrefixedDataSerializer serializer = new TwoByteLengthPrefixedDataSerializer();
List<BinaryFieldData> fieldDataList = BinaryFieldData.getStructFieldList(this.getClass());
BinaryFieldData stringFieldData = fieldDataList.get(0);
BinaryFieldData byteFieldData = fieldDataList.get(1);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 0 Length
outputStream.reset();outputStream.reset();
serializer.write(outputStream, null, stringFieldData, this);
assertArrayEquals(new byte[]{}, outputStream.toByteArray());
outputStream.reset();
serializer.write(outputStream, null, byteFieldData, this);
assertArrayEquals(new byte[]{}, outputStream.toByteArray());
// 0 Length
outputStream.reset();
serializer.write(outputStream, "", stringFieldData, this);
assertArrayEquals(new byte[]{0, 0}, outputStream.toByteArray());
outputStream.reset();
serializer.write(outputStream, new byte[0], byteFieldData, this);
assertArrayEquals(new byte[]{0, 0}, outputStream.toByteArray());
// String "1234"
outputStream.reset();
serializer.write(outputStream, "1234", stringFieldData, this);
assertArrayEquals(new byte[]{0, 4, 49, 50, 51, 52}, outputStream.toByteArray());
outputStream.reset();
serializer.write(outputStream, "1234".getBytes(StandardCharsets.UTF_8), byteFieldData, this);
assertArrayEquals(new byte[]{0, 4, 49, 50, 51, 52}, outputStream.toByteArray());
}
}