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}