diff --git a/build.gradle b/build.gradle index 45c59d128..ec3447fa3 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ apply plugin: 'java' group = 'network.casper' // Version number update for release -version='2.7.0-BETA.2' +version='2.7.0-BETA.3' sourceCompatibility = 1.8 targetCompatibility = 1.8 @@ -24,7 +24,6 @@ repositories { } dependencies { - implementation "dev.oak3:sbs4j:${sbs4jVersion}" implementation "io.github.oak:jsonrpc4j:${jsonrpc4jVersion}" implementation "org.bouncycastle:bcpkix-jdk15on:${bouncyCastleVersion}" implementation "org.web3j:core:${web3jVersion}" diff --git a/src/main/java/com/casper/sdk/model/transaction/field/CalltableSerializationEnvelopeBuilder.java b/src/main/java/com/casper/sdk/model/transaction/field/CalltableSerializationEnvelopeBuilder.java new file mode 100644 index 000000000..6fe3b927e --- /dev/null +++ b/src/main/java/com/casper/sdk/model/transaction/field/CalltableSerializationEnvelopeBuilder.java @@ -0,0 +1,150 @@ +package com.casper.sdk.model.transaction.field; + +import com.casper.sdk.exception.NoSuchTypeException; +import com.casper.sdk.model.clvalue.serde.CasperSerializableObject; +import com.casper.sdk.model.clvalue.serde.Target; +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.exception.ValueSerializationException; +import dev.oak3.sbs4j.interfaces.DeserializableObject; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * The call table serialization envelope builder used to serialize and deserialize transaction fields/ + * + * @author ian@meywood.com + */ +@AllArgsConstructor +@NoArgsConstructor +@Setter +public class CalltableSerializationEnvelopeBuilder implements CasperSerializableObject, DeserializableObject { + + /** The fields to serialize */ + private List fields = new ArrayList<>(); + /** The current field index */ + private long currentFieldIndex = -1; + /** The expected number of fields */ + private long expectedFields; + /** The offset of the current field om the serialized bytes */ + private long offset = 0; + /** The total size of all field values when serialized */ + private long size = 0; + + /** + * Add a field to the envelope. + * + * @param index the zero based index + * @param value the value to be serialized to value bytes of the field + */ + public void addField(final int index, final T value) throws ValueSerializationException { + this.addField(new Field(index, this.offset, value)); + } + + /** + * Add a field to the envelope. + * + * @param index the zero based index + * @param value the bytes of the value to add + */ + public void addFieldBytes(final int index, final byte[] value) { + + this.addField(new Field((short) index, offset, value)); + } + + /** + * Adds a field to the envelope. + * + * @param field the field to add + */ + public void addField(final Field field) { + if (this.currentFieldIndex >= field.getIndex()) { + throw new IllegalArgumentException("Field index must be greater than the previous field index"); + } + + if (this.currentFieldIndex + 1 != field.getIndex()) { + throw new IllegalArgumentException("Field index must be sequential"); + } + + this.fields.add(field); + this.currentFieldIndex = field.getIndex(); + this.size += field.getValue().length; + this.offset += field.getValue().length; + } + + /** + * Obtains the fields and converts its byte value to the specified type. + * + * @param index the index of the field + * @param clazz the tye to convert the field value to + * @param the type to convert the field value to + * @return the field value as the specified type + * @throws ValueDeserializationException if the field value cannot be converted to the specified type + */ + public T getFieldValue(final int index, final Class clazz) throws ValueDeserializationException { + final Field field = this.fields.get(index); + return field.getValue(clazz); + } + + /** + * Obtains the field's value bytes. + * + * @param index the index of the field + * @return the fields value bytes + */ + public byte[] getFieldBytes(final int index) { + return fields.get(index).getValue(); + } + + @Override + public void serialize(final SerializerBuffer ser, final Target target) throws ValueSerializationException, NoSuchTypeException { + + if (this.fields.size() != this.expectedFields) { + throw new IllegalArgumentException("Field index must be expected length " + this.expectedFields); + } + + // Write the number of fields + ser.writeI32(this.fields.size()); + + for (final Field field : this.fields) { + field.serialize(ser, target); + } + + // Write total bytes of all field values + ser.writeU32(this.size); + for (final Field field : this.fields) { + ser.writeByteArray(field.getValue()); + } + } + + @Override + public void deserialize(final DeserializerBuffer deserializerBuffer) throws ValueDeserializationException { + + final long numFields = deserializerBuffer.readU32(); + + for (int i = 0; i < numFields; i++) { + final Field field = new Field(); + field.deserialize(deserializerBuffer); + this.fields.add(field); + } + + this.size = deserializerBuffer.readU32(); + for (Field field : this.fields) { + final long fieldValueLen = getFieldLength(field); + field.setValue(deserializerBuffer.readByteArray((int) fieldValueLen)); + } + } + + private long getFieldLength(final Field field) { + if (field.getIndex() == this.fields.size() - 1) { + return size - field.getOffset(); + } else { + return this.fields.get(field.getIndex() + 1).getOffset() - field.getOffset(); + } + } +} diff --git a/src/main/java/com/casper/sdk/model/transaction/field/Field.java b/src/main/java/com/casper/sdk/model/transaction/field/Field.java new file mode 100644 index 000000000..34e40fe5a --- /dev/null +++ b/src/main/java/com/casper/sdk/model/transaction/field/Field.java @@ -0,0 +1,122 @@ +package com.casper.sdk.model.transaction.field; + +import com.casper.sdk.exception.NoSuchTypeException; +import com.casper.sdk.model.clvalue.serde.CasperSerializableObject; +import com.casper.sdk.model.clvalue.serde.Target; +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.exception.ValueSerializationException; +import dev.oak3.sbs4j.interfaces.DeserializableObject; +import dev.oak3.sbs4j.interfaces.SerializableObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * An indexed field with an offset and value. + * + * @author ian@meywood.com + */ +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class Field implements CasperSerializableObject, DeserializableObject { + + /** The field index */ + private short index; + /** The offset of the field's value when written to bytes */ + private long offset; + /** The field value as bytes */ + private byte[] value; + + /** + * Constructs a field with the specified index, offset and value. + * + * @param index the index of the field + * @param offset the offset of the fields bytes within a CalltableSerializationEnvelope + * @param value the value of the field + * @throws ValueSerializationException if the value cannot be serialized + */ + public Field(final int index, final long offset, final Object value) throws ValueSerializationException { + final SerializerBuffer serializerBuffer = new SerializerBuffer(); + + if (value instanceof SerializableObject) { + ((SerializableObject) value).serialize(serializerBuffer); + } else if (value instanceof byte[]) { + serializerBuffer.writeByteArray((byte[]) value); + } else if (value.getClass().equals(Byte.class)) { + serializerBuffer.writeU8((Byte) value); + } else if (value.getClass().equals(Long.class)) { + serializerBuffer.writeI64((Long) value); + } else if (value.getClass().equals(Integer.class)) { + serializerBuffer.writeI32((Integer) value); + } else if (value.getClass().equals(Short.class)) { + serializerBuffer.writeU16((Short) value); + } else if (value.getClass().equals(String.class)) { + serializerBuffer.writeString((String) value); + } else { + throw new ValueSerializationException("Unsupported type " + value.getClass().getName()); + } + + this.index = (short) index; + this.offset = offset; + this.value = serializerBuffer.toByteArray(); + } + + @Override + public void serialize(final SerializerBuffer ser, final Target target) throws ValueSerializationException, NoSuchTypeException { + this.serialize(ser); + } + + @Override + public void serialize(final SerializerBuffer ser) throws ValueSerializationException { + ser.writeU16(index); + ser.writeU32(offset); + } + + @Override + public void deserialize(final DeserializerBuffer deser) throws ValueDeserializationException { + this.index = deser.readU16(); + this.offset = deser.readU32(); + } + + /** + * Obtains the fields value converted to the specified type. + * + * @param clazz the type to convert the value's bytes to + * @param the expected type + * @return the bytes converted to value of type clazz + * @throws ValueDeserializationException if the value cannot be converted to the specified type + */ + @SuppressWarnings("unchecked") + public T getValue(final Class clazz) throws ValueDeserializationException { + final DeserializerBuffer deserializerBuffer = new DeserializerBuffer(this.value); + + if (clazz.isAssignableFrom(DeserializableObject.class)) { + try { + final T value = clazz.getDeclaredConstructor().newInstance(); + ((DeserializableObject) value).deserialize(deserializerBuffer); + return value; + } catch (Exception e) { + throw new ValueDeserializationException("Unsupported type " + clazz.getName(), e); + } + } else if (clazz == byte[].class) { + return (T) deserializerBuffer.readByteArray(this.value.length); + } else if (clazz == Byte.class) { + return (T) (Byte) deserializerBuffer.readU8(); + } else if (clazz == Long.class) { + return (T) (Long) deserializerBuffer.readU32(); + } else if (clazz == Integer.class) { + return (T) (Integer) deserializerBuffer.readI32(); + } else if (clazz == Short.class) { + return (T) (Short) deserializerBuffer.readU16(); + } else if (clazz == String.class) { + return (T) deserializerBuffer.readString(); + } + + throw new IllegalArgumentException("Unsupported type " + clazz.getName()); + } +} diff --git a/src/main/java/dev/oak3/sbs4j/DeserializerBuffer.java b/src/main/java/dev/oak3/sbs4j/DeserializerBuffer.java new file mode 100644 index 000000000..ceeb202e8 --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/DeserializerBuffer.java @@ -0,0 +1,380 @@ +package dev.oak3.sbs4j; + +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.util.ByteUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Deserializing methods + * + * @since 0.1.0 + */ +public class DeserializerBuffer { + + private final ByteBuffer buffer; + + private static final Logger LOGGER = LoggerFactory.getLogger(DeserializerBuffer.class); + + private static final String LOG_BUFFER_INIT_MESSAGE_HEX_STRING = "Initializing DeserializerBuffer with hexString: {} and byte order {}"; + private static final String LOG_BUFFER_INIT_MESSAGE = "Initializing DeserializerBuffer with bytes: {} and byte order {}"; + private static final String LOG_BUFFER_VALUE_MESSAGE_STRING = "Buffer value: {}"; + private static final String LOG_SERIALIZED_VALUE_MESSAGE_STRING = "Deserialized value for {}: {}"; + private static final String SERIALIZE_EXCEPTION_OUT_OF_BOUNDS_MESSAGE_STRING = "Value %s out of bounds for expected type %s"; + + /** + * Initializes buffer with serialized bytes from hex-encoded {@link String} + * + * @param hexString hex-encoded {@link String} to deserialize and read from + */ + public DeserializerBuffer(final String hexString) { + this(!hexString.isEmpty() ? ByteUtils.parseHexString(hexString) : new byte[]{}); + } + + /** + * Initializes buffer with serialized bytes from hex-encoded {@link String} + * + * @param hexString hex-encoded {@link String} to deserialize and read from + * @param byteOrder the byte order to be using + */ + public DeserializerBuffer(final String hexString, final ByteOrder byteOrder) { + this(!hexString.isEmpty() ? ByteUtils.parseHexString(hexString) : new byte[]{}, byteOrder); + + LOGGER.debug(LOG_BUFFER_INIT_MESSAGE_HEX_STRING, hexString, byteOrder); + } + + /** + * Initializes buffer with serialized bytes and {@link ByteOrder#LITTLE_ENDIAN} + * + * @param bytes byte array to deserialize and read from + */ + public DeserializerBuffer(final byte[] bytes) { + this(bytes, ByteOrder.LITTLE_ENDIAN); + } + + /** + * Initializes buffer with serialized bytes and byte order + * + * @param bytes byte array to deserialize and read from + * @param byteOrder the byte order to be using + */ + public DeserializerBuffer(final byte[] bytes, final ByteOrder byteOrder) { + this.buffer = ByteBuffer.wrap(bytes); + this.buffer.order(byteOrder); + this.buffer.mark(); + + LOGGER.debug(LOG_BUFFER_INIT_MESSAGE, Arrays.toString(bytes), byteOrder); + } + + /** + * Reads a Boolean value + * + * @return true if 1, while false if 0 + * @throws ValueDeserializationException exception holding information of failure to deserialize a value + */ + public Boolean readBool() throws ValueDeserializationException { + try { + final byte buf = this.buffer.get(); + + LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf); + + if (buf == 1) { + return true; + } else if (buf == 0) { + return false; + } else { + throw new ValueDeserializationException( + String.format(SERIALIZE_EXCEPTION_OUT_OF_BOUNDS_MESSAGE_STRING, buf, Boolean.class.getSimpleName())); + } + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading boolean from buffer", bufferUnderflowException); + } + } + + /** + * Reads a byte from buffer + * + * @return the byte + */ + public byte readU8() throws ValueDeserializationException { + try { + final byte u8 = this.buffer.get(); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Byte.class.getSimpleName(), u8); + + return u8; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U8 from buffer", bufferUnderflowException); + } + } + + /** + * Reads a byte from buffer + * + * @return the byte + */ + public short readU16() throws ValueDeserializationException { + try { + final short u16 = this.buffer.getShort(); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Short.class.getSimpleName(), u16); + + return u16; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U16 from buffer", bufferUnderflowException); + } + } + + + /** + * Reads a byte[] from buffer + * + * @param length the length of the array + * @return the byte array as byte[] + */ + public byte[] readByteArray(final int length) throws ValueDeserializationException { + try { + final byte[] bytes = readBytes(length); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Byte.class.getSimpleName(), bytes); + + return bytes; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading byte array from buffer", bufferUnderflowException); + } + } + + /** + * Reads a float of 32 bits (4 bytes) + * + * @return the number as a float + */ + public float readF32() throws ValueDeserializationException { + try { + final float floatNumber = this.buffer.getFloat(); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Float.class.getSimpleName(), floatNumber); + + return floatNumber; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading F32 from buffer", bufferUnderflowException); + } + } + + /** + * Reads a float of 64 bits (8 bytes) + * + * @return the number as a double + */ + public double readF64() throws ValueDeserializationException { + try { + final double doubleNumber = this.buffer.getDouble(); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Double.class.getSimpleName(), doubleNumber); + + return doubleNumber; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading F64 from buffer", bufferUnderflowException); + } + } + + /** + * Reads a signed int of 32 bits (4 bytes) + * + * @return the number as an int + */ + public int readI32() throws ValueDeserializationException { + try { + final int integerNumber = this.buffer.getInt(); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), integerNumber); + + return integerNumber; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading I32 from buffer", bufferUnderflowException); + } + } + + /** + * Reads an unsigned int of 32 bits (4 bytes) + * + * @return the number as a long + */ + public long readU32() throws ValueDeserializationException { + try { + final int signedInteger = this.buffer.getInt(); + final long unsignedIntegerLong = Integer.toUnsignedLong(signedInteger); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), unsignedIntegerLong); + return unsignedIntegerLong; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U32 from buffer", bufferUnderflowException); + } + } + + /** + * Reads a signed int of 64 bits (8 bytes) + * + * @return the number as a long + */ + public long readI64() throws ValueDeserializationException { + try { + final long longNumber = this.buffer.getLong(); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), longNumber); + + return longNumber; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading I64 from buffer", bufferUnderflowException); + } + } + + /** + * Reads an unsigned int of 64 bits (8 bytes) + * + * @return the number as a BigInteger + */ + public BigInteger readU64() throws ValueDeserializationException { + try { + // Since this is a positive (unsigned) number, we should prefix with a zero + // byte to parse correctly + final ByteBuffer bb = ByteBuffer.allocate(9); + bb.put((byte) 0); + bb.putLong(this.buffer.getLong()); + final BigInteger unsignedLong = new BigInteger(bb.array()); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, BigInteger.class.getSimpleName(), unsignedLong); + + return unsignedLong; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U64 from buffer", bufferUnderflowException); + } + } + + /** + * Reads an unsigned int of 128 bits (16 bytes) max + * + * @return the number as a BigInteger + */ + public BigInteger readU128() throws ValueDeserializationException { + try { + return this.readBigInteger(); + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U128 from buffer", bufferUnderflowException); + } + } + + /** + * Reads U256 from buffer + * + * @return the number as a BigInteger + */ + public BigInteger readU256() throws ValueDeserializationException { + try { + return this.readBigInteger(); + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U256 from buffer", bufferUnderflowException); + } + } + + /** + * Reads U512 from buffer + * + * @return the number as a BigInteger + */ + public BigInteger readU512() throws ValueDeserializationException { + try { + return this.readBigInteger(); + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading U512 from buffer", bufferUnderflowException); + } + } + + /** + * Larger numeric values (e.g., U128, U256, U512) serialize as one byte of the + * length of the next number, followed by the two’s complement representation + * with little-endian byte order. + * + * @return the number as a BigInteger + */ + protected BigInteger readBigInteger() { + final byte lengthOfNextNumber = this.buffer.get(); + + LOGGER.debug("Length of next number: {}", lengthOfNextNumber); + + final byte[] buf = new byte[lengthOfNextNumber + 1]; + + this.buffer.get(buf, 1, lengthOfNextNumber); + + LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf); + + if (this.buffer.order() == ByteOrder.LITTLE_ENDIAN) { + ByteUtils.reverse(buf, 1, buf.length - 1); + } + + final BigInteger bigInt = new BigInteger(buf); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, BigInteger.class.getSimpleName(), bigInt); + + return bigInt; + } + + /** + * Reads a String value from buffer + * + * @return the String read + */ + public String readString() throws ValueDeserializationException { + try { + final int length = this.buffer.getInt(); + + LOGGER.debug("Reading string of length: {}", length); + + final byte[] bufString = new byte[length]; + + this.buffer.get(bufString, 0, length); + + LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, bufString); + + final String string = new String(bufString, StandardCharsets.UTF_8); + + LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, String.class.getSimpleName(), string); + + return string; + } catch (BufferUnderflowException bufferUnderflowException) { + throw new ValueDeserializationException("Error while reading String from buffer", bufferUnderflowException); + } + } + + /** + * Retrieves the backing buffer + * + * @return the Deserializer ByteBuffer + */ + public ByteBuffer getBuffer() { + return this.buffer; + } + + /** + * Reads a specified number of bytes + * + * @param length the number of bytes to read + * @return bytes read + */ + protected byte[] readBytes(int length) { + final byte[] buf = new byte[length]; + + this.buffer.get(buf, 0, length); + + LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf); + + return buf; + } +} diff --git a/src/main/java/dev/oak3/sbs4j/SerializerBuffer.java b/src/main/java/dev/oak3/sbs4j/SerializerBuffer.java new file mode 100644 index 000000000..f3208f05a --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/SerializerBuffer.java @@ -0,0 +1,402 @@ +package dev.oak3.sbs4j; + +import dev.oak3.sbs4j.exception.NoSuchTypeException; +import dev.oak3.sbs4j.exception.ValueSerializationException; +import dev.oak3.sbs4j.util.ByteUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static java.util.Objects.requireNonNull; + +/** + * Serializing methods + * + * @since 0.1.0 + */ +public class SerializerBuffer { + + private static final Logger LOGGER = LoggerFactory.getLogger(SerializerBuffer.class); + + private ByteBuffer buffer; + + private static final int INITIAL_CAPACITY = 4096; + + public static final BigInteger ZERO = new BigInteger("0", 10); + public static final BigInteger ONE = new BigInteger("1", 10); + public static final BigInteger TWO = new BigInteger("2", 10); + public static final BigInteger MAX_U64 = TWO.pow(64).subtract(ONE); + public static final BigInteger MAX_U128 = TWO.pow(128).subtract(ONE); + public static final BigInteger MAX_U256 = TWO.pow(256).subtract(ONE); + public static final BigInteger MAX_U512 = TWO.pow(512).subtract(ONE); + + private static final String LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING = "Writing type {} with value: {}"; + private static final String SERIALIZE_EXCEPTION_OUT_OF_BOUNDS_MESSAGE_STRING = "Value %s out of bounds for expected type %s"; + + /** + * Initializes buffer with initial capacity of {@link #INITIAL_CAPACITY} in bytes and {@link ByteOrder#LITTLE_ENDIAN} + */ + public SerializerBuffer() { + this(INITIAL_CAPACITY, ByteOrder.LITTLE_ENDIAN); + } + + /** + * Initializes buffer with initial capacity of {@link #INITIAL_CAPACITY} and given byte order. + * + * @param byteOrder the byte order to be using + */ + public SerializerBuffer(final ByteOrder byteOrder) { + this(INITIAL_CAPACITY, byteOrder); + } + + /** + * Initializes buffer with given initial capacity in bytes and byte order. + * + * @param initialCapacity the initial capacity of the buffer + * @param byteOrder the byte order to be using + */ + public SerializerBuffer(final int initialCapacity, final ByteOrder byteOrder) { + this.buffer = ByteBuffer.allocate(initialCapacity); + this.buffer.order(byteOrder); + this.buffer.mark(); + } + + /** + * Writes a boolean value to the value byte buffer + * + * @param value boolean value to serialize + */ + public void writeBool(final boolean value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Boolean.class.getSimpleName(), + value); + + byte boolByte = Boolean.TRUE.equals(value) ? (byte) 0x01 : (byte) 0x00; + + put(boolByte); + } + + /** + * Writes a single byte value + * + * @param value byte value to serialize + */ + public void writeU8(final byte value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Byte.class.getSimpleName(), value); + + put(value); + } + + /** + * Writes a single byte value + * + * @param value byte value to serialize + */ + public void writeU16(final short value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Short.class.getSimpleName(), value); + + put(value); + } + + + /** + * Writes a byte array value + * + * @param value byte array value to serialize + */ + public void writeByteArray(final byte[] value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, byte[].class.getSimpleName(), value); + + put(value); + } + + /** + * Writes a Float/F32 value + * + * @param value F32 value to serialize + */ + public void writeF32(final float value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Float.class.getSimpleName(), value); + + put(value); + } + + /** + * Writes a Double/F64 value + * + * @param value F64 value to serialize + */ + public void writeF64(final double value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Double.class.getSimpleName(), value); + + put(value); + } + + /** + * Writes an Integer/I32 value + * + * @param value I32 value to serialize + */ + public void writeI32(final int value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Integer.class.getSimpleName(), value); + + put(value); + } + + /** + * Writes an Unsigned Integer (Long)/U32 + * + * @param value U32 value to serialize + */ + public void writeU32(final Long value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Integer.class.getSimpleName(), value); + + put(value.intValue()); + } + + /** + * Writes a Long/I64 value + * + * @param value I64 value to serialize + */ + public void writeI64(final long value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), value); + + put(value); + } + + /** + * Writes an Unsigned Long (BigInteger)/U64 to the value byte buffer + * + * @param value U64 value to serialize + * error with input/output while reading the byte array + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + public void writeU64(final BigInteger value) throws ValueSerializationException { + requireNonNull(value); + checkBoundsFor(value, 64); + + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, BigInteger.class.getSimpleName(), + value); + + put(value.longValue()); + } + + /** + * Writes a BigInteger/U128 to the value byte buffer + * + * @param value U128 value to serialize + * error with input/output while reading the byte array + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + public void writeU128(final BigInteger value) throws ValueSerializationException { + writeBigInteger(value, 128); + } + + /** + * Writes a BigInteger/U256 to the value byte buffer + * + * @param value U256 value to serialize + * error with input/output while reading the byte array + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + public void writeU256(final BigInteger value) throws ValueSerializationException { + writeBigInteger(value, 256); + } + + /** + * Writes a BigInteger/U512 to the value byte buffer + * + * @param value U512 value to serialize + * error with input/output while reading the byte array + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + public void writeU512(final BigInteger value) throws ValueSerializationException { + writeBigInteger(value, 512); + } + + /** + * Writes a BigInteger/U128-U256-U512 to the value byte buffer + * + * @param value BigInteger to serialize + * @param size the bit size of BigInteger + * error with input/output while reading the byte array + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + protected void writeBigInteger(final BigInteger value, final int size) throws ValueSerializationException { + requireNonNull(value); + checkBoundsFor(value, size); + + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, BigInteger.class.getSimpleName(), value); + + final byte bigIntegerLength = (byte) (Math.ceil(value.bitLength() / 8.0)); + + final byte[] byteArray = value.toByteArray(); + + int skipped = 0; + boolean skip = true; + for (byte b : byteArray) { + boolean signByte = b == (byte) 0x00; + if (skip && signByte) { + skipped++; + } else if (skip) { + skip = false; + } + } + + final byte[] bigIntegerBytes = Arrays.copyOfRange(byteArray, skipped, byteArray.length); + + if (this.buffer.order() == ByteOrder.LITTLE_ENDIAN) { + ByteUtils.reverse(bigIntegerBytes); + } + + put(bigIntegerLength); + put(bigIntegerBytes); + } + + /** + * Writes a String to the value byte buffer + * + * @param value String value to serialize + * error with input/output while reading the byte array + */ + public void writeString(final String value) { + LOGGER.debug(LOG_BUFFER_WRITE_TYPE_VALUE_MESSAGE_STRING, String.class.getSimpleName(), value); + + put(value.getBytes(StandardCharsets.UTF_8).length); + put(value.getBytes(StandardCharsets.UTF_8)); + } + + /** + * Checks if the value is within valid bounds for given CLType + * + * @param value the value to check + * @param size the bit size to check against + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + private void checkBoundsFor(final BigInteger value, final int size) throws ValueSerializationException { + final BigInteger max; + if (size == 64) { + max = MAX_U64; + } else if (size == 128) { + max = MAX_U128; + } else if (size == 256) { + max = MAX_U256; + } else if (size == 512) { + max = MAX_U512; + } else { + throw new ValueSerializationException("Error checking numeric bounds", new NoSuchTypeException( + String.format("%s is not a numeric size with check bounds for serializing", size))); + } + + if (value.compareTo(max) > 0 || value.compareTo(ZERO) < 0) { + throw new ValueSerializationException(String.format(SERIALIZE_EXCEPTION_OUT_OF_BOUNDS_MESSAGE_STRING, + value, size)); + } + } + + /** + * Retrieves the backing buffer + * + * @return the Deserializer ByteBuffer + */ + public ByteBuffer getBuffer() { + return this.buffer; + } + + /** + * Gets the full byte array corresponding to serialized data + * + * @return the byte array serialized data + */ + public byte[] toByteArray() { + return Arrays.copyOfRange( + this.buffer.array(), + this.buffer.arrayOffset(), this.buffer.arrayOffset() + this.buffer.position() + ); + } + + private void put(final byte newByte) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + 1 >= this.buffer.capacity()) { + increaseCapacity(1); + } + this.buffer.put(newByte); + } + + private void put(final short bytes) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + 2 >= this.buffer.capacity()) { + increaseCapacity(2); + } + this.buffer.putShort(bytes); + } + + + private void put(final byte[] newBytes) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + newBytes.length >= this.buffer.capacity()) { + increaseCapacity(newBytes.length); + } + this.buffer.put(newBytes); + } + + private void put(final float bytes) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + 4 >= this.buffer.capacity()) { + increaseCapacity(4); + } + this.buffer.putFloat(bytes); + } + + private void put(final double bytes) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + 8 >= this.buffer.capacity()) { + increaseCapacity(8); + } + this.buffer.putDouble(bytes); + } + + private void put(final int bytes) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + 4 >= this.buffer.capacity()) { + increaseCapacity(4); + } + this.buffer.putInt(bytes); + } + + private void put(final long bytes) { + // check if it fits and increase buffer if needed + if (this.buffer.position() + 8 >= this.buffer.capacity()) { + increaseCapacity(8); + } + this.buffer.putLong(bytes); + } + + /** + * Increases the buffer size and replaces with the new capacity buffer including current buffer data + * + * @param increaseByteCount the byte count to increase + * @throws IllegalArgumentException if the parameter is invalid + */ + protected void increaseCapacity(final int increaseByteCount) throws IllegalArgumentException { + if (this.buffer == null) { + throw new IllegalArgumentException("Buffer is null"); + } + + if (increaseByteCount < 0) { + throw new IllegalArgumentException("Size cannot be less than 0"); + } + + int newCapacity = this.buffer.capacity() + increaseByteCount; + ByteBuffer newBuffer = ByteBuffer.allocate(newCapacity); + this.buffer.flip(); + newBuffer.order(this.buffer.order()); + newBuffer.put(this.buffer); + this.buffer = newBuffer; + } +} diff --git a/src/main/java/dev/oak3/sbs4j/exception/NoSuchTypeException.java b/src/main/java/dev/oak3/sbs4j/exception/NoSuchTypeException.java new file mode 100644 index 000000000..7d1322133 --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/exception/NoSuchTypeException.java @@ -0,0 +1,12 @@ +package dev.oak3.sbs4j.exception; + +/** + * Thrown in case of a type which does not exist being requested + * + * @since 0.1.0 + */ +public class NoSuchTypeException extends Exception { + public NoSuchTypeException(String message) { + super(message); + } +} diff --git a/src/main/java/dev/oak3/sbs4j/exception/ValueDeserializationException.java b/src/main/java/dev/oak3/sbs4j/exception/ValueDeserializationException.java new file mode 100644 index 000000000..8466d8272 --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/exception/ValueDeserializationException.java @@ -0,0 +1,16 @@ +package dev.oak3.sbs4j.exception; + +/** + * Thrown when type could not be deserialized + * + * @since 0.1.0 + */ +public class ValueDeserializationException extends Exception { + public ValueDeserializationException(String message) { + super(message); + } + + public ValueDeserializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/dev/oak3/sbs4j/exception/ValueSerializationException.java b/src/main/java/dev/oak3/sbs4j/exception/ValueSerializationException.java new file mode 100644 index 000000000..94eb1ba7a --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/exception/ValueSerializationException.java @@ -0,0 +1,16 @@ +package dev.oak3.sbs4j.exception; + +/** + * Thrown when type could not be serialized + * + * @since 0.1.0 + */ +public class ValueSerializationException extends Exception { + public ValueSerializationException(String message) { + super(message); + } + + public ValueSerializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/dev/oak3/sbs4j/interfaces/DeserializableObject.java b/src/main/java/dev/oak3/sbs4j/interfaces/DeserializableObject.java new file mode 100644 index 000000000..c4707f423 --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/interfaces/DeserializableObject.java @@ -0,0 +1,19 @@ +package dev.oak3.sbs4j.interfaces; + +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; + +/** + * Defines an object as being capable of deserializing with {@link DeserializerBuffer} + * + * @since 0.1.0 + */ +public interface DeserializableObject { + /** + * Called when the object's values must be deserialized + * + * @param deserializerBuffer the deserializer buffer to be used + * @throws ValueDeserializationException exception holding information of failure to deserialize a value + */ + void deserialize(DeserializerBuffer deserializerBuffer) throws ValueDeserializationException; +} diff --git a/src/main/java/dev/oak3/sbs4j/interfaces/SerializableObject.java b/src/main/java/dev/oak3/sbs4j/interfaces/SerializableObject.java new file mode 100644 index 000000000..4dce8bbc7 --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/interfaces/SerializableObject.java @@ -0,0 +1,19 @@ +package dev.oak3.sbs4j.interfaces; + +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueSerializationException; + +/** + * Defines an object as being capable of deserializing with {@link SerializerBuffer} + * + * @since 0.1.0 + */ +public interface SerializableObject { + /** + * Called when the object's values must be serialized + * + * @param serializerBuffer the serializer buffer to be used + * @throws ValueSerializationException exception holding information of failure to serialize a value + */ + void serialize(SerializerBuffer serializerBuffer) throws ValueSerializationException; +} diff --git a/src/main/java/dev/oak3/sbs4j/util/ByteUtils.java b/src/main/java/dev/oak3/sbs4j/util/ByteUtils.java new file mode 100644 index 000000000..e31c5f475 --- /dev/null +++ b/src/main/java/dev/oak3/sbs4j/util/ByteUtils.java @@ -0,0 +1,124 @@ +package dev.oak3.sbs4j.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utilities class for Byte related operations + * + * @since 0.1.0 + */ +public final class ByteUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(ByteUtils.class); + + /** + * Hex string code case selector + */ + public enum HexCase { + LOWER("0123456789abcdef".toCharArray()), + UPPER("0123456789ABCDEF".toCharArray()); + + private final char[] hexCodes; + + public char[] getHexCodes() { + return this.hexCodes; + } + + HexCase(char[] hexCodes) { + this.hexCodes = hexCodes; + } + } + + /** + * Reverses a byte array, used to change endian + * + * @param bytes array of bytes to reverse + */ + public static void reverse(byte[] bytes) { + reverse(bytes, 0, bytes.length - 1); + } + + /** + * Reverses a byte array, used to change endian + * + * @param bytes array of bytes to reverse + * @param from first byte position to swap + * @param to last byte position to swap + */ + public static void reverse(byte[] bytes, int from, int to) { + LOGGER.debug("Reversing {}", bytes); + for (int i = from; i < Math.ceil((to - from) / 2f) + from; i++) { + byte temp = bytes[i]; + bytes[i] = bytes[to - i + from]; + bytes[to - i + from] = temp; + } + LOGGER.debug("Reversed {}", bytes); + } + + /** + * Parses a hex string to byte array + * + * @param hexString the hex string to parse + * @return the encoded byte array + * @throws IllegalArgumentException thrown if hexString is invalid + */ + public static byte[] parseHexString(String hexString) throws IllegalArgumentException { + final int len = hexString.length(); + + // "111" is not a valid hex encoding. + if (len % 2 != 0) + throw new IllegalArgumentException("hexBinary needs to be even-length: " + hexString); + + byte[] out = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + int h = hexToByte(hexString.charAt(i)); + int l = hexToByte(hexString.charAt(i + 1)); + if (h == -1 || l == -1) + throw new IllegalArgumentException("contains illegal character for hexBinary: " + hexString); + + out[i / 2] = (byte) (h * 16 + l); + } + + return out; + } + + /** + * Converts hex characters to int value of byte + * + * @param charToConvert the char to convert to byte + * @return the int value of the converted byte + */ + private static int hexToByte(char charToConvert) { + if ('0' <= charToConvert && charToConvert <= '9') return charToConvert - '0'; + if ('A' <= charToConvert && charToConvert <= 'F') return charToConvert - 'A' + 10; + if ('a' <= charToConvert && charToConvert <= 'f') return charToConvert - 'a' + 10; + return -1; + } + + /** + * Encodes a byte array to an hex string in lower case as default + * + * @param byteArray the byte array to encode + * @return the encoded hex string + */ + public static String encodeHexString(byte[] byteArray) { + return encodeHexString(byteArray, HexCase.LOWER); + } + + /** + * Encodes a byte array to an hex string + * + * @param byteArray the byte array to encode + * @param hexCase upper or lower hex code case to use + * @return the encoded hex string + */ + public static String encodeHexString(byte[] byteArray, HexCase hexCase) { + StringBuilder r = new StringBuilder(byteArray.length * 2); + for (byte b : byteArray) { + r.append(hexCase.getHexCodes()[(b >> 4) & 0xF]); + r.append(hexCase.getHexCodes()[(b & 0xF)]); + } + return r.toString(); + } +} diff --git a/src/test/java/com/casper/sdk/model/entity/StateGetEntityTest.java b/src/test/java/com/casper/sdk/model/entity/StateGetEntityTest.java index 3747be61d..41c833f99 100644 --- a/src/test/java/com/casper/sdk/model/entity/StateGetEntityTest.java +++ b/src/test/java/com/casper/sdk/model/entity/StateGetEntityTest.java @@ -88,9 +88,8 @@ void validateGetStateEntityActualSmartContractCctlReturnedResult() throws IOExce assertThat(contract.getEntryPoints().size(), is(15)); assertThat(contract.getNamedKeys().get(0).getName(), is("allowances")); - assertThat(contract.getNamedKeys().get(0).getKey(), is("uref-5e1239586b122bfe8ec9e3285f375d060ffbf90599fade7e807027fd228275cf-007")); + assertThat(contract.getNamedKeys().get(0).getKey().toString(), is("uref-5e1239586b122bfe8ec9e3285f375d060ffbf90599fade7e807027fd228275cf-007")); assertThat(contract.getEntryPoints().get(14).getV1().getName(), is("balance_of")); - } } diff --git a/src/test/java/com/casper/sdk/model/transaction/field/CalltableSerializationEnvelopeBuilderTest.java b/src/test/java/com/casper/sdk/model/transaction/field/CalltableSerializationEnvelopeBuilderTest.java new file mode 100644 index 000000000..2a9704c82 --- /dev/null +++ b/src/test/java/com/casper/sdk/model/transaction/field/CalltableSerializationEnvelopeBuilderTest.java @@ -0,0 +1,92 @@ +package com.casper.sdk.model.transaction.field; + +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.exception.ValueSerializationException; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Unit tests for the {@link CalltableSerializationEnvelopeBuilder} + * + * @author ian@meywood.com + */ +class CalltableSerializationEnvelopeBuilderTest { + + @Test + void serializeBytes() throws ValueSerializationException, ValueDeserializationException { + + final byte[] expected = { + 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x5, 0x0, 0x0, + 0x0, 0x7, 0x0, 0x0, 0x0, (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x2b, 0x2 + }; + + final byte[] fieldZeroVal = new byte[]{(byte) 254}; + + SerializerBuffer ser = new SerializerBuffer(); + ser.writeU32(4294967295L); + final byte[] fieldOneVal = ser.toByteArray(); + + ser = new SerializerBuffer(); + ser.writeU16((short) 555); + final byte[] fieldTwoVal = ser.toByteArray(); + + final CalltableSerializationEnvelopeBuilder builder = new CalltableSerializationEnvelopeBuilder(); + builder.setExpectedFields(3); + builder.addFieldBytes(0, fieldZeroVal); + builder.addFieldBytes(1, fieldOneVal); + builder.addFieldBytes(2, fieldTwoVal); + + builder.serialize(ser); + byte[] bytes = ser.toByteArray(); + + assertNotNull(bytes); + assertThat(bytes.length, is(33)); + assertThat(bytes, is(expected)); + + final CalltableSerializationEnvelopeBuilder deserializedBuilder = new CalltableSerializationEnvelopeBuilder(); + deserializedBuilder.deserialize(new DeserializerBuffer(bytes)); + + assertThat(deserializedBuilder.getFieldBytes(0), is(fieldZeroVal)); + assertThat(deserializedBuilder.getFieldBytes(1), is(fieldOneVal)); + assertThat(deserializedBuilder.getFieldBytes(2), is(fieldTwoVal)); + } + + @Test + void serializeValues() throws ValueSerializationException, ValueDeserializationException { + + final byte[] expected = { + 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x5, 0x0, 0x0, + 0x0, 0x7, 0x0, 0x0, 0x0, (byte) 0xfe, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x2b, 0x2 + }; + + final byte fieldZeroVal = (byte) 254; + final long fieldOneVal = 4294967295L; + final short fieldTwoVal = 555; + + final CalltableSerializationEnvelopeBuilder builder = new CalltableSerializationEnvelopeBuilder(); + builder.setExpectedFields(3); + builder.addField(0, fieldZeroVal); + builder.addField(1, (int) fieldOneVal); + builder.addField(2, fieldTwoVal); + + SerializerBuffer ser = new SerializerBuffer(); + builder.serialize(ser); + byte[] bytes = ser.toByteArray(); + + assertNotNull(bytes); + assertThat(bytes.length, is(33)); + assertThat(bytes, is(expected)); + + final CalltableSerializationEnvelopeBuilder deserializedBuilder = new CalltableSerializationEnvelopeBuilder(); + deserializedBuilder.deserialize(new DeserializerBuffer(bytes)); + + assertThat(deserializedBuilder.getFieldValue(0, Byte.class), is(fieldZeroVal)); + assertThat(deserializedBuilder.getFieldValue(1, Long.class), is(fieldOneVal)); + assertThat(deserializedBuilder.getFieldValue(2, Short.class), is(fieldTwoVal)); + } +} diff --git a/src/test/java/com/casper/sdk/service/TransactionTests.java b/src/test/java/com/casper/sdk/service/TransactionTests.java index 46b0b9447..16ebdf7f5 100644 --- a/src/test/java/com/casper/sdk/service/TransactionTests.java +++ b/src/test/java/com/casper/sdk/service/TransactionTests.java @@ -175,10 +175,8 @@ void chainPutContractCep18() throws IOException, ValueSerializationException, UR assertThat(((ExecutionResultV2) transactionResult.getExecutionInfo().getExecutionResult()).getErrorMessage(), is(nullValue())); //Tests for the returned getTransaction Entities/Kinds/Entries are in EffectsTest and CasperServiceTests - } - private GetTransactionResult waitForTransaction(final TransactionHash hash, final CasperService casperService) throws TimeoutException { final long timeout = 300 * 1000L; @@ -202,11 +200,6 @@ private GetTransactionResult waitForTransaction(final TransactionHash hash, fina } } - return result; - } - - - } diff --git a/src/test/java/dev/oak3/sbs4j/DeserializerBufferTest.java b/src/test/java/dev/oak3/sbs4j/DeserializerBufferTest.java new file mode 100644 index 000000000..32717e8ef --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/DeserializerBufferTest.java @@ -0,0 +1,257 @@ +package dev.oak3.sbs4j; + +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.model.Point; +import dev.oak3.sbs4j.model.Polygon; +import dev.oak3.sbs4j.model.Sphere; +import dev.oak3.sbs4j.model.TestData; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.nio.ByteOrder; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +class DeserializerBufferTest { + private static final Logger LOGGER = LoggerFactory.getLogger(DeserializerBufferTest.class); + + @Test + void deserializePoint_should_match_expected_object() throws ValueDeserializationException { + byte[] serializedBytes = new byte[]{4, 0, 0, 0, 2, 0, 0, 0, 9, 0, 0, 0}; + + DeserializerBuffer deserializerBuffer = new DeserializerBuffer(serializedBytes); + + Point expected = new Point(4, 2, 9); + + Point point = new Point(); + point.deserialize(deserializerBuffer); + + assertEquals(expected, point); + } + + @Test + void deserializeCircle_should_match_expected_object() throws ValueDeserializationException { + byte[] serializedBytes = new byte[]{4, 0, 0, 0, 2, 0, 0, 0, 9, 0, 0, 0, 0, 0, -96, 64}; + + DeserializerBuffer deserializerBuffer = new DeserializerBuffer(serializedBytes); + + Sphere expected = new Sphere(new Point(4, 2, 9), 5.0f); + + Sphere sphere = new Sphere(); + sphere.deserialize(deserializerBuffer); + + assertEquals(expected, sphere); + } + + @Test + void deserializePolygon_should_match_expected_object() throws ValueDeserializationException { + byte[] serializedBytes = new byte[]{4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 1, 0, 0, 0, 0, 0, 0, 0}; + + DeserializerBuffer deserializerBuffer = new DeserializerBuffer(serializedBytes); + + Polygon expected = new Polygon(); + + Point p1 = new Point(1, 1, 0); + Point p2 = new Point(1, -1, 0); + Point p3 = new Point(-1, -1, 0); + Point p4 = new Point(-1, 1, 0); + + expected.setVertices(Arrays.asList(p1, p2, p3, p4)); + + Polygon polygon = new Polygon(); + polygon.deserialize(deserializerBuffer); + + assertEquals(expected, polygon); + } + + @Test + void validateDeserialize_LITTLE_ENDIAN_with_SampleData() throws ValueDeserializationException { + for (TestData testData : TestData.SUCCESS_TEST_DATA) { + DeserializerBuffer deserializerBuffer = new DeserializerBuffer(testData.getByteValueLittleEndian()); + switch (testData.getName()) { + case TestData.BOOLEAN: + boolean valueBool = deserializerBuffer.readBool(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueBool, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueBool); + break; + case TestData.BYTE_ARRAY: + byte[] valueByteArray = deserializerBuffer.readByteArray(testData.getByteValueLittleEndian().length); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueByteArray, testData.getByteValueLittleEndianHex()); + assertArrayEquals((byte[]) testData.getValue(), valueByteArray); + break; + case TestData.U_8: + byte valueU8 = deserializerBuffer.readU8(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU8, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU8); + break; + case TestData.U_16: + short valueU16 = deserializerBuffer.readU16(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU16, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU16); + break; + case TestData.U_32: + long valueU32 = deserializerBuffer.readU32(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU32, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU32); + break; + case TestData.F_32: + float valueF32 = deserializerBuffer.readF32(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueF32, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueF32); + break; + case TestData.F_64: + double valueF64 = deserializerBuffer.readF64(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueF64, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueF64); + break; + case TestData.U_64: + BigInteger valueU64 = deserializerBuffer.readU64(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU64, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU64); + break; + case TestData.I_32: + int valueI32 = deserializerBuffer.readI32(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueI32, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueI32); + break; + case TestData.I_64: + long valueI64 = deserializerBuffer.readI64(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueI64, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueI64); + break; + case TestData.U_128: + BigInteger valueU128 = deserializerBuffer.readU128(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU128, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU128); + break; + case TestData.U_256: + BigInteger valueU256 = deserializerBuffer.readU256(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU256, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU256); + break; + case TestData.U_512: + BigInteger valueU512 = deserializerBuffer.readU512(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU512, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueU512); + break; + case TestData.STRING: + String valueString = deserializerBuffer.readString(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueString, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueString); + break; + } + } + } + + @Test + void validateDeserialize_BIG_ENDIAN_with_SampleData() throws ValueDeserializationException { + for (TestData testData : TestData.SUCCESS_TEST_DATA) { + DeserializerBuffer deserializerBuffer; + switch (testData.getName()) { + case TestData.BOOLEAN: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian()); + boolean valueBool = deserializerBuffer.readBool(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueBool, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueBool); + break; + case TestData.BYTE_ARRAY: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + byte[] valueByteArray = deserializerBuffer.readByteArray(testData.getByteValueBigEndian().length); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueByteArray, testData.getByteValueBigEndianHex()); + assertArrayEquals((byte[]) testData.getValue(), valueByteArray); + break; + case TestData.U_8: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + byte valueU8 = deserializerBuffer.readU8(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU8, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueU8); + break; + case TestData.U_32: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + long valueU32 = deserializerBuffer.readU32(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU32, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueU32); + break; + case TestData.F_32: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + float valueF32 = deserializerBuffer.readF32(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueF32, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueF32); + break; + case TestData.F_64: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + double valueF64 = deserializerBuffer.readF64(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueF64, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueF64); + break; + case TestData.U_64: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + BigInteger valueU64 = deserializerBuffer.readU64(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU64, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueU64); + break; + case TestData.I_32: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + int valueI32 = deserializerBuffer.readI32(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueI32, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueI32); + break; + case TestData.I_64: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + long valueI64 = deserializerBuffer.readI64(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueI64, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueI64); + break; + case TestData.U_128: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + BigInteger valueU128 = deserializerBuffer.readU128(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU128, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueU128); + break; + case TestData.U_256: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + BigInteger valueU256 = deserializerBuffer.readU256(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU256, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueU256); + break; + case TestData.U_512: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + BigInteger valueU512 = deserializerBuffer.readU512(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueU512, testData.getByteValueBigEndianHex()); + assertEquals(testData.getValue(), valueU512); + break; + case TestData.STRING: + deserializerBuffer = new DeserializerBuffer(testData.getByteValueBigEndian(), ByteOrder.BIG_ENDIAN); + String valueString = deserializerBuffer.readString(); + LOGGER.debug("Expected: {}, Actual: {} - Hex: {}", testData.getValue(), valueString, testData.getByteValueLittleEndianHex()); + assertEquals(testData.getValue(), valueString); + break; + } + } + } + + @Test + void dataWithWrongInputLength_should_throw_BufferUnderflowException() { + DeserializerBuffer deserializerBuffer1 = new DeserializerBuffer(""); + + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readBool); + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readU32); + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readU64); + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readU128); + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readU256); + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readU512); + assertThrows(ValueDeserializationException.class, deserializerBuffer1::readString); + assertThrows(ValueDeserializationException.class, () -> deserializerBuffer1.readByteArray(32)); + + DeserializerBuffer deserializerBuffer2 = new DeserializerBuffer("01"); + + assertThrows(ValueDeserializationException.class, deserializerBuffer2::readU512); + + DeserializerBuffer deserializerBuffer3 = new DeserializerBuffer("01"); + + assertThrows(ValueDeserializationException.class, deserializerBuffer3::readString); + } +} diff --git a/src/test/java/dev/oak3/sbs4j/SerializerBufferTest.java b/src/test/java/dev/oak3/sbs4j/SerializerBufferTest.java new file mode 100644 index 000000000..d94e96076 --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/SerializerBufferTest.java @@ -0,0 +1,298 @@ +package dev.oak3.sbs4j; + +import dev.oak3.sbs4j.exception.ValueSerializationException; +import dev.oak3.sbs4j.model.Point; +import dev.oak3.sbs4j.model.Polygon; +import dev.oak3.sbs4j.model.Sphere; +import dev.oak3.sbs4j.model.TestData; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URISyntaxException; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.*; + +class SerializerBufferTest { + private static final Logger LOGGER = LoggerFactory.getLogger(SerializerBufferTest.class); + + @Test + void serializePoint_should_match_expected_array() { + byte[] expectedBytes = new byte[]{4, 0, 0, 0, 2, 0, 0, 0, 9, 0, 0, 0}; + + SerializerBuffer serializerBuffer = new SerializerBuffer(); + + Point point = new Point(4, 2, 9); + point.serialize(serializerBuffer); + + byte[] serializedBytes = serializerBuffer.toByteArray(); + + assertArrayEquals(expectedBytes, serializedBytes); + } + + @Test + void serializeCircle_should_match_expected_array() { + byte[] expectedBytes = new byte[]{4, 0, 0, 0, 2, 0, 0, 0, 9, 0, 0, 0, 0, 0, -96, 64}; + + SerializerBuffer serializerBuffer = new SerializerBuffer(); + + Point center = new Point(4, 2, 9); + Sphere sphere = new Sphere(center, 5.0f); + sphere.serialize(serializerBuffer); + + byte[] serializedBytes = serializerBuffer.toByteArray(); + + assertArrayEquals(expectedBytes, serializedBytes); + } + + @Test + void serializePolygon_should_match_expected_array() { + byte[] expectedBytes = new byte[]{4, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, 1, 0, 0, 0, 0, 0, 0, 0}; + + SerializerBuffer serializerBuffer = new SerializerBuffer(); + + Polygon square = new Polygon(); + + Point p1 = new Point(1, 1, 0); + Point p2 = new Point(1, -1, 0); + Point p3 = new Point(-1, -1, 0); + Point p4 = new Point(-1, 1, 0); + + square.setVertices(Arrays.asList(p1, p2, p3, p4)); + + square.serialize(serializerBuffer); + + byte[] serializedBytes = serializerBuffer.toByteArray(); + + assertArrayEquals(expectedBytes, serializedBytes); + } + + @Test + void validateSerialize_with_large_data_without_buffer_overflow() throws URISyntaxException, IOException { + Path imagePath = Paths.get(Objects.requireNonNull(getClass().getClassLoader().getResource("test_image.png")).toURI()); + final byte[] inputBytes = Files.readAllBytes(imagePath); + + SerializerBuffer serializerBuffer = new SerializerBuffer(); + assertDoesNotThrow(() -> serializerBuffer.writeI32(1)); + assertDoesNotThrow(() -> serializerBuffer.writeF32(2.0f)); + assertDoesNotThrow(() -> serializerBuffer.writeI64(3)); + assertDoesNotThrow(() -> serializerBuffer.writeF64(2.0)); + assertDoesNotThrow(() -> serializerBuffer.writeByteArray(inputBytes)); + assertDoesNotThrow(() -> serializerBuffer.writeString("Test string")); + } + + @Test + void validateSerialize_with_SampleData() throws ValueSerializationException { + for (TestData testData : TestData.SUCCESS_TEST_DATA) { + SerializerBuffer serializerBuffer = new SerializerBuffer(); + switch (testData.getName()) { + case TestData.BOOLEAN: + serializerBuffer.writeBool((Boolean) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Boolean.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.BYTE_ARRAY: + serializerBuffer.writeByteArray((byte[]) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", byte[].class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_8: + serializerBuffer.writeU8((Byte) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Byte.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_16: + serializerBuffer.writeU16((Short) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Short.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_32: + serializerBuffer.writeU32((Long) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Long.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.F_32: + serializerBuffer.writeF32((Float) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Float.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.F_64: + serializerBuffer.writeF64((Double) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Double.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_64: + serializerBuffer.writeU64((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.I_32: + serializerBuffer.writeI32((Integer) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Integer.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.I_64: + serializerBuffer.writeI64((Long) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Long.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_128: + serializerBuffer.writeU128((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_256: + serializerBuffer.writeU256((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_512: + serializerBuffer.writeU512((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + case TestData.STRING: + serializerBuffer.writeString((String) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", String.class.getSimpleName(), testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueLittleEndian(), serializerBuffer.toByteArray()); + break; + } + } + } + + @Test + void validateSerialize_BIG_ENDIAN_with_SampleData() throws ValueSerializationException { + for (TestData testData : TestData.SUCCESS_TEST_DATA) { + SerializerBuffer serializerBuffer = new SerializerBuffer(ByteOrder.BIG_ENDIAN); + switch (testData.getName()) { + case TestData.BOOLEAN: + serializerBuffer.writeBool((Boolean) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Boolean.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.BYTE_ARRAY: + serializerBuffer.writeByteArray((byte[]) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", byte[].class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_8: + serializerBuffer.writeU8((Byte) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Byte.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_32: + serializerBuffer.writeU32((Long) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Long.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.F_32: + serializerBuffer.writeF32((Float) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Float.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.F_64: + serializerBuffer.writeF64((Double) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Double.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_64: + serializerBuffer.writeU64((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.I_32: + serializerBuffer.writeI32((Integer) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Integer.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.I_64: + serializerBuffer.writeI64((Long) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", Long.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_128: + serializerBuffer.writeU128((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_256: + serializerBuffer.writeU256((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.U_512: + serializerBuffer.writeU512((BigInteger) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", BigInteger.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + case TestData.STRING: + serializerBuffer.writeString((String) testData.getValue()); + LOGGER.debug("{} | Expected: {} | Actual {}", String.class.getSimpleName(), testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + assertArrayEquals(testData.getByteValueBigEndian(), serializerBuffer.toByteArray()); + break; + } + } + } + + @Test + void numbersShouldBeInsideTheirTypeBounds() throws ValueSerializationException { + for (TestData testData : TestData.LAST_VALID_NUMBER_TEST_DATA) { + SerializerBuffer serializerBuffer = new SerializerBuffer(); + switch (testData.getName()) { + case TestData.U_64: + LOGGER.debug("Testing last valid number (value: {}) for {}", testData.getValue(), + "U64"); + serializerBuffer.writeU64((BigInteger) testData.getValue()); + assertNotNull(serializerBuffer.toByteArray()); + break; + case TestData.U_128: + LOGGER.debug("Testing last valid number (value: {}) for {}", testData.getValue(), + "U128"); + serializerBuffer.writeU128((BigInteger) testData.getValue()); + assertNotNull(serializerBuffer.toByteArray()); + break; + case TestData.U_256: + LOGGER.debug("Testing last valid number (value: {}) for {}", testData.getValue(), + "U256"); + serializerBuffer.writeU256((BigInteger) testData.getValue()); + assertNotNull(serializerBuffer.toByteArray()); + break; + case TestData.U_512: + LOGGER.debug("Testing last valid number (value: {}) for {}", testData.getValue(), + "U512"); + serializerBuffer.writeU512((BigInteger) testData.getValue()); + assertNotNull(serializerBuffer.toByteArray()); + break; + } + } + } + + @Test + void dataOutOfBounds_should_throw_ValueSerializationException() { + for (TestData testData : TestData.OUT_OF_BOUNDS_TEST_DATA) { + SerializerBuffer serializerBuffer = new SerializerBuffer(); + switch (testData.getName()) { + case TestData.U_64: + assertThrows(ValueSerializationException.class, () -> serializerBuffer.writeU64((BigInteger) testData.getValue())); + break; + case TestData.U_128: + assertThrows(ValueSerializationException.class, () -> serializerBuffer.writeU128((BigInteger) testData.getValue())); + break; + case TestData.U_256: + assertThrows(ValueSerializationException.class, () -> serializerBuffer.writeU256((BigInteger) testData.getValue())); + break; + case TestData.U_512: + assertThrows(ValueSerializationException.class, () -> serializerBuffer.writeU512((BigInteger) testData.getValue())); + break; + } + } + } +} diff --git a/src/test/java/dev/oak3/sbs4j/model/Point.java b/src/test/java/dev/oak3/sbs4j/model/Point.java new file mode 100644 index 000000000..8f93525e3 --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/model/Point.java @@ -0,0 +1,70 @@ +package dev.oak3.sbs4j.model; + +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.interfaces.DeserializableObject; +import dev.oak3.sbs4j.interfaces.SerializableObject; + +public class Point implements SerializableObject, DeserializableObject { + private int x; + private int y; + private int z; + + public int getX() { + return x; + } + + public int getY() { + return y; + } + + public int getZ() { + return z; + } + + public Point() { + } + + public Point(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + @Override + public void serialize(SerializerBuffer serializer) { + // The written order... + serializer.writeI32(x); + serializer.writeI32(y); + serializer.writeI32(z); + } + + @Override + public void deserialize(DeserializerBuffer deserializer) throws ValueDeserializationException { + // ...defines the read order + this.x = deserializer.readI32(); + this.y = deserializer.readI32(); + this.z = deserializer.readI32(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Point point = (Point) o; + + if (x != point.x) return false; + if (y != point.y) return false; + return z == point.z; + } + + @Override + public int hashCode() { + int result = x; + result = 31 * result + y; + result = 31 * result + z; + return result; + } +} \ No newline at end of file diff --git a/src/test/java/dev/oak3/sbs4j/model/Polygon.java b/src/test/java/dev/oak3/sbs4j/model/Polygon.java new file mode 100644 index 000000000..96b01e850 --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/model/Polygon.java @@ -0,0 +1,64 @@ +package dev.oak3.sbs4j.model; + +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.interfaces.DeserializableObject; +import dev.oak3.sbs4j.interfaces.SerializableObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Polygon implements SerializableObject, DeserializableObject { + private List vertices; + + public List getVertices() { + return vertices; + } + + public void setVertices(List vertices) { + this.vertices = vertices; + } + + public Polygon() { + } + + public Polygon(List vertices) { + this.vertices = vertices; + } + + @Override + public void serialize(SerializerBuffer serializerBuffer) { + serializerBuffer.writeI32(vertices.size()); + for (Point vertex : vertices) { + vertex.serialize(serializerBuffer); + } + } + + @Override + public void deserialize(DeserializerBuffer deserializerBuffer) throws ValueDeserializationException { + int length = deserializerBuffer.readI32(); + this.vertices = new ArrayList<>(); + for (int i = 0; i < length; i++) { + Point point = new Point(); + point.deserialize(deserializerBuffer); + this.vertices.add(point); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Polygon polygon = (Polygon) o; + + return Objects.equals(vertices, polygon.vertices); + } + + @Override + public int hashCode() { + return vertices != null ? vertices.hashCode() : 0; + } +} diff --git a/src/test/java/dev/oak3/sbs4j/model/Sphere.java b/src/test/java/dev/oak3/sbs4j/model/Sphere.java new file mode 100644 index 000000000..a52be02e1 --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/model/Sphere.java @@ -0,0 +1,55 @@ +package dev.oak3.sbs4j.model; + +import dev.oak3.sbs4j.DeserializerBuffer; +import dev.oak3.sbs4j.SerializerBuffer; +import dev.oak3.sbs4j.exception.ValueDeserializationException; +import dev.oak3.sbs4j.interfaces.DeserializableObject; +import dev.oak3.sbs4j.interfaces.SerializableObject; + +import java.util.Objects; + +public class Sphere implements SerializableObject, DeserializableObject { + private Point center; + private float radius; + + public Sphere() { + } + + public Sphere(Point center, float radius) { + this.center = center; + this.radius = radius; + } + + @Override + public void serialize(SerializerBuffer serializerBuffer) { + // The written order... + this.center.serialize(serializerBuffer); + serializerBuffer.writeF32(this.radius); + } + + @Override + public void deserialize(DeserializerBuffer deserializerBuffer) throws ValueDeserializationException { + // ...defines the read order + this.center = new Point(); + this.center.deserialize(deserializerBuffer); + this.radius = deserializerBuffer.readF32(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Sphere sphere = (Sphere) o; + + if (Float.compare(sphere.radius, radius) != 0) return false; + return Objects.equals(center, sphere.center); + } + + @Override + public int hashCode() { + int result = center != null ? center.hashCode() : 0; + result = 31 * result + (radius != 0.0f ? Float.floatToIntBits(radius) : 0); + return result; + } +} \ No newline at end of file diff --git a/src/test/java/dev/oak3/sbs4j/model/TestData.java b/src/test/java/dev/oak3/sbs4j/model/TestData.java new file mode 100644 index 000000000..aa7c71a61 --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/model/TestData.java @@ -0,0 +1,155 @@ +package dev.oak3.sbs4j.model; + +import dev.oak3.sbs4j.SerializerBuffer; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +/** + * A container for test data for the Serializer/Deserializer + * + * @since 0.1.0 + */ +public class TestData { + + /* + * Type name constants + */ + public static final String BOOLEAN = "BOOLEAN"; + public static final String U_8 = "U8"; + public static final String BYTE_ARRAY = "BYTE_ARRAY"; + public static final String U_16 = "U16"; + public static final String U_32 = "U32"; + public static final String I_32 = "I32"; + public static final String F_32 = "F32"; + public static final String F_64 = "F64"; + public static final String I_64 = "I64"; + public static final String U_64 = "U64"; + public static final String U_128 = "U128"; + public static final String U_256 = "U256"; + public static final String U_512 = "U512"; + public static final String STRING = "STRING"; + + private final String name; + private final T value; + private final byte[] byteValueLittleEndian; + private final byte[] byteValueBigEndian; + + /** + * Gets the test data name + * + * @return the test data name + */ + public String getName() { + return name; + } + + /** + * Gets the test data value + * + * @return the test data value + */ + public T getValue() { + return value; + } + + /** + * Gets the test data byte value as Little Endian where applicable + * + * @return the test data byte value + */ + public byte[] getByteValueLittleEndian() { + return byteValueLittleEndian; + } + + /** + * Gets the test data byte value as Big Endian where applicable + * + * @return the test data byte value + */ + public byte[] getByteValueBigEndian() { + return byteValueBigEndian; + } + + /** + * Gets the test data byte value as hex string + * + * @return the test data byte value as hex string + */ + public String getByteValueLittleEndianHex() { + StringBuilder hex = new StringBuilder(); + for (byte b : byteValueLittleEndian) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + + /** + * Gets the test data byte value as hex string + * + * @return the test data byte value as hex string + */ + public String getByteValueBigEndianHex() { + StringBuilder hex = new StringBuilder(); + for (byte b : byteValueBigEndian) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } + + /** + * Data with correct value/byteValue for loop testing serializer/deserializer + */ + public final static List> SUCCESS_TEST_DATA = Arrays.asList( + new TestData<>(BOOLEAN, true, new byte[]{1}, new byte[]{1}), + new TestData<>(U_8, Byte.valueOf("7"), new byte[]{7}, new byte[]{7}), + new TestData<>(BYTE_ARRAY, new byte[]{'s', 'b', 's', '4', 'j'}, new byte[]{115, 98, 115, 52, 106}, new byte[]{115, 98, 115, 52, 106}), + new TestData<>(U_16, (short) 7, new byte[]{7, 0,}, new byte[]{0, 7}), + new TestData<>(U_32, 7L, new byte[]{7, 0, 0, 0}, new byte[]{0, 0, 0, 7}), + new TestData<>(I_32, 7, new byte[]{7, 0, 0, 0}, new byte[]{0, 0, 0, 7}), + new TestData<>(F_32, (float) 5, new byte[]{0, 0, -96, 64}, new byte[]{64, -96, 0, 0}), + new TestData<>(F_64, (double) 5, new byte[]{0, 0, 0, 0, 0, 0, 20, 64}, new byte[]{64, 20, 0, 0, 0, 0, 0, 0}), + new TestData<>(I_64, 7L, new byte[]{7, 0, 0, 0, 0, 0, 0, 0}, new byte[]{0, 0, 0, 0, 0, 0, 0, 7}), + new TestData<>(U_64, new BigInteger("1024", 10), new byte[]{0, 4, 0, 0, 0, 0, 0, 0}, new byte[]{0, 0, 0, 0, 0, 0, 4, 0}), + new TestData<>(U_64, new BigInteger("18446744073709551615", 10), new byte[]{-1, -1, -1, -1, -1, -1, -1, -1}, new byte[]{-1, -1, -1, -1, -1, -1, -1, -1}), + new TestData<>(U_128, new BigInteger("7", 10), new byte[]{1, 7}, new byte[]{1, 7}), + new TestData<>(U_256, new BigInteger("1024", 10), new byte[]{2, 0, 4}, new byte[]{2, 4, 0}), + new TestData<>(U_512, new BigInteger("123456789101112131415", 10), new byte[]{9, 87, -1, 26, -38, -107, -97, 78, -79, 6}, new byte[]{9, 6, -79, 78, -97, -107, -38, 26, -1, 87}), + new TestData<>(U_512, new BigInteger("2500010000", 10), new byte[]{4, 16, 32, 3, -107}, new byte[]{4, -107, 3, 32, 16}), + new TestData<>(U_512, new BigInteger("0", 10), new byte[]{0}, new byte[]{0}), + new TestData<>(STRING, "the string", new byte[]{10, 0, 0, 0, 116, 104, 101, 32, 115, 116, 114, 105, 110, 103}, new byte[]{0, 0, 0, 10, 116, 104, 101, 32, 115, 116, 114, 105, 110, 103}), + new TestData<>(STRING, "Hello, World!", new byte[]{13, 0, 0, 0, 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}, new byte[]{0, 0, 0, 13, 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33})); + + /** + * Test data for ensuring numeric values would serialize until inside its valid bounds + */ + public final static List> LAST_VALID_NUMBER_TEST_DATA = Arrays.asList( + new TestData<>(U_64, SerializerBuffer.MAX_U64, null, null), + new TestData<>(U_128, SerializerBuffer.MAX_U128, null, null), + new TestData<>(U_256, SerializerBuffer.MAX_U256, null, null), + new TestData<>(U_512, SerializerBuffer.MAX_U512, null, null)); + + /** + * Test data for ensuring numeric values would throw while serializing outside its valid bounds + */ + public final static List> OUT_OF_BOUNDS_TEST_DATA = Arrays.asList( + new TestData<>(U_64, SerializerBuffer.MAX_U64.add(SerializerBuffer.ONE), null, null), + new TestData<>(U_128, SerializerBuffer.MAX_U128.add(SerializerBuffer.ONE), null, null), + new TestData<>(U_256, SerializerBuffer.MAX_U256.add(SerializerBuffer.ONE), null, null), + new TestData<>(U_512, SerializerBuffer.MAX_U512.add(SerializerBuffer.ONE), null, null)); + + /** + * Instantiates a TestData object + * + * @param name its identifier name + * @param value its value + * @param byteValueLittleEndian its corresponding value in bytes + */ + public TestData(String name, T value, byte[] byteValueLittleEndian, byte[] byteValueBigEndian) { + this.name = name; + this.value = value; + this.byteValueLittleEndian = byteValueLittleEndian; + this.byteValueBigEndian = byteValueBigEndian; + } +} diff --git a/src/test/java/dev/oak3/sbs4j/util/ByteUtilsTest.java b/src/test/java/dev/oak3/sbs4j/util/ByteUtilsTest.java new file mode 100644 index 000000000..285d02be2 --- /dev/null +++ b/src/test/java/dev/oak3/sbs4j/util/ByteUtilsTest.java @@ -0,0 +1,87 @@ +package dev.oak3.sbs4j.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +class ByteUtilsTest { + @Test + void reverseEvenByteArray() { + byte[] expected = new byte[]{0, 2, 4, 6}; + byte[] input = new byte[]{6, 4, 2, 0}; + + ByteUtils.reverse(input); + + assertArrayEquals(expected, input); + } + + @Test + void reverseEvenByteArrayRange_from_1_to_2() { + byte[] expected = new byte[]{0, 4, 2, 6}; + byte[] input = new byte[]{0, 2, 4, 6}; + + ByteUtils.reverse(input, 1, 2); + + assertArrayEquals(expected, input); + } + + @Test + void reverseEvenByteArrayRange_from_1_to_3() { + byte[] expected = new byte[]{0, 6, 4, 2}; + byte[] input = new byte[]{0, 2, 4, 6}; + + ByteUtils.reverse(input, 1, 3); + + assertArrayEquals(expected, input); + } + + @Test + void reverseOddByteArray() { + byte[] expected = new byte[]{8, 6, 4, 2, 0}; + byte[] input = new byte[]{0, 2, 4, 6, 8}; + + ByteUtils.reverse(input); + + assertArrayEquals(expected, input); + } + + @Test + void reverseOddByteArrayRange_from_1_to_4() { + byte[] expected = new byte[]{0, 8, 6, 4, 2}; + byte[] input = new byte[]{0, 2, 4, 6, 8}; + + ByteUtils.reverse(input, 1, 4); + + assertArrayEquals(expected, input); + } + + @Test + void reverseOddByteArrayRange_from_0_to_4() { + byte[] expected = new byte[]{8, 6, 4, 2, 0, 10}; + byte[] input = new byte[]{0, 2, 4, 6, 8, 10}; + + ByteUtils.reverse(input, 0, 4); + + assertArrayEquals(expected, input); + } + + @Test + void reverseEmptyByteArray() { + byte[] expected = new byte[]{}; + byte[] input = new byte[]{}; + + ByteUtils.reverse(input); + + assertArrayEquals(expected, input); + } + + @Test + void reverseOneElementByteArray() { + byte[] expected = new byte[]{0}; + byte[] input = new byte[]{0}; + + ByteUtils.reverse(input); + + assertArrayEquals(expected, input); + } +} \ No newline at end of file diff --git a/src/test/resources/test_image.png b/src/test/resources/test_image.png new file mode 100644 index 000000000..bac647dc2 Binary files /dev/null and b/src/test/resources/test_image.png differ