DeserializerBuffer.java

package dev.oak3.sbs4j;

import dev.oak3.sbs4j.exception.InvalidByteStringException;
import dev.oak3.sbs4j.exception.ValueDeserializationException;
import dev.oak3.sbs4j.helper.StringByteHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;

import static java.util.Objects.requireNonNull;

/**
 * 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_STRING = "Initializing with hexString: {}";
    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 a byte array
     *
     * @param bytes byte array to deserialize and read from
     */
    public DeserializerBuffer(byte[] bytes) {
        this.buffer = ByteBuffer.wrap(bytes);
        this.buffer.order(ByteOrder.LITTLE_ENDIAN);
        this.buffer.mark();

        LOGGER.debug(LOG_BUFFER_INIT_MESSAGE_STRING, Arrays.toString(bytes));
    }

    /**
     * Initializes buffer with serialized bytes from hex-encoded {@link String}
     *
     * @param hexString hex-encoded {@link String} to deserialize and read from
     * @throws InvalidByteStringException if the byte string is invalid or can't be parsed
     */
    public DeserializerBuffer(String hexString) throws InvalidByteStringException {
        this(StringByteHelper.hexStringToByteArray(hexString));

        LOGGER.debug(LOG_BUFFER_INIT_MESSAGE_STRING, hexString);
    }

    /**
     * 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 {
        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()));
        }
    }

    /**
     * Reads a byte from buffer
     *
     * @return the byte
     */
    public byte readU8() {
        byte u8 = this.buffer.get();

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Byte.class.getSimpleName(), u8);

        return u8;
    }

    /**
     * Reads a byte[] from buffer
     *
     * @param length the length of the array
     * @return the byte array as byte[]
     */
    public byte[] readByteArray(int length) {
        byte[] bytes = readBytes(length);

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Byte.class.getSimpleName(), bytes);

        return bytes;
    }

    /**
     * Reads a float of 32 bits (4 bytes)
     *
     * @return the number as a float
     */
    public float readF32() {
        float floatNumber = this.buffer.getFloat();

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Float.class.getSimpleName(), floatNumber);

        return floatNumber;
    }

    /**
     * Reads a float of 64 bits (8 bytes)
     *
     * @return the number as a double
     */
    public double readF64() {
        double doubleNumber = this.buffer.getDouble();

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Double.class.getSimpleName(), doubleNumber);

        return doubleNumber;
    }

    /**
     * Reads a signed int of 32 bits (4 bytes)
     *
     * @return the number as an int
     */
    public int readI32() {
        int integerNumber = this.buffer.getInt();

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Integer.class.getSimpleName(), integerNumber);

        return integerNumber;
    }

    /**
     * Reads an unsigned int of 32 bits (4 bytes)
     *
     * @return the number as a long
     */
    public long readU32() {
        long unsignedInteger = this.buffer.getLong();

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), unsignedInteger);

        return unsignedInteger;
    }

    /**
     * Reads a signed int of 64 bits (8 bytes)
     *
     * @return the number as a long
     */
    public long readI64() {
        long longNumber = this.buffer.getLong();

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), longNumber);

        return longNumber;
    }

    /**
     * Reads an unsigned int of 64 bits (8 bytes)
     *
     * @return the number as a BigInteger
     */
    public BigInteger readU64() {
        // Since this is a positive (unsigned) number, we should prefix with a zero
        // byte to parse correctly
        ByteBuffer bb = ByteBuffer.allocate(9);
        bb.put((byte) 0);
        bb.putLong(this.buffer.getLong());
        BigInteger unsignedLong = new BigInteger(bb.array());

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, BigInteger.class.getSimpleName(), unsignedLong);

        return unsignedLong;
    }

    /**
     * Reads an unsigned int of 128 bits (16 bytes) max
     *
     * @return the number as a BigInteger
     */
    public BigInteger readU128() {
        return this.readBigInteger();
    }

    /**
     * Reads U256 from buffer
     *
     * @return the number as a BigInteger
     */
    public BigInteger readU256() {
        return this.readBigInteger();
    }

    /**
     * Reads U512 from buffer
     *
     * @return the number as a BigInteger
     */
    public BigInteger readU512() {
        return this.readBigInteger();
    }

    /**
     * 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() {
        byte lengthOfNextNumber = this.buffer.get();

        LOGGER.debug("Length of next number: {}", lengthOfNextNumber);

        byte[] buf = new byte[lengthOfNextNumber];

        this.buffer.get(buf, 0, lengthOfNextNumber);

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        BigInteger bigInt = new BigInteger(StringByteHelper.convertBytesToHex(buf), 16);

        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() {
        int length = this.buffer.getInt();

        LOGGER.debug("Reading string of length: {}", length);

        byte[] bufString = new byte[length];

        this.buffer.get(bufString, 0, length);

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, bufString);

        String string = new String(bufString);

        LOGGER.debug(LOG_SERIALIZED_VALUE_MESSAGE_STRING, String.class.getSimpleName(), string);

        return string;
    }

    /**
     * Reads a specified number of bytes
     *
     * @param length the number of bytes to read
     * @return bytes read
     */
    protected byte[] readBytes(int length) {
        byte[] buf = new byte[length];

        this.buffer.get(buf, 0, length);

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        return buf;
    }
}