IonTypeID.java

// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package com.amazon.ion.impl;

import com.amazon.ion.IonType;

/**
 * Holds pre-computed information about a binary Ion type ID byte.
 */
final class IonTypeID {

    private static final int NUMBER_OF_BYTES = 0x100;
    private static final int BITS_PER_NIBBLE = 4;
    private static final int LOW_NIBBLE_BITMASK = 0x0F;
    private static final int NULL_VALUE_NIBBLE = 0xF;
    private static final int VARIABLE_LENGTH_NIBBLE = 0xE;
    private static final int NEGATIVE_INT_TYPE_CODE = 0x3;
    private static final int TYPE_CODE_INVALID = 0xF;
    private static final int ANNOTATION_WRAPPER_MIN_LENGTH = 0x3;
    private static final int ANNOTATION_WRAPPER_MAX_LENGTH = 0xE;
    static final int ORDERED_STRUCT_NIBBLE = 0x1;

    // NOTE: 'annotation wrapper' is not an IonType, but it is simplest to treat it as one for the purposes of this
    // implementation in order to have a direct mapping from binary type IDs to IonType enum values. IonType.DATAGRAM
    // does not have a type ID, so we will use it to mean 'annotation wrapper' instead.
    static final IonType ION_TYPE_ANNOTATION_WRAPPER = IonType.DATAGRAM;

    // Lookup table from type ID to "binary token type", loosely represented by the IonType enum to avoid the need to
    // define a completely new enum with translations between them. "Binary token types" are a superset of IonType,
    // adding annotation wrapper and `null` (i.e., illegal).
    // See https://amzn.github.io/ion-docs/docs/binary.html#typed-value-formats
    static final IonType[] BINARY_TOKEN_TYPES_1_0 = new IonType[] {
        IonType.NULL,
        IonType.BOOL,
        IonType.INT,
        IonType.INT,
        IonType.FLOAT,
        IonType.DECIMAL,
        IonType.TIMESTAMP,
        IonType.SYMBOL,
        IonType.STRING,
        IonType.CLOB,
        IonType.BLOB,
        IonType.LIST,
        IonType.SEXP,
        IonType.STRUCT,
        ION_TYPE_ANNOTATION_WRAPPER,
        null // The 0xF type code is illegal in Ion 1.0.
    };

    // Singleton invalid type ID.
    private static final IonTypeID ALWAYS_INVALID_TYPE_ID = new IonTypeID((byte) 0xFF, 0);

    // Pre-compute all possible type ID bytes.
    static final IonTypeID[] TYPE_IDS_NO_IVM;
    static final IonTypeID[] TYPE_IDS_1_0;
    static {
        TYPE_IDS_NO_IVM = new IonTypeID[NUMBER_OF_BYTES];
        TYPE_IDS_1_0 = new IonTypeID[NUMBER_OF_BYTES];
        for (int b = 0x00; b < NUMBER_OF_BYTES; b++) {
            TYPE_IDS_NO_IVM[b] = ALWAYS_INVALID_TYPE_ID;
            TYPE_IDS_1_0[b] = new IonTypeID((byte) b, 0);
        }
    }

    final IonType type;
    final int length;
    final boolean variableLength;
    final boolean isNull;
    final boolean isNopPad;
    final byte lowerNibble;
    final boolean isValid;
    final boolean isNegativeInt;
    final boolean isTemplateInvocation; // Unused in Ion 1.0
    final int templateId; // Unused in Ion 1.0
    final boolean isDelimited; // Unused in Ion 1.0
    // For structs, denotes whether field names are VarSyms. For symbols, denotes whether the text is inline.
    // For annotation wrappers, denotes whether tokens are VarSyms.
    final boolean isInlineable; // Unused in Ion 1.0

    /**
     * Determines whether the Ion spec allows this particular upperNibble/lowerNibble pair.
     */
    private static boolean isValid_1_0(byte upperNibble, byte lowerNibble, IonType type) {
        if (upperNibble == TYPE_CODE_INVALID) {
            // Type code F is unused in Ion 1.0.
            return false;
        }
        if (type == IonType.BOOL) {
            // Bool values can only be false (0), true (1), or null (F).
            return lowerNibble <= 1 || lowerNibble == NULL_VALUE_NIBBLE;
        }
        if (type == IonType.INT && upperNibble == NEGATIVE_INT_TYPE_CODE) {
            // There is no negative zero int.
            return lowerNibble != 0;
        }
        if (type == IonType.FLOAT) {
            // Floats are either 0e0 (0), 32-bit (4), 64-bit (8), or null (F).
            return lowerNibble == 0 || lowerNibble == 4 || lowerNibble == 8 || lowerNibble == NULL_VALUE_NIBBLE;
        }
        if (type == IonType.TIMESTAMP) {
            // There is no zero-length timestamp representation.
            return lowerNibble > 1;
        }
        if (type == ION_TYPE_ANNOTATION_WRAPPER) {
            return lowerNibble >= ANNOTATION_WRAPPER_MIN_LENGTH && lowerNibble <= ANNOTATION_WRAPPER_MAX_LENGTH;
        }
        return true;
    }

    private IonTypeID(byte id, int minorVersion) {
        if (minorVersion == 0) {
            byte upperNibble = (byte) ((id >> BITS_PER_NIBBLE) & LOW_NIBBLE_BITMASK);
            this.lowerNibble = (byte) (id & LOW_NIBBLE_BITMASK);
            if (upperNibble == 0 && lowerNibble != NULL_VALUE_NIBBLE) {
                this.isNopPad = true;
                this.type = null;
            } else {
                this.isNopPad = false;
                this.type = BINARY_TOKEN_TYPES_1_0[upperNibble];
            }
            this.isValid = isValid_1_0(upperNibble, lowerNibble, type);
            this.isNull = lowerNibble == NULL_VALUE_NIBBLE;
            byte length = lowerNibble;
            if (type == IonType.NULL || type == IonType.BOOL || !isValid) {
                variableLength = false;
                length = 0;
            } else if (type == IonType.STRUCT && length == ORDERED_STRUCT_NIBBLE) {
                variableLength = true;
            } else {
                variableLength = length == VARIABLE_LENGTH_NIBBLE;
            }
            if (isNull) {
                length = 0;
            }
            this.isNegativeInt = type == IonType.INT && upperNibble == NEGATIVE_INT_TYPE_CODE;
            this.length = length;
            this.isTemplateInvocation = false;
            this.templateId = -1;
            this.isDelimited = false;
            this.isInlineable = false;
        } else {
            throw new IllegalStateException("Only Ion 1.0 is currently supported.");
        }
    }

    /**
     * @return a String representation of this object (for debugging).
     */
    @Override
    public String toString() {
        return String.format("%s(%s)", type, length);
    }
}