001package gudusoft.gsqlparser.lineage.v2.identity;
002
003import gudusoft.gsqlparser.lineage.v2.SqllrException;
004
005import java.io.ByteArrayOutputStream;
006import java.math.BigInteger;
007import java.nio.ByteBuffer;
008import java.nio.CharBuffer;
009import java.nio.charset.CharacterCodingException;
010import java.nio.charset.CodingErrorAction;
011import java.nio.charset.StandardCharsets;
012import java.util.ArrayDeque;
013import java.util.Deque;
014import java.util.Iterator;
015import java.util.List;
016
017/**
018 * The framing of {@code PUBLIC-LINEAGE-ID-V1} ({@code identity-v1.md}, Section 3.1): one type byte and a
019 * payload per value.
020 *
021 * <table>
022 *   <caption>Framed types</caption>
023 *   <tr><th>Byte</th><th>Java value</th><th>Payload</th></tr>
024 *   <tr><td>{@code 0x00}</td><td>{@code null}</td><td>none</td></tr>
025 *   <tr><td>{@code 0x01}</td><td>{@code String}</td><td>8-byte length, strict UTF-8</td></tr>
026 *   <tr><td>{@code 0x02}</td><td>{@code Long}, {@code Integer}, {@code Short}, {@code Byte}, {@code BigInteger}</td>
027 *       <td>8 bytes, signed big-endian</td></tr>
028 *   <tr><td>{@code 0x03}</td><td>{@code List}</td><td>8-byte count, then each element</td></tr>
029 *   <tr><td>{@code 0x04}</td><td>{@code Boolean}</td><td>1 byte</td></tr>
030 *   <tr><td>{@code 0x05}</td><td>{@link Code}</td><td>as a string</td></tr>
031 * </table>
032 *
033 * @since 4.2.17
034 */
035public final class Framing {
036
037    public static final int T_NULL = 0x00;
038    public static final int T_STRING = 0x01;
039    public static final int T_INTEGER = 0x02;
040    public static final int T_LIST = 0x03;
041    public static final int T_BOOLEAN = 0x04;
042    public static final int T_CODE = 0x05;
043
044    private static final BigInteger INT64_MIN = BigInteger.valueOf(Long.MIN_VALUE);
045    private static final BigInteger INT64_MAX = BigInteger.valueOf(Long.MAX_VALUE);
046
047    private Framing() {
048    }
049
050    /**
051     * The framed bytes of one value.
052     *
053     * @throws SqllrException {@code ID_INVALID_UNICODE} for a string that is not a sequence of Unicode scalar
054     *                        values, {@code ID_INTEGER_OUT_OF_RANGE} for an integer outside 64-bit signed,
055     *                        {@code ID_INVALID_INPUT} for any other Java type
056     */
057    public static byte[] frame(Object value) {
058        ByteArrayOutputStream out = new ByteArrayOutputStream();
059        frameInto(value, out);
060        return out.toByteArray();
061    }
062
063    /** {@code frame(value)} appended to {@code out}; lists are walked with an explicit stack. */
064    public static void frameInto(Object value, ByteArrayOutputStream out) {
065        if (!(value instanceof List<?>)) {
066            scalar(value, out);
067            return;
068        }
069        Deque<Iterator<?>> stack = new ArrayDeque<Iterator<?>>();
070        openList((List<?>) value, out, stack);
071        while (!stack.isEmpty()) {
072            Iterator<?> top = stack.peek();
073            if (!top.hasNext()) {
074                stack.pop();
075                continue;
076            }
077            Object item = top.next();
078            if (item instanceof List<?>) {
079                openList((List<?>) item, out, stack);
080            } else {
081                scalar(item, out);
082            }
083        }
084    }
085
086    private static void openList(List<?> list, ByteArrayOutputStream out, Deque<Iterator<?>> stack) {
087        out.write(T_LIST);
088        length(list.size(), out);
089        stack.push(list.iterator());
090    }
091
092    private static void scalar(Object value, ByteArrayOutputStream out) {
093        if (value == null) {
094            out.write(T_NULL);
095        } else if (value instanceof Code) {
096            byte[] data = ((Code) value).value().getBytes(StandardCharsets.US_ASCII);
097            out.write(T_CODE);
098            length(data.length, out);
099            out.write(data, 0, data.length);
100        } else if (value instanceof Boolean) {
101            out.write(T_BOOLEAN);
102            out.write(((Boolean) value).booleanValue() ? 1 : 0);
103        } else if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) {
104            out.write(T_INTEGER);
105            long v = ((Number) value).longValue();
106            for (int shift = 56; shift >= 0; shift -= 8) {
107                out.write((int) (v >>> shift) & 0xff);
108            }
109        } else if (value instanceof BigInteger) {
110            BigInteger big = (BigInteger) value;
111            if (big.compareTo(INT64_MIN) < 0 || big.compareTo(INT64_MAX) > 0) {
112                throw new SqllrException("ID_INTEGER_OUT_OF_RANGE", big.toString());
113            }
114            scalar(Long.valueOf(big.longValue()), out);
115        } else if (value instanceof String) {
116            byte[] data = utf8((String) value);
117            out.write(T_STRING);
118            length(data.length, out);
119            out.write(data, 0, data.length);
120        } else {
121            throw new SqllrException("ID_INVALID_INPUT", "cannot frame a value of type " + value.getClass().getName());
122        }
123    }
124
125    private static void length(long n, ByteArrayOutputStream out) {
126        for (int shift = 56; shift >= 0; shift -= 8) {
127            out.write((int) (n >>> shift) & 0xff);
128        }
129    }
130
131    /**
132     * Strict UTF-8 of a string ({@code identity-v1.md}, Section 3.2): no normalization.
133     *
134     * @throws SqllrException {@code ID_INVALID_UNICODE} for an unpaired surrogate
135     */
136    public static byte[] utf8(String text) {
137        try {
138            ByteBuffer buffer = StandardCharsets.UTF_8.newEncoder()
139                    .onMalformedInput(CodingErrorAction.REPORT)
140                    .onUnmappableCharacter(CodingErrorAction.REPORT)
141                    .encode(CharBuffer.wrap(text));
142            byte[] out = new byte[buffer.remaining()];
143            buffer.get(out);
144            return out;
145        } catch (CharacterCodingException e) {
146            throw new SqllrException("ID_INVALID_UNICODE", "string is not a sequence of Unicode scalar values");
147        }
148    }
149
150    /** Lowercase hexadecimal of bytes. */
151    public static String hex(byte[] data) {
152        StringBuilder out = new StringBuilder(data.length * 2);
153        for (byte b : data) {
154            out.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16));
155        }
156        return out.toString();
157    }
158}