CLValueDecoder.java

package com.syntifi.casper.sdk.model.clvalue.encdec;

import com.syntifi.casper.sdk.exception.BufferEndCLValueDecodeException;
import com.syntifi.casper.sdk.exception.CLValueDecodeException;
import com.syntifi.casper.sdk.exception.InvalidByteStringException;
import com.syntifi.casper.sdk.exception.NoSuchKeyTagException;
import com.syntifi.casper.sdk.model.clvalue.AbstractCLValue;
import com.syntifi.casper.sdk.model.clvalue.CLValueAny;
import com.syntifi.casper.sdk.model.clvalue.CLValueBool;
import com.syntifi.casper.sdk.model.clvalue.CLValueByteArray;
import com.syntifi.casper.sdk.model.clvalue.CLValueI32;
import com.syntifi.casper.sdk.model.clvalue.CLValueI64;
import com.syntifi.casper.sdk.model.clvalue.CLValueKey;
import com.syntifi.casper.sdk.model.clvalue.CLValuePublicKey;
import com.syntifi.casper.sdk.model.clvalue.CLValueString;
import com.syntifi.casper.sdk.model.clvalue.CLValueU128;
import com.syntifi.casper.sdk.model.clvalue.CLValueU256;
import com.syntifi.casper.sdk.model.clvalue.CLValueU32;
import com.syntifi.casper.sdk.model.clvalue.CLValueU512;
import com.syntifi.casper.sdk.model.clvalue.CLValueU64;
import com.syntifi.casper.sdk.model.clvalue.CLValueU8;
import com.syntifi.casper.sdk.model.clvalue.cltype.CLTypeData;
import com.syntifi.casper.sdk.model.key.Key;
import com.syntifi.casper.sdk.model.key.PublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;

/**
 * Casper CLValue Decoding methods
 *
 * @author Alexandre Carvalho
 * @author Andre Bertolace
 * @see AbstractCLValue
 * @since 0.0.1
 */
public class CLValueDecoder extends ByteArrayInputStream {

    private static final Logger LOGGER = LoggerFactory.getLogger(CLValueDecoder.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_DECODED_VALUE_MESSAGE_STRING = "Decoded value for {}: {}";
    private static final String DECODE_EXCEPTION_BUFFER_END_EMPTY_MESSAGE_STRING = "Buffer empty, could not read data";
    private static final String DECODE_EXCEPTION_BUFFER_END_MESSAGE_STRING = "Buffer ended, could not read more data";
    private static final String DECODE_EXCEPTION_WRONG_LENGTH_MESSAGE_STRING = "Could not read %s (Expected length: %d, Actual length: %d)";
    private static final String DECODE_EXCEPTION_OUT_OF_BOUNDS_MESSAGE_STRING = "Value %s out of bounds for expected type %s";

    /**
     * Initializes buffer with decoded bytes from hex-encoded {@link String}
     *
     * @param hexString hex-encoded {@link String} of a CLValue
     * @throws InvalidByteStringException if the byte string is invalid or can't be parsed
     */
    public CLValueDecoder(String hexString) throws InvalidByteStringException {
        super(StringByteHelper.hexStringToByteArray(hexString));

        LOGGER.debug(LOG_BUFFER_INIT_MESSAGE_STRING, hexString);
    }

    /**
     * Boolean values serialize as a single byte; true maps to 1, while false maps
     * to 0.
     *
     * @param clValue target {@link CLValueBool}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readBool(CLValueBool clValue) throws IOException, CLValueDecodeException {
        int length = 1;
        byte[] buf = new byte[length];

        int readBytes;
        if ((readBytes = this.read(buf)) != length) {
            throwReadBytesError(Boolean.class.getSimpleName(), length, readBytes);
        }

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        clValue.setBytes(StringByteHelper.convertBytesToHex(buf));
        if (buf[0] == 1) {
            clValue.setValue(true);
        } else if (buf[0] == 0) {
            clValue.setValue(false);
        } else {
            throw new CLValueDecodeException(
                    String.format(DECODE_EXCEPTION_OUT_OF_BOUNDS_MESSAGE_STRING, buf[0], CLTypeData.BOOL));
        }
    }

    /**
     * Numeric values consisting of 64 bits or fewer serialize in the two’s
     * complement representation with little-endian byte order, and the appropriate
     * number of bytes for the bit-width.
     * <p>
     * E.g. 7u8 serializes as 0x07
     *
     * @param clValue target {@link CLValueU8}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readU8(CLValueU8 clValue) throws IOException, CLValueDecodeException {
        byte u8 = readByte();

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

        clValue.setBytes(StringByteHelper.convertBytesToHex(new byte[]{u8}));
        clValue.setValue(u8);
    }

    /**
     * Reads a byteArray into a clvalue
     *
     * @param clValue target {@link CLValueByteArray}
     * @param length  the length of the array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     * @throws IOException            error with input/output while reading the byte array
     */
    public void readByteArray(CLValueByteArray clValue, int length) throws CLValueDecodeException, IOException {
        byte[] bytes = readBytes(length);

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

        clValue.setBytes(StringByteHelper.convertBytesToHex(bytes));
        clValue.setValue(bytes);
    }

    /**
     * Numeric values consisting of 64 bits or fewer serialize in the two’s
     * complement representation with little-endian byte order, and the appropriate
     * number of bytes for the bit-width.
     * <p>
     * E.g. 7u32 serializes as 0x07000000 E.g. 1024u32 serializes as 0x00040000
     *
     * @param clValue target {@link CLValueI32}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readI32(CLValueI32 clValue) throws IOException, CLValueDecodeException {
        int length = 4;
        byte[] buf = new byte[length];

        int readBytes;
        if ((readBytes = this.read(buf)) != length) {
            throwReadBytesError(Integer.class.getSimpleName(), length, readBytes);
        }

        clValue.setBytes(StringByteHelper.convertBytesToHex(buf));

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        int integerNumber = 0;
        for (int i = 0; i < length; i++) {
            integerNumber += (buf[i] & 0xFF) << (8 * i);
        }

        LOGGER.debug(LOG_DECODED_VALUE_MESSAGE_STRING, Integer.class.getSimpleName(), integerNumber);
        clValue.setValue(integerNumber);
    }

    /**
     * Reads a {@link Long} value from buffer, representing an Unsigned
     * {@link Integer} (U32)
     *
     * @param clValue target {@link CLValueU32}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readU32(CLValueU32 clValue) throws IOException, CLValueDecodeException {
        int length = 4;
        byte[] buf = new byte[length];

        int readBytes;
        if ((readBytes = this.read(buf)) != length) {
            throwReadBytesError(Long.class.getSimpleName(), length, readBytes);
        }

        clValue.setBytes(StringByteHelper.convertBytesToHex(buf));

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        int integerNumber = 0;
        for (int i = 0; i < length; i++) {
            integerNumber += (buf[i] & 0xFF) << (8 * i);
        }

        long unsignedInteger = Integer.toUnsignedLong(integerNumber);

        LOGGER.debug(LOG_DECODED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), unsignedInteger);
        clValue.setValue(unsignedInteger);
    }

    /**
     * Numeric values consisting of 64 bits or fewer serialize in the two’s
     * complement representation with little-endian byte order, and the appropriate
     * number of bytes for the bit-width.
     * <p>
     * E.g. 7u8 serializes as 0x07 E.g. 7u32 serializes as 0x07000000 E.g. 1024u32
     * serializes as 0x00040000
     *
     * @param clValue target {@link CLValueI64}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readI64(CLValueI64 clValue) throws IOException, CLValueDecodeException {
        int length = 8;
        byte[] buf = new byte[length];

        int readBytes;
        if ((readBytes = this.read(buf)) != length) {
            throwReadBytesError(Long.class.getSimpleName(), length, readBytes);
        }

        clValue.setBytes(StringByteHelper.convertBytesToHex(buf));

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        StringByteHelper.reverse(buf);

        String longStringHex = StringByteHelper.convertBytesToHex(buf);

        Long longNumber = Long.parseLong(longStringHex, 16);

        LOGGER.debug(LOG_DECODED_VALUE_MESSAGE_STRING, Long.class.getSimpleName(), longNumber);
        clValue.setValue(longNumber);
    }

    /**
     * Reads a {@link BigInteger} value from buffer, representing an Unsigned
     * {@link Long} (U32)
     *
     * @param clValue target {@link CLValueU64}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readU64(CLValueU64 clValue) throws IOException, CLValueDecodeException {
        int length = 8;
        byte[] buf = new byte[length];

        int readBytes;
        if ((readBytes = this.read(buf)) != length) {
            throwReadBytesError(BigInteger.class.getSimpleName(), length, readBytes);
        }

        clValue.setBytes(StringByteHelper.convertBytesToHex(buf));

        StringByteHelper.reverse(buf);

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        BigInteger unsignedLong;
        // Since this is a positive (unsigned) number, we should prefix with a zero
        // byte to parse correctly
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            outputStream.write(0);
            outputStream.write(buf);

            unsignedLong = new BigInteger(outputStream.toByteArray());
        }

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

        clValue.setValue(unsignedLong);
    }

    /**
     * Reads U128 from buffer
     *
     * @param clValue target {@link CLValueU128}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readU128(CLValueU128 clValue) throws IOException, CLValueDecodeException {
        this.readBigInteger(clValue);
    }

    /**
     * Reads U256 from buffer
     *
     * @param clValue target {@link CLValueU256}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readU256(CLValueU256 clValue) throws IOException, CLValueDecodeException {
        this.readBigInteger(clValue);
    }

    /**
     * Reads U512 from buffer
     *
     * @param clValue target {@link CLValueU512}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readU512(CLValueU512 clValue) throws IOException, CLValueDecodeException {
        this.readBigInteger(clValue);
    }

    /**
     * Wider numeric values (i.e. U128, U256, U512) serialize as one byte given the
     * length of the next number (in bytes), followed by the two’s complement
     * representation with little-endian byte order. The number of bytes should be
     * chosen as small as possible to represent the given number. This is done to
     * reduce the serialization size when small numbers are represented within a
     * wide data type.
     * <p>
     * E.g. U512::from(7) serializes as 0x0107 E.g. U512::from(1024) serializes as
     * 0x020004 E.g. U512::from("123456789101112131415") serializes as
     * 0x0957ff1ada959f4eb106
     *
     * @param clValue target {@link AbstractCLValue}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    protected void readBigInteger(AbstractCLValue<BigInteger, ?> clValue) throws IOException, CLValueDecodeException {
        byte[] buf = new byte[1];

        int readBytes;
        if ((readBytes = this.read(buf)) != 1) {
            throw new CLValueDecodeException(String.format(DECODE_EXCEPTION_WRONG_LENGTH_MESSAGE_STRING,
                    Byte.class.getSimpleName(), 1, readBytes));
        }

        byte lengthOfNextNumber = buf[0];

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

        buf = new byte[lengthOfNextNumber];

        if ((readBytes = this.read(buf)) != lengthOfNextNumber) {
            throwReadBytesError(BigInteger.class.getSimpleName(), lengthOfNextNumber, readBytes);
        }

        clValue.setBytes(StringByteHelper.convertBytesToHex(new byte[]{lengthOfNextNumber})
                + StringByteHelper.convertBytesToHex(buf));

        StringByteHelper.reverse(buf);

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

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

        LOGGER.debug(LOG_DECODED_VALUE_MESSAGE_STRING, BigInteger.class.getSimpleName(), bigInt);

        clValue.setValue(bigInt);
    }

    /**
     * Reads a {@link CLValueString} value from buffer
     *
     * @param clValue target {@link CLValueString}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readString(CLValueString clValue) throws IOException, CLValueDecodeException {
        int numberByteLength = 4;
        byte[] bufLength = new byte[numberByteLength];

        int readBytes;
        if ((readBytes = this.read(bufLength)) != numberByteLength) {
            throwReadBytesError(Integer.class.getSimpleName(), numberByteLength, readBytes);
        }

        clValue.setBytes(StringByteHelper.convertBytesToHex(bufLength));

        int length = 0;
        for (int i = 0; i < numberByteLength; i++) {
            length += (bufLength[i] & 0xFF) << (8 * i);
        }

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

        byte[] bufString = new byte[length];

        if ((readBytes = this.read(bufString)) != length) {
            throwReadBytesError(String.class.getSimpleName(), length, readBytes);
        }

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, bufString);

        String string = new String(bufString);

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

        clValue.setBytes(clValue.getBytes() + StringByteHelper.convertBytesToHex(bufString));
        clValue.setValue(string);
    }

    /**
     * Reads a {@link CLValuePublicKey} value from buffer
     *
     * @param clValue target {@link CLValuePublicKey}
     * @throws NoSuchAlgorithmException the requested algorithm was not found
     * @throws CLValueDecodeException   exception holding information of failure to decode a {@link AbstractCLValue}
     * @throws IOException              error with input/output while reading the byte array
     */
    public void readPublicKey(CLValuePublicKey clValue) throws NoSuchAlgorithmException, CLValueDecodeException, IOException {
        byte[] key = this.readBytes(buf.length);
        clValue.setBytes(StringByteHelper.convertBytesToHex(key));
        clValue.setValue(PublicKey.fromTaggedHexString(clValue.getBytes()));
    }

    /**
     * Reads a {@link CLValueKey} value from buffer
     *
     * @param clValue target {@link CLValueKey}
     * @throws NoSuchKeyTagException  the requested key tag was not found
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     * @throws IOException            error with input/output while reading the byte array
     */
    public void readKey(CLValueKey clValue) throws NoSuchKeyTagException, CLValueDecodeException, IOException {
        byte[] key = this.readBytes(buf.length);
        clValue.setBytes(StringByteHelper.convertBytesToHex(key));
        clValue.setValue(Key.fromTaggedHexString(clValue.getBytes()));
    }

    /**
     * Reads all bytes as a generic {@link Object} into a {@link CLValueAny}
     *
     * @param clValue target {@link CLValueAny}
     * @throws IOException            error with input/output while reading the byte array
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    public void readAny(CLValueAny clValue) throws IOException, CLValueDecodeException {
        try (ObjectInputStream ois = new ObjectInputStream(this)) {
            Object obj = ois.readObject();
            clValue.setValue(obj);
        } catch (ClassNotFoundException e) {
            throw new CLValueDecodeException("Class not found", e);
        }
    }

    /**
     * Reads a single byte
     *
     * @return the byte read
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     * @throws IOException            error with input/output while reading the byte array
     */
    protected byte readByte() throws CLValueDecodeException, IOException {
        return this.readBytes(1)[0];
    }

    /**
     * Reads a specified number of bytes
     *
     * @param length the number of bytes to read
     * @return bytes read
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     * @throws IOException            error with input/output while reading the byte array
     */
    protected byte[] readBytes(int length) throws CLValueDecodeException, IOException {
        byte[] buf = new byte[length];

        int readBytes;
        if ((readBytes = this.read(buf)) != length) {
            throwReadBytesError(Byte.class.getSimpleName(), length, readBytes);
        }

        LOGGER.debug(LOG_BUFFER_VALUE_MESSAGE_STRING, buf);

        return buf;
    }

    /**
     * @param simpleName      the object's simple name
     * @param expectedLength  the expected length
     * @param readBytesLength the actual read bytes length
     * @throws CLValueDecodeException exception holding information of failure to decode a {@link AbstractCLValue}
     */
    private void throwReadBytesError(String simpleName, int expectedLength, int readBytesLength) throws CLValueDecodeException {
        if (this.buf.length == 0) {
            throw new BufferEndCLValueDecodeException(DECODE_EXCEPTION_BUFFER_END_EMPTY_MESSAGE_STRING);
        } else if (readBytesLength == -1) {
            throw new BufferEndCLValueDecodeException(DECODE_EXCEPTION_BUFFER_END_MESSAGE_STRING);
        } else {
            throw new CLValueDecodeException(String.format(DECODE_EXCEPTION_WRONG_LENGTH_MESSAGE_STRING, simpleName,
                    expectedLength, readBytesLength));
        }
    }
}