diff --git a/src/zutil/ArrayUtil.java b/src/zutil/ArrayUtil.java index 681c233..81e55e6 100755 --- a/src/zutil/ArrayUtil.java +++ b/src/zutil/ArrayUtil.java @@ -25,6 +25,7 @@ package zutil; import java.util.List; +import java.util.Objects; /** * 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. * * @return True if the given Object is found inside the array, false otherwise. */ public static boolean contains(T[] array, T obj) { for (final T element : array) - if (element == obj || obj != null && obj.equals(element)) + if (Objects.equals(obj, element)) return true; 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 + */ + public static 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 + */ + 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 + */ + 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; + } } diff --git a/src/zutil/parser/binary/BinaryFieldData.java b/src/zutil/parser/binary/BinaryFieldData.java index b403a04..3b8a18e 100755 --- a/src/zutil/parser/binary/BinaryFieldData.java +++ b/src/zutil/parser/binary/BinaryFieldData.java @@ -32,8 +32,8 @@ import zutil.parser.binary.BinaryStruct.BinaryField; import zutil.parser.binary.BinaryStruct.CustomBinaryField; import zutil.parser.binary.BinaryStruct.VariableLengthBinaryField; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.nio.charset.StandardCharsets; import java.util.*; @@ -53,7 +53,7 @@ public class BinaryFieldData { private Class serializerClass; - protected static List getStructFieldList(Class clazz) { + public static List getStructFieldList(Class clazz) { if (!cache.containsKey(clazz)) { try { ArrayList list = new ArrayList<>(); @@ -198,7 +198,9 @@ public class BinaryFieldData { public BinaryFieldSerializer getSerializer() { try { - return serializerClass.getDeclaredConstructor().newInstance(); + Constructor constructor = serializerClass.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException("Unable to instantiate class: " + serializerClass, e); } diff --git a/src/zutil/parser/binary/BinaryFieldSerializer.java b/src/zutil/parser/binary/BinaryFieldSerializer.java index 09373cc..e504e65 100755 --- a/src/zutil/parser/binary/BinaryFieldSerializer.java +++ b/src/zutil/parser/binary/BinaryFieldSerializer.java @@ -31,17 +31,59 @@ import java.io.OutputStream; /** * An Interface defining a custom field parser and writer. *

- * 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. *

* NOTE: Partial octet serializing not supported. - * */ public interface BinaryFieldSerializer { + /** + * 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, 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, T obj, BinaryFieldData field) throws IOException; diff --git a/src/zutil/parser/binary/BinaryStructInputStream.java b/src/zutil/parser/binary/BinaryStructInputStream.java index 3f68205..31a2890 100755 --- a/src/zutil/parser/binary/BinaryStructInputStream.java +++ b/src/zutil/parser/binary/BinaryStructInputStream.java @@ -41,14 +41,12 @@ import java.util.Map; * * @author Ziver */ -public class BinaryStructInputStream { +public class BinaryStructInputStream extends InputStream{ private InputStream in; private byte data; private int dataBitIndex = -1; - private Map serializerCache = new HashMap<>(); - public BinaryStructInputStream(InputStream in) { this.in = in; @@ -85,13 +83,9 @@ public class BinaryStructInputStream { int totalReadLength = 0; for (BinaryFieldData field : structDataList) { if (field.hasSerializer()) { - BinaryFieldSerializer serializer = serializerCache.get(field.getSerializerClass()); - if (serializer == null) { - serializer = field.getSerializer(); - serializerCache.put(serializer.getClass(), serializer); - } + BinaryFieldSerializer serializer = field.getSerializer(); - Object value = serializer.read(in, field); + Object value = serializer.read(in, field, struct); field.setValue(struct, value); } else { byte[] valueData = new byte[(int) Math.ceil(field.getBitLength(struct) / 8.0)]; @@ -119,21 +113,35 @@ public class BinaryStructInputStream { 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() */ + @Override public boolean markSupported() { return in.markSupported(); } + /** * @see InputStream#mark(int) */ + @Override public void mark(int limit) { in.mark(limit); } /** * @see InputStream#reset() */ + @Override public void reset() throws IOException { in.reset(); } diff --git a/src/zutil/parser/binary/BinaryStructOutputStream.java b/src/zutil/parser/binary/BinaryStructOutputStream.java index ad2a8b7..7248d1c 100755 --- a/src/zutil/parser/binary/BinaryStructOutputStream.java +++ b/src/zutil/parser/binary/BinaryStructOutputStream.java @@ -29,9 +29,7 @@ import zutil.ByteUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.util.HashMap; import java.util.List; -import java.util.Map; /** @@ -42,14 +40,12 @@ import java.util.Map; * * @author Ziver */ -public class BinaryStructOutputStream { +public class BinaryStructOutputStream extends OutputStream { private OutputStream out; private byte rest; private int restBitLength; // length from Most Significant Bit - private Map serializerCache = new HashMap<>(); - public BinaryStructOutputStream(OutputStream out) { this.out = out; @@ -71,16 +67,23 @@ public class BinaryStructOutputStream { return buffer.toByteArray(); } + /** + * @see OutputStream#write(int b) + */ + @Override + public void write(int b) throws IOException { + out.write(b); + } /** * @see OutputStream#write(byte[]) */ - public void write(byte b[]) throws IOException { + public void write(byte[] b) throws IOException { out.write(b); } /** * @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); } @@ -93,14 +96,10 @@ public class BinaryStructOutputStream { for (BinaryFieldData field : structDataList) { if (field.hasSerializer()) { - BinaryFieldSerializer serializer = serializerCache.get(field.getSerializerClass()); - if (serializer == null) { - serializer = field.getSerializer(); - serializerCache.put(serializer.getClass(), serializer); - } + BinaryFieldSerializer serializer = field.getSerializer(); localFlush(); - serializer.write(out, field.getValue(struct), field); + serializer.write(out, field.getValue(struct), field, struct); } else { int fieldBitLength = field.getBitLength(struct); byte[] data = field.getByteValue(struct); diff --git a/src/zutil/parser/binary/serializer/TwoByteLengthPrefixedDataSerializer.java b/src/zutil/parser/binary/serializer/TwoByteLengthPrefixedDataSerializer.java new file mode 100644 index 0000000..138cddf --- /dev/null +++ b/src/zutil/parser/binary/serializer/TwoByteLengthPrefixedDataSerializer.java @@ -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. + *

+ * Currently only these types are supported: + *

    + *
  • byte[]
  • + *
  • String
  • + *
+ */ +public class TwoByteLengthPrefixedDataSerializer implements BinaryFieldSerializer { + + @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); + } +} diff --git a/test/zutil/ArrayUtilTest.java b/test/zutil/ArrayUtilTest.java new file mode 100644 index 0000000..6b47045 --- /dev/null +++ b/test/zutil/ArrayUtilTest.java @@ -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})); + } +} \ No newline at end of file diff --git a/test/zutil/parser/binary/serializer/TwoByteLengthPrefixedDataSerializerTest.java b/test/zutil/parser/binary/serializer/TwoByteLengthPrefixedDataSerializerTest.java new file mode 100644 index 0000000..5a41756 --- /dev/null +++ b/test/zutil/parser/binary/serializer/TwoByteLengthPrefixedDataSerializerTest.java @@ -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 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 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()); + } +} \ No newline at end of file