From ccfbc0a830f152cca92c9a1048afab3c56f6fecb Mon Sep 17 00:00:00 2001 From: Sokwhan Huh Date: Wed, 25 Oct 2023 16:36:53 -0700 Subject: [PATCH] Implement immutable byte string class for CEL PiperOrigin-RevId: 576681833 --- .../java/dev/cel/common/values/BUILD.bazel | 19 ++++ .../dev/cel/common/values/CelByteString.java | 93 +++++++++++++++++++ .../java/dev/cel/common/values/BUILD.bazel | 24 +++++ .../cel/common/values/CelByteStringTest.java | 88 ++++++++++++++++++ common/values/BUILD.bazel | 9 ++ 5 files changed, 233 insertions(+) create mode 100644 common/src/main/java/dev/cel/common/values/BUILD.bazel create mode 100644 common/src/main/java/dev/cel/common/values/CelByteString.java create mode 100644 common/src/test/java/dev/cel/common/values/BUILD.bazel create mode 100644 common/src/test/java/dev/cel/common/values/CelByteStringTest.java create mode 100644 common/values/BUILD.bazel diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel new file mode 100644 index 000000000..8dbdbe95e --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -0,0 +1,19 @@ +package( + default_applicable_licenses = [ + "//:license", + ], + default_visibility = [ + "//common/values:__pkg__", + ], +) + +java_library( + name = "cel_byte_string", + srcs = ["CelByteString.java"], + tags = [ + ], + deps = [ + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + ], +) diff --git a/common/src/main/java/dev/cel/common/values/CelByteString.java b/common/src/main/java/dev/cel/common/values/CelByteString.java new file mode 100644 index 000000000..196af790b --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/CelByteString.java @@ -0,0 +1,93 @@ +// Copyright 2023 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 +// +// https://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 dev.cel.common.values; + +import com.google.common.base.Preconditions; +import com.google.errorprone.annotations.Immutable; +import java.util.Arrays; + +/** CelByteString is an immutable sequence of a byte array. */ +@Immutable +@SuppressWarnings("Immutable") // We make defensive copies on the byte array. +public final class CelByteString { + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + public static final CelByteString EMPTY = new CelByteString(EMPTY_BYTE_ARRAY); + + private final byte[] data; + + private volatile int hash = 0; + + public static CelByteString of(byte[] buffer) { + Preconditions.checkNotNull(buffer); + if (buffer.length == 0) { + return EMPTY; + } + return new CelByteString(buffer); + } + + public int size() { + return data.length; + } + + public boolean isEmpty() { + return data.length == 0; + } + + public byte[] toByteArray() { + int size = size(); + if (size == 0) { + return EMPTY_BYTE_ARRAY; + } + + return Arrays.copyOf(data, size); + } + + private CelByteString(byte[] buffer) { + data = Arrays.copyOf(buffer, buffer.length); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof CelByteString)) { + return false; + } + + return Arrays.equals(data, ((CelByteString) o).data); + } + + /** + * Note that we do not use Arrays.hashCode directly due to its implementation using 31 as an odd + * prime, which is outdated and is more prone to hash collisions. This code is very similar to + * what AutoValue generates. + */ + @Override + public int hashCode() { + if (hash == 0) { + int h = 1; + h *= 1000003; + h ^= Arrays.hashCode(data); + if (h == 0) { + h = 1; + } + hash = h; + } + + return hash; + } +} diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel new file mode 100644 index 000000000..e60b124c5 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -0,0 +1,24 @@ +load("//:testing.bzl", "junit4_test_suites") + +package(default_applicable_licenses = ["//:license"]) + +java_library( + name = "tests", + testonly = True, + srcs = glob(["*.java"]), + deps = [ + "//:java_truth", + "//common/values:cel_byte_string", + "@maven//:com_google_guava_guava_testlib", + "@maven//:junit_junit", + ], +) + +junit4_test_suites( + name = "test_suites", + sizes = [ + "small", + ], + src_dir = "src/test/java", + deps = [":tests"], +) diff --git a/common/src/test/java/dev/cel/common/values/CelByteStringTest.java b/common/src/test/java/dev/cel/common/values/CelByteStringTest.java new file mode 100644 index 000000000..e59212bf2 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/CelByteStringTest.java @@ -0,0 +1,88 @@ +// Copyright 2023 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 +// +// https://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 dev.cel.common.values; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.testing.ClassSanityTester; +import com.google.common.testing.EqualsTester; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class CelByteStringTest { + + @Test + public void emptyBytes() { + CelByteString byteString = CelByteString.of(new byte[0]); + + assertThat(byteString).isEqualTo(CelByteString.EMPTY); + assertThat(byteString.isEmpty()).isTrue(); + assertThat(byteString.toByteArray()).isEqualTo(new byte[0]); + } + + @Test + public void equalityTest() { + new EqualsTester() + .addEqualityGroup(CelByteString.of(new byte[0]), CelByteString.EMPTY) + .addEqualityGroup(CelByteString.of(new byte[] {0xa}), CelByteString.of(new byte[] {0xa})) + .addEqualityGroup( + CelByteString.of(new byte[] {0x1, 0x5, 0xc}), + CelByteString.of(new byte[] {0x1, 0x5, 0xc})) + .addEqualityGroup(CelByteString.of(new byte[] {0xf})) + .testEquals(); + } + + @Test + public void sanityTest() throws Exception { + new ClassSanityTester() + .setDefault(CelByteString.class, CelByteString.of(new byte[] {0x1, 0x5, 0xc})) + .setDistinctValues( + CelByteString.class, + CelByteString.of(new byte[] {0x1, 0x5, 0xc}), + CelByteString.of(new byte[] {0x2, 0x6, 0xc})) + .forAllPublicStaticMethods(CelByteString.class) + .thatReturn(CelByteString.class) + .testEquals() + .testNulls(); + } + + @Test + public void hashCode_smokeTest() { + assertThat(CelByteString.of(new byte[0]).hashCode()).isEqualTo(1000002); + assertThat(CelByteString.of(new byte[] {0x1}).hashCode()).isEqualTo(1000035); + assertThat(CelByteString.of(new byte[] {0x1, 0x5}).hashCode()).isEqualTo(999846); + assertThat(CelByteString.of(new byte[] {0x1, 0x5, 0xc}).hashCode()).isEqualTo(998020); + } + + @Test + public void byteString_isImmutable() { + byte[] bytes = {0x1, 0xa, 0x3}; + CelByteString byteString = CelByteString.of(bytes); + + bytes[0] = 0x2; + byte[] newByteArray = byteString.toByteArray(); + assertThat(newByteArray).isNotEqualTo(bytes); + bytes[0] = 0x1; + assertThat(newByteArray).isEqualTo(bytes); + } + + @Test + public void nullBytes_throws() { + assertThrows(NullPointerException.class, () -> CelByteString.of(null)); + } +} diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel new file mode 100644 index 000000000..79b73472b --- /dev/null +++ b/common/values/BUILD.bazel @@ -0,0 +1,9 @@ +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], # TODO: Expose to public when ready +) + +java_library( + name = "cel_byte_string", + exports = ["//common/src/main/java/dev/cel/common/values:cel_byte_string"], +)