-
Notifications
You must be signed in to change notification settings - Fork 136
feat: add SsFormat encoding library for SpanFE bypass #4292
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a6b8fc2
5ad36f7
5b58188
080f7c6
4311563
dab56b9
a1c6bf1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,371 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package com.google.cloud.spanner.spi.v1; | ||
|
|
||
| import com.google.protobuf.ByteString; | ||
| import java.io.ByteArrayOutputStream; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| public final class SsFormat { | ||
|
|
||
| /** | ||
| * Makes the given key a prefix successor. This means that the returned key is the smallest | ||
| * possible key that is larger than the input key, and that does not have the input key as a | ||
| * prefix. | ||
| * | ||
| * <p>This is done by flipping the least significant bit of the last byte of the key. | ||
| * | ||
| * @param key The key to make a prefix successor. | ||
| * @return The prefix successor key. | ||
| */ | ||
| public static ByteString makePrefixSuccessor(ByteString key) { | ||
| if (key == null || key.isEmpty()) { | ||
| return ByteString.EMPTY; | ||
| } | ||
| byte[] bytes = key.toByteArray(); | ||
| if (bytes.length > 0) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this if statement seems redundant, given the check above If it is needed: Can we have a test that covers both branches of this if statement? |
||
| bytes[bytes.length - 1] = (byte) (bytes[bytes.length - 1] | 1); | ||
| } | ||
| return ByteString.copyFrom(bytes); | ||
| } | ||
|
|
||
| private SsFormat() {} | ||
|
|
||
| private static final int IS_KEY = 0x80; | ||
| private static final int TYPE_MASK = 0x7f; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to be unused. Can it be removed? |
||
|
|
||
| // HeaderType enum values (selected) | ||
| private static final int TYPE_UINT_1 = 0; | ||
| private static final int TYPE_UINT_9 = 8; | ||
|
Comment on lines
+52
to
+53
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (here and for many of the other constants here): Can we either add some comments, or otherwise change the names of these constants so they are easier to understand. What is |
||
| private static final int TYPE_NEG_INT_8 = 9; | ||
| private static final int TYPE_NEG_INT_1 = 16; | ||
| private static final int TYPE_POS_INT_1 = 17; | ||
| private static final int TYPE_POS_INT_8 = 24; | ||
| private static final int TYPE_STRING = 25; | ||
| private static final int TYPE_NULL_ORDERED_FIRST = 27; | ||
| private static final int TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST = 28; | ||
| private static final int TYPE_DECREASING_UINT_9 = 32; | ||
| private static final int TYPE_DECREASING_UINT_1 = 40; | ||
| private static final int TYPE_DECREASING_NEG_INT_8 = 41; | ||
| private static final int TYPE_DECREASING_NEG_INT_1 = 48; | ||
| private static final int TYPE_DECREASING_POS_INT_1 = 49; | ||
| private static final int TYPE_DECREASING_POS_INT_8 = 56; | ||
| private static final int TYPE_DECREASING_STRING = 57; | ||
| private static final int TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST = 59; | ||
| private static final int TYPE_NULL_ORDERED_LAST = 60; | ||
| private static final int TYPE_NEG_DOUBLE_8 = 66; | ||
| private static final int TYPE_NEG_DOUBLE_1 = 73; | ||
| private static final int TYPE_POS_DOUBLE_1 = 74; | ||
| private static final int TYPE_POS_DOUBLE_8 = 81; | ||
| private static final int TYPE_DECREASING_NEG_DOUBLE_8 = 82; | ||
| private static final int TYPE_DECREASING_NEG_DOUBLE_1 = 89; | ||
| private static final int TYPE_DECREASING_POS_DOUBLE_1 = 90; | ||
| private static final int TYPE_DECREASING_POS_DOUBLE_8 = 97; | ||
|
Comment on lines
+52
to
+77
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Multiple of these constants seem to be unused. Can we remove the ones that are not used? |
||
|
|
||
| // EscapeChar enum values | ||
| private static final byte ASCENDING_ZERO_ESCAPE = (byte) 0xf0; | ||
| private static final byte ASCENDING_FF_ESCAPE = (byte) 0x10; | ||
| private static final byte SEP = (byte) 0x78; // 'x' | ||
|
|
||
| // For AppendCompositeTag | ||
| private static final int K_OBJECT_EXISTENCE_TAG = 0x7e; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This constant appears only to be used in the check at the start of |
||
| private static final int K_MAX_FIELD_TAG = 0xffff; | ||
|
|
||
| public static void appendCompositeTag(ByteArrayOutputStream out, int tag) { | ||
| if (tag == K_OBJECT_EXISTENCE_TAG || tag <= 0 || tag > K_MAX_FIELD_TAG) { | ||
| throw new IllegalArgumentException("Invalid tag value: " + tag); | ||
| } | ||
|
|
||
| if (tag < 16) { | ||
| // Short tag: 000 TTTT S (S is LSB of tag, but here tag is original, so S=0) | ||
| // Encodes as (tag << 1) | ||
| out.write((byte) (tag << 1)); | ||
| } else { | ||
| // Long tag | ||
| int shiftedTag = tag << 1; // LSB is 0 for prefix successor | ||
| if (shiftedTag < (1 << (5 + 8))) { // Original tag < 4096 | ||
| // Header: num_extra_bytes=1 (01xxxxx), P=payload bits from tag | ||
| // (1 << 5) is 00100000 | ||
| // (shiftedTag >> 8) are the 5 MSBs of the payload part of the tag | ||
| out.write((byte) ((1 << 5) | (shiftedTag >> 8))); | ||
| out.write((byte) (shiftedTag & 0xFF)); | ||
| } else { // Original tag >= 4096 and <= K_MAX_FIELD_TAG (65535) | ||
| // Header: num_extra_bytes=2 (10xxxxx) | ||
| // (2 << 5) is 01000000 | ||
| out.write((byte) ((2 << 5) | (shiftedTag >> 16))); | ||
| out.write((byte) ((shiftedTag >> 8) & 0xFF)); | ||
| out.write((byte) (shiftedTag & 0xFF)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| public static void appendNullOrderedFirst(ByteArrayOutputStream out) { | ||
| out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_FIRST)); | ||
| out.write((byte) 0); | ||
| } | ||
|
|
||
| public static void appendNullOrderedLast(ByteArrayOutputStream out) { | ||
| out.write((byte) (IS_KEY | TYPE_NULL_ORDERED_LAST)); | ||
| out.write((byte) 0); | ||
| } | ||
|
|
||
| public static void appendNotNullMarkerNullOrderedFirst(ByteArrayOutputStream out) { | ||
| out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_FIRST)); | ||
| } | ||
|
|
||
| public static void appendNotNullMarkerNullOrderedLast(ByteArrayOutputStream out) { | ||
| out.write((byte) (IS_KEY | TYPE_NULLABLE_NOT_NULL_NULL_ORDERED_LAST)); | ||
| } | ||
|
|
||
| public static void appendUnsignedIntIncreasing(ByteArrayOutputStream out, long val) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC, the intention here is to append an unsigned 64-bit integer. Java does not have a native unsigned int64 type, so this uses a
|
||
| if (val < 0) { | ||
| throw new IllegalArgumentException("Unsigned int cannot be negative: " + val); | ||
| } | ||
| byte[] buf = new byte[9]; // Max 9 bytes for value payload | ||
| int len = 0; | ||
|
|
||
| long tempVal = val; | ||
| buf[8 - len] = (byte) ((tempVal & 0x7F) << 1); // LSB is prefix-successor bit (0) | ||
| tempVal >>= 7; | ||
| len++; | ||
|
|
||
| while (tempVal > 0) { | ||
| buf[8 - len] = (byte) (tempVal & 0xFF); | ||
| tempVal >>= 8; | ||
| len++; | ||
| } | ||
|
|
||
| out.write((byte) (IS_KEY | (TYPE_UINT_1 + len - 1))); | ||
| for (int i = 0; i < len; i++) { | ||
| out.write((byte) (buf[8 - len + 1 + i] & 0xFF)); | ||
| } | ||
| } | ||
|
|
||
| public static void appendUnsignedIntDecreasing(ByteArrayOutputStream out, long val) { | ||
| if (val < 0) { | ||
| throw new IllegalArgumentException("Unsigned int cannot be negative: " + val); | ||
| } | ||
|
Comment on lines
+158
to
+161
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above |
||
| byte[] buf = new byte[9]; | ||
| int len = 0; | ||
| long tempVal = val; | ||
|
|
||
| // InvertByte(val & 0x7f) << 1 | ||
| buf[8 - len] = (byte) ((~(tempVal & 0x7F) & 0x7F) << 1); | ||
| tempVal >>= 7; | ||
| len++; | ||
|
|
||
| while (tempVal > 0) { | ||
| buf[8 - len] = (byte) (~(tempVal & 0xFF)); | ||
| tempVal >>= 8; | ||
| len++; | ||
| } | ||
| // If val was 0, loop doesn't run for len > 1. If len is still 1, all bits of tempVal (0) are | ||
| // covered. | ||
| // If val was large, but remaining tempVal became 0, this is correct. | ||
| // If tempVal was 0 initially, buf[8] has (~0 & 0x7f) << 1. len = 1. | ||
| // If tempVal was >0 but became 0 after some shifts, buf[8-len] has inverted last byte. | ||
|
|
||
| out.write((byte) (IS_KEY | (TYPE_DECREASING_UINT_1 - len + 1))); | ||
rahul2393 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| for (int i = 0; i < len; i++) { | ||
| out.write((byte) (buf[8 - len + 1 + i] & 0xFF)); | ||
| } | ||
| } | ||
|
|
||
| private static void appendIntInternal( | ||
| ByteArrayOutputStream out, long val, boolean decreasing, boolean isDouble) { | ||
| if (decreasing) { | ||
| val = ~val; | ||
| } | ||
|
|
||
| byte[] buf = new byte[8]; // Max 8 bytes for payload | ||
| int len = 0; | ||
| long tempVal = val; | ||
|
|
||
| if (tempVal >= 0) { | ||
| buf[7 - len] = (byte) ((tempVal & 0x7F) << 1); | ||
| tempVal >>= 7; | ||
| len++; | ||
| while (tempVal > 0) { | ||
| buf[7 - len] = (byte) (tempVal & 0xFF); | ||
| tempVal >>= 8; | ||
| len++; | ||
| } | ||
| } else { // tempVal < 0 | ||
| // For negative numbers, extend sign bit after shifting | ||
| buf[7 - len] = (byte) ((tempVal & 0x7F) << 1); | ||
| // Simulate sign extension for right shift of negative number | ||
| // (x >> 7) | 0xFE00000000000000ULL; (if x has 64 bits) | ||
| // In Java, right shift `>>` on negative longs performs sign extension. | ||
| tempVal >>= 7; | ||
| len++; | ||
| while (tempVal != -1L) { // Loop until all remaining bits are 1s (sign extension) | ||
| buf[7 - len] = (byte) (tempVal & 0xFF); | ||
| tempVal >>= 8; | ||
| len++; | ||
| if (len > 8) throw new AssertionError("Signed int encoding overflow"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: add curly braces (see https://google.github.io/styleguide/javaguide.html#s4.1-braces) Is this condition covered by tests? |
||
| } | ||
| } | ||
|
|
||
| int type; | ||
| if (val >= 0) { // Original val before potential bit-negation for decreasing | ||
| if (!decreasing) { | ||
| type = isDouble ? (TYPE_POS_DOUBLE_1 + len - 1) : (TYPE_POS_INT_1 + len - 1); | ||
| } else { | ||
| type = | ||
| isDouble | ||
| ? (TYPE_DECREASING_POS_DOUBLE_1 + len - 1) | ||
| : (TYPE_DECREASING_POS_INT_1 + len - 1); | ||
| } | ||
| } else { | ||
| if (!decreasing) { | ||
| type = isDouble ? (TYPE_NEG_DOUBLE_1 - len + 1) : (TYPE_NEG_INT_1 - len + 1); | ||
| } else { | ||
| type = | ||
| isDouble | ||
| ? (TYPE_DECREASING_NEG_DOUBLE_1 - len + 1) | ||
| : (TYPE_DECREASING_NEG_INT_1 - len + 1); | ||
| } | ||
| } | ||
| out.write((byte) (IS_KEY | type)); | ||
| for (int i = 0; i < len; i++) { | ||
| out.write((byte) (buf[7 - len + 1 + i] & 0xFF)); | ||
| } | ||
| } | ||
|
|
||
| public static void appendIntIncreasing(ByteArrayOutputStream out, long value) { | ||
| appendIntInternal(out, value, false, false); | ||
| } | ||
|
|
||
| public static void appendIntDecreasing(ByteArrayOutputStream out, long value) { | ||
| appendIntInternal(out, value, true, false); | ||
| } | ||
|
|
||
| public static void appendDoubleIncreasing(ByteArrayOutputStream out, double value) { | ||
| long enc = Double.doubleToRawLongBits(value); | ||
| if (enc < 0) { | ||
| // Transform negative doubles to maintain lexicographic sort order | ||
| enc = Long.MIN_VALUE - enc; | ||
| } | ||
| appendIntInternal(out, enc, false, true); | ||
| } | ||
|
|
||
| public static void appendDoubleDecreasing(ByteArrayOutputStream out, double value) { | ||
| long enc = Double.doubleToRawLongBits(value); | ||
| if (enc < 0) { | ||
| enc = Long.MIN_VALUE - enc; | ||
| } | ||
| appendIntInternal(out, enc, true, true); | ||
| } | ||
|
|
||
| private static void appendByteSequence( | ||
| ByteArrayOutputStream out, byte[] bytes, boolean decreasing) { | ||
| out.write((byte) (IS_KEY | (decreasing ? TYPE_DECREASING_STRING : TYPE_STRING))); | ||
|
|
||
| for (byte b : bytes) { | ||
| byte currentByte = decreasing ? (byte) ~b : b; | ||
| int unsignedByte = currentByte & 0xFF; | ||
| if (unsignedByte == 0x00) { | ||
| out.write((byte) 0x00); | ||
| out.write( | ||
| decreasing | ||
| ? ASCENDING_ZERO_ESCAPE | ||
| : ASCENDING_ZERO_ESCAPE); // After inversion, 0xFF becomes 0x00. Escape for 0x00 | ||
|
Comment on lines
+284
to
+286
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems weird. Regardless of the value of |
||
| // (inverted) is F0. | ||
| // If increasing, 0x00 -> 0x00 F0. | ||
| } else if (unsignedByte == 0xFF) { | ||
| out.write((byte) 0xFF); | ||
| out.write( | ||
| decreasing | ||
| ? ASCENDING_FF_ESCAPE | ||
| : ASCENDING_FF_ESCAPE); // After inversion, 0x00 becomes 0xFF. Escape for 0xFF | ||
|
Comment on lines
+291
to
+294
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above: Either remove the ternary operation, or add a test that fails with this implementation, and succeeds after fixing this. |
||
| // (inverted) is 0x10. | ||
| // If increasing, 0xFF -> 0xFF 0x10. | ||
| } else { | ||
| out.write((byte) unsignedByte); | ||
| } | ||
| } | ||
| // Terminator | ||
| out.write((byte) (decreasing ? 0xFF : 0x00)); | ||
| out.write(SEP); | ||
| } | ||
|
|
||
| public static void appendStringIncreasing(ByteArrayOutputStream out, String value) { | ||
| appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), false); | ||
| } | ||
|
|
||
| public static void appendStringDecreasing(ByteArrayOutputStream out, String value) { | ||
| appendByteSequence(out, value.getBytes(StandardCharsets.UTF_8), true); | ||
| } | ||
|
|
||
| public static void appendBytesIncreasing(ByteArrayOutputStream out, byte[] value) { | ||
| appendByteSequence(out, value, false); | ||
| } | ||
|
|
||
| public static void appendBytesDecreasing(ByteArrayOutputStream out, byte[] value) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This appears unused (including by tests). Can we remove it, or add tests for it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes - it's needed for completeness. The KeyRecipe PR will need it for BYTES columns with DESC order. Removing it now would just mean adding it back later The same applies to other "unused" methods like appendStringDecreasing, appendIntDecreasing, appendDoubleDecreasing - they're all needed when recipes specify DESC order for those column types. For now I added the tests for it |
||
| appendByteSequence(out, value, true); | ||
| } | ||
|
|
||
| /** | ||
| * Encodes a timestamp as 12 bytes: 8 bytes for seconds since epoch (with offset to handle | ||
| * negative), 4 bytes for nanoseconds. | ||
| */ | ||
| public static byte[] encodeTimestamp(long seconds, int nanos) { | ||
| // Add offset to make negative seconds sort correctly | ||
| long kSecondsOffset = 1L << 63; | ||
| long hi = seconds + kSecondsOffset; | ||
| int lo = nanos; | ||
|
|
||
| byte[] buf = new byte[12]; | ||
| // Big-endian encoding | ||
| buf[0] = (byte) (hi >> 56); | ||
| buf[1] = (byte) (hi >> 48); | ||
| buf[2] = (byte) (hi >> 40); | ||
| buf[3] = (byte) (hi >> 32); | ||
| buf[4] = (byte) (hi >> 24); | ||
| buf[5] = (byte) (hi >> 16); | ||
| buf[6] = (byte) (hi >> 8); | ||
| buf[7] = (byte) hi; | ||
| buf[8] = (byte) (lo >> 24); | ||
| buf[9] = (byte) (lo >> 16); | ||
| buf[10] = (byte) (lo >> 8); | ||
| buf[11] = (byte) lo; | ||
| return buf; | ||
|
Comment on lines
+332
to
+346
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can simplify this to: |
||
| } | ||
|
|
||
| /** Encodes a UUID (128-bit) as 16 bytes in big-endian order. */ | ||
| public static byte[] encodeUuid(long high, long low) { | ||
| byte[] buf = new byte[16]; | ||
| // Big-endian encoding | ||
| buf[0] = (byte) (high >> 56); | ||
| buf[1] = (byte) (high >> 48); | ||
| buf[2] = (byte) (high >> 40); | ||
| buf[3] = (byte) (high >> 32); | ||
| buf[4] = (byte) (high >> 24); | ||
| buf[5] = (byte) (high >> 16); | ||
| buf[6] = (byte) (high >> 8); | ||
| buf[7] = (byte) high; | ||
| buf[8] = (byte) (low >> 56); | ||
| buf[9] = (byte) (low >> 48); | ||
| buf[10] = (byte) (low >> 40); | ||
| buf[11] = (byte) (low >> 32); | ||
| buf[12] = (byte) (low >> 24); | ||
| buf[13] = (byte) (low >> 16); | ||
| buf[14] = (byte) (low >> 8); | ||
| buf[15] = (byte) low; | ||
| return buf; | ||
|
Comment on lines
+351
to
+369
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above: you can just use |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: mark as
@InternalApi