diff --git a/MODULE.bazel b/MODULE.bazel index ee804697a..895912315 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -28,6 +28,11 @@ bazel_dep(name = "rules_android", version = "0.7.3") bazel_dep(name = "rules_shell", version = "0.8.0") bazel_dep(name = "googleapis-java", version = "1.1.5") bazel_dep(name = "cel-spec", version = "0.25.3", repo_name = "cel_spec") +git_override( + module_name = "cel-spec", + commit = "21ec6f4e52e8d694687ff9ed7408d3eccbb955d7", + remote = "https://github.com/cel-expr/cel-spec", +) bazel_dep(name = "rules_go", version = "0.65.0") # Required by cel-spec to satisfy gazelle transitive dependency @@ -102,7 +107,6 @@ maven.install( "org.jline:jline-reader:" + JLINE_VERSION, "org.jline:jline-terminal:" + JLINE_VERSION, "org.jspecify:jspecify:1.0.1", - "org.threeten:threeten-extra:1.11.0", "org.yaml:snakeyaml:2.7", "tools.aqua:z3-turnkey:4.14.1", ], diff --git a/common/src/main/java/dev/cel/common/CelOptions.java b/common/src/main/java/dev/cel/common/CelOptions.java index e1063e567..c3b4e2534 100644 --- a/common/src/main/java/dev/cel/common/CelOptions.java +++ b/common/src/main/java/dev/cel/common/CelOptions.java @@ -520,12 +520,10 @@ public abstract static class Builder { public abstract Builder enableJsonFieldNames(boolean value); /** - * Enable or disable validating that duration values resulting from timestamp arithmetic do not - * overflow 64-bit nanoseconds. Defaults to enabled. - * - *

Disabling this option is an out-of-conformance behavior that suppresses nanosecond - * overflow validation when subtracting timestamps. + * @deprecated No-op. CEL durations now support the full protobuf duration range, so subtracting + * valid timestamps never overflows. */ + @Deprecated public abstract Builder enableTimestampOverflowCheck(boolean value); /** diff --git a/common/src/main/java/dev/cel/common/internal/BUILD.bazel b/common/src/main/java/dev/cel/common/internal/BUILD.bazel index 409ddfb8f..7c562da31 100644 --- a/common/src/main/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/internal/BUILD.bazel @@ -426,7 +426,6 @@ cel_android_library( deps = [ "//common/annotations", "@maven//:com_google_errorprone_error_prone_annotations", - "@maven//:com_google_guava_guava", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], @@ -438,8 +437,10 @@ java_library( tags = [ ], deps = [ + ":proto_time_utils", "//common/annotations", "//common/exceptions:bad_format", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", ], @@ -451,8 +452,10 @@ cel_android_library( tags = [ ], deps = [ + ":proto_time_utils_android", "//common/annotations", "//common/exceptions:bad_format", + "@maven//:com_google_errorprone_error_prone_annotations", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], diff --git a/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java b/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java index 805a9bd75..d3d289f1f 100644 --- a/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java +++ b/common/src/main/java/dev/cel/common/internal/DateTimeHelpers.java @@ -14,7 +14,15 @@ package dev.cel.common.internal; +import static dev.cel.common.internal.ProtoTimeUtils.DURATION_SECONDS_MAX; +import static dev.cel.common.internal.ProtoTimeUtils.DURATION_SECONDS_MIN; +import static dev.cel.common.internal.ProtoTimeUtils.MAX_NANOS; +import static dev.cel.common.internal.ProtoTimeUtils.NANOS_PER_SECOND; +import static dev.cel.common.internal.ProtoTimeUtils.TIMESTAMP_SECONDS_MAX; +import static dev.cel.common.internal.ProtoTimeUtils.TIMESTAMP_SECONDS_MIN; + import com.google.common.base.Strings; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.Timestamp; import dev.cel.common.annotations.Internal; import dev.cel.common.exceptions.CelBadFormatException; @@ -32,14 +40,8 @@ public final class DateTimeHelpers { public static final String UTC = "UTC"; - // Timestamp for "0001-01-01T00:00:00Z" - private static final long TIMESTAMP_SECONDS_MIN = -62135596800L; - // Timestamp for "9999-12-31T23:59:59Z" - private static final long TIMESTAMP_SECONDS_MAX = 253402300799L; - - private static final long DURATION_SECONDS_MIN = -315576000000L; - private static final long DURATION_SECONDS_MAX = 315576000000L; - private static final int NANOS_PER_SECOND = 1000000000; + private static final Duration DURATION_MIN = Duration.ofSeconds(DURATION_SECONDS_MIN, -MAX_NANOS); + private static final Duration DURATION_MAX = Duration.ofSeconds(DURATION_SECONDS_MAX, MAX_NANOS); /** * Constructs a new {@link LocalDateTime} instance @@ -79,6 +81,14 @@ public static LocalDateTime newLocalDateTime(Instant instant, String tz) { return instant.atZone(timeZone(tz)).toLocalDateTime(); } + /** Creates an {@link Instant} from epoch seconds, validating CEL timestamp bounds. */ + // CEL's timestamp(int) conversion accepts epoch seconds as an int64. + @SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") + public static Instant fromSecondsToInstant(long seconds) { + checkSecondsValid(seconds); + return Instant.ofEpochSecond(seconds); + } + /** * Parse from RFC 3339 date string to {@link java.time.Instant}. * @@ -92,6 +102,75 @@ public static Instant parse(String text) { return instant; } + /** Parses a CEL duration string (following Go {@code time.ParseDuration} syntax). */ + public static Duration parseDuration(String text) { + if (text.isEmpty()) { + throw invalidDurationFormat(); + } + int len = text.length(); + int i = 0; + char first = text.charAt(0); + boolean negative = (first == '-'); + if (negative || first == '+') { + i++; + } + if (i == len) { + throw invalidDurationFormat(); + } + if (len - i == 1 && text.charAt(i) == '0') { + return Duration.ZERO; + } + long totalSeconds = 0; + long totalNanos = 0; + while (i < len) { + int intStart = i; + int intEnd = scanAsciiDigits(text, intStart); + int fracStart = intEnd; + int fracEnd = intEnd; + if (intEnd < len && text.charAt(intEnd) == '.') { + fracStart = intEnd + 1; + fracEnd = scanAsciiDigits(text, fracStart); + } + if (intStart == intEnd && fracStart == fracEnd) { + throw invalidDurationFormat(); + } + i = fracEnd; + int unitStart = i; + while (i < len && text.charAt(i) != '.' && !isAsciiDigit(text.charAt(i))) { + i++; + } + long unitNanos = parseUnitNanos(text, unitStart, i); + + long segSeconds = 0; + long segNanos = 0; + for (int p = intStart; p < intEnd; p++) { + int digit = Character.digit(text.charAt(p), 10); + segSeconds *= 10; + segNanos = segNanos * 10 + digit * unitNanos; + segSeconds += segNanos / NANOS_PER_SECOND; + segNanos %= NANOS_PER_SECOND; + if (segSeconds > DURATION_SECONDS_MAX) { + throw outOfRangeDuration(text); + } + } + + long fracNanos = 0; + for (int p = fracEnd - 1; p >= fracStart; p--) { + fracNanos = (Character.digit(text.charAt(p), 10) * unitNanos + fracNanos) / 10; + } + + totalNanos += segNanos + fracNanos; + totalSeconds += segSeconds + totalNanos / NANOS_PER_SECOND; + totalNanos %= NANOS_PER_SECOND; + if (totalSeconds > DURATION_SECONDS_MAX) { + throw outOfRangeDuration(text); + } + } + return negative + ? Duration.ofSeconds(-totalSeconds, -totalNanos) + : Duration.ofSeconds(totalSeconds, totalNanos); + } + /** Adds a duration to an instant. */ public static Instant add(Instant ts, Duration dur) { Instant newInstant = ts.plus(dur); @@ -102,10 +181,7 @@ public static Instant add(Instant ts, Duration dur) { /** Adds two durations */ public static Duration add(Duration d1, Duration d2) { - Duration newDuration = d1.plus(d2); - checkValid(newDuration); - - return newDuration; + return checkValid(d1.plus(d2)); } /** Subtracts a duration to an instant. */ @@ -118,54 +194,72 @@ public static Instant subtract(Instant ts, Duration dur) { /** Subtract a duration from another. */ public static Duration subtract(Duration d1, Duration d2) { - Duration newDuration = d1.minus(d2); - checkValid(newDuration); + return checkValid(d1.minus(d2)); + } - return newDuration; + /** Calculates the difference between two instants. */ + public static Duration between(Instant d1, Instant d2) { + return checkValid(Duration.between(d1, d2)); } - /** - * Formats a {@link Duration} into a minimal seconds-based representation. - * - *

Note: follows {@code ProtoTimeUtils#toString(Duration)} implementation - */ + /** Formats a {@link Duration} into a minimal seconds-based representation. */ public static String toString(Duration duration) { - if (duration.isZero()) { - return "0s"; + long seconds = duration.getSeconds(); + int nanos = duration.getNano(); + if (seconds < 0 && nanos > 0) { + seconds++; + nanos -= NANOS_PER_SECOND; } + return ProtoTimeUtils.formatDuration(seconds, nanos); + } - long totalNanos = duration.toNanos(); - StringBuilder sb = new StringBuilder(); - - if (totalNanos < 0) { - sb.append('-'); - totalNanos = -totalNanos; + private static int scanAsciiDigits(String text, int i) { + while (i < text.length() && isAsciiDigit(text.charAt(i))) { + i++; } + return i; + } + + private static boolean isAsciiDigit(char c) { + return c >= '0' && c <= '9'; + } - long seconds = totalNanos / 1_000_000_000; - int nanos = (int) (totalNanos % 1_000_000_000); - - sb.append(seconds); - - // Follows ProtoTimeUtils.toString(Duration) implementation - if (nanos > 0) { - sb.append('.'); - if (nanos % 1_000_000 == 0) { - // Millisecond precision (3 digits) - int millis = nanos / 1_000_000; - sb.append(String.format(Locale.US, "%03d", millis)); - } else if (nanos % 1_000 == 0) { - // Microsecond precision (6 digits) - int micros = nanos / 1_000; - sb.append(String.format(Locale.US, "%06d", micros)); - } else { - // Nanosecond precision (9 digits) - sb.append(String.format(Locale.US, "%09d", nanos)); + private static long parseUnitNanos(String text, int start, int end) { + int len = end - start; + if (len == 1) { + switch (text.charAt(start)) { + case 's': + return NANOS_PER_SECOND; + case 'm': + return 60L * NANOS_PER_SECOND; + case 'h': + return 3600L * NANOS_PER_SECOND; + default: + break; + } + } else if (len == 2 && text.charAt(start + 1) == 's') { + switch (text.charAt(start)) { + case 'n': + return 1L; + case 'u': + case '\u00b5': + case '\u03bc': + return 1_000L; + case 'm': + return 1_000_000L; + default: + break; } } + throw invalidDurationFormat(); + } + + private static IllegalArgumentException invalidDurationFormat() { + return new IllegalArgumentException("invalid duration format"); + } - sb.append('s'); - return sb.toString(); + private static IllegalArgumentException outOfRangeDuration(String text) { + return new IllegalArgumentException("Duration is out of valid range: " + text); } /** @@ -205,55 +299,29 @@ private static ZoneId timeZone(String tz) { /** Throws an {@link IllegalArgumentException} if the given {@link Timestamp} is not valid. */ public static void checkValid(Instant instant) { - long seconds = instant.getEpochSecond(); + checkSecondsValid(instant.getEpochSecond()); + } - if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) { + @CanIgnoreReturnValue + private static Duration checkValid(Duration duration) { + if (duration.compareTo(DURATION_MIN) < 0 || duration.compareTo(DURATION_MAX) > 0) { throw new IllegalArgumentException( Strings.lenientFormat( - "Timestamp is not valid. " - + "Seconds (%s) must be in range [-62,135,596,800, +253,402,300,799]. " - + "Nanos (%s) must be in range [0, +999,999,999].", - seconds, instant.getNano())); + "Duration is out of valid range [-315,576,000,000.999999999s," + + " +315,576,000,000.999999999s]: %s", + duration)); } + return duration; } - /** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */ - private static void checkValid(Duration duration) { - long seconds = duration.getSeconds(); - int nanos = duration.getNano(); - if (!isDurationValid(seconds, nanos)) { + private static void checkSecondsValid(long seconds) { + if (seconds < TIMESTAMP_SECONDS_MIN || seconds > TIMESTAMP_SECONDS_MAX) { throw new IllegalArgumentException( Strings.lenientFormat( - "Duration is not valid. " - + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " - + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " - + "Nanos must have the same sign as seconds", - seconds, nanos)); - } - } - - /** - * Returns true if the given number of seconds and nanos is a valid {@link Duration}. The {@code - * seconds} value must be in the range [-315,576,000,000, +315,576,000,000]. The {@code nanos} - * value must be in the range [-999,999,999, +999,999,999]. - * - *

Note: Durations less than one second are represented with a 0 {@code seconds} field - * and a positive or negative {@code nanos} field. For durations of one second or more, a non-zero - * value for the {@code nanos} field must be of the same sign as the {@code seconds} field. - */ - private static boolean isDurationValid(long seconds, int nanos) { - if (seconds < DURATION_SECONDS_MIN || seconds > DURATION_SECONDS_MAX) { - return false; - } - if (nanos < -999999999L || nanos >= NANOS_PER_SECOND) { - return false; - } - if (seconds < 0 || nanos < 0) { - if (seconds > 0 || nanos > 0) { - return false; - } + "Timestamp is not valid. Seconds (%s) must be in range [-62,135,596,800," + + " +253,402,300,799].", + seconds)); } - return true; } private DateTimeHelpers() {} diff --git a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java index b5405382b..63a275f5f 100644 --- a/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java +++ b/common/src/main/java/dev/cel/common/internal/ProtoTimeUtils.java @@ -14,6 +14,7 @@ package dev.cel.common.internal; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.math.LongMath.checkedAdd; import static com.google.common.math.LongMath.checkedMultiply; import static com.google.common.math.LongMath.checkedSubtract; @@ -54,10 +55,10 @@ public final class ProtoTimeUtils { public static final long TIMESTAMP_SECONDS_MAX = 253402300799L; public static final long DURATION_SECONDS_MIN = -315576000000L; public static final long DURATION_SECONDS_MAX = 315576000000L; + static final int NANOS_PER_SECOND = 1000000000; + public static final int MAX_NANOS = NANOS_PER_SECOND - 1; private static final int MILLIS_PER_SECOND = 1000; - - private static final int NANOS_PER_SECOND = 1000000000; private static final int NANOS_PER_MILLISECOND = 1000000; private static final int NANOS_PER_MICROSECOND = 1000; @@ -180,7 +181,7 @@ public static long toMillis(Duration duration) { /** Create a Timestamp from the number of seconds elapsed from the epoch. */ public static Timestamp fromSecondsToTimestamp(long seconds) { - return normalizedTimestamp(seconds, 0); + return checkValid(normalizedTimestamp(seconds, 0)); } /** Create a Timestamp from the number of seconds elapsed from the epoch. */ @@ -195,20 +196,23 @@ public static Duration fromMillisToDuration(long milliseconds) { (int) (milliseconds % MILLIS_PER_SECOND * NANOS_PER_MILLISECOND)); } + /** Validates {@link Duration} and {@link Timestamp} bounds; returns all other values as-is. */ + // CEL-Java targets Java 8, which does not support pattern-matching instanceof. + @SuppressWarnings("PatternMatchingInstanceof") + @CanIgnoreReturnValue + public static T validateWellKnownProto(T message) { + if (message instanceof Duration) { + checkValid((Duration) message); + } else if (message instanceof Timestamp) { + checkValid((Timestamp) message); + } + return message; + } + /** Throws an {@link IllegalArgumentException} if the given {@link Duration} is not valid. */ @CanIgnoreReturnValue private static Duration checkValid(Duration duration) { - long seconds = duration.getSeconds(); - int nanos = duration.getNanos(); - if (!isDurationValid(seconds, nanos)) { - throw new IllegalArgumentException( - Strings.lenientFormat( - "Duration is not valid. See proto definition for valid values. " - + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " - + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " - + "Nanos must have the same sign as seconds", - seconds, nanos)); - } + checkDurationValid(duration.getSeconds(), duration.getNanos()); return duration; } @@ -228,6 +232,17 @@ private static Timestamp checkValid(Timestamp timestamp) { return timestamp; } + private static void checkDurationValid(long seconds, int nanos) { + checkArgument( + isDurationValid(seconds, nanos), + "Duration is not valid. See proto definition for valid values. " + + "Seconds (%s) must be in range [-315,576,000,000, +315,576,000,000]. " + + "Nanos (%s) must be in range [-999,999,999, +999,999,999]. " + + "Nanos must have the same sign as seconds", + seconds, + nanos); + } + /** * Convert Timestamp to RFC 3339 date string format. The output will always be Z-normalized and * uses 0, 3, 6 or 9 fractional digits as required to represent the exact value. Note that @@ -268,11 +283,12 @@ public static String toString(Timestamp timestamp) { * @throws IllegalArgumentException if the given duration is not in the valid range. */ public static String toString(Duration duration) { - checkValid(duration); - - long seconds = duration.getSeconds(); - int nanos = duration.getNanos(); + return formatDuration(duration.getSeconds(), duration.getNanos()); + } + /** Formats normalized duration components (same sign) as a seconds string (e.g. "-0.500s"). */ + public static String formatDuration(long seconds, int nanos) { + checkDurationValid(seconds, nanos); StringBuilder result = new StringBuilder(); if (seconds < 0 || nanos < 0) { result.append("-"); @@ -355,60 +371,34 @@ public static Timestamp parse(String value) throws ParseException { } } - /** Adds two durations */ + /** Adds two durations. */ public static Duration add(Duration d1, Duration d2) { - java.time.Duration javaDuration1 = ProtoTimeUtils.toJavaDuration(checkValid(d1)); - java.time.Duration javaDuration2 = ProtoTimeUtils.toJavaDuration(checkValid(d2)); - - java.time.Duration sum = javaDuration1.plus(javaDuration2); - - return ProtoTimeUtils.toProtoDuration(sum); + return checkValid( + toProtoDuration(toJavaDuration(checkValid(d1)).plus(toJavaDuration(checkValid(d2))))); } - /** Adds two timestamps. */ + /** Adds a duration to a timestamp. */ public static Timestamp add(Timestamp ts, Duration dur) { - Instant javaInstant = ProtoTimeUtils.toJavaInstant(checkValid(ts)); - java.time.Duration javaDuration = ProtoTimeUtils.toJavaDuration(checkValid(dur)); - - Instant newInstant = javaInstant.plus(javaDuration); - - return ProtoTimeUtils.toProtoTimestamp(newInstant); + return checkValid( + toProtoTimestamp(toJavaInstant(checkValid(ts)).plus(toJavaDuration(checkValid(dur))))); } - /** Subtract a duration from another. */ + /** Subtracts a duration from another. */ public static Duration subtract(Duration d1, Duration d2) { - java.time.Duration javaDuration1 = ProtoTimeUtils.toJavaDuration(checkValid(d1)); - java.time.Duration javaDuration2 = ProtoTimeUtils.toJavaDuration(checkValid(d2)); - - java.time.Duration sum = javaDuration1.minus(javaDuration2); - - return ProtoTimeUtils.toProtoDuration(sum); + return checkValid( + toProtoDuration(toJavaDuration(checkValid(d1)).minus(toJavaDuration(checkValid(d2))))); } - /** Subtracts two timestamps */ + /** Subtracts a duration from a timestamp. */ public static Timestamp subtract(Timestamp ts, Duration dur) { - Instant javaInstant = ProtoTimeUtils.toJavaInstant(checkValid(ts)); - java.time.Duration javaDuration = ProtoTimeUtils.toJavaDuration(checkValid(dur)); - - Instant newInstant = javaInstant.minus(javaDuration); - - return ProtoTimeUtils.toProtoTimestamp(newInstant); + return checkValid( + toProtoTimestamp(toJavaInstant(checkValid(ts)).minus(toJavaDuration(checkValid(dur))))); } - /** Calculate the difference between two timestamps. */ - public static Duration between(Timestamp from, Timestamp to, boolean validateOverflow) { - Instant javaFrom = ProtoTimeUtils.toJavaInstant(checkValid(from)); - Instant javaTo = ProtoTimeUtils.toJavaInstant(checkValid(to)); - - java.time.Duration between = java.time.Duration.between(javaFrom, javaTo); - if (validateOverflow) { - // Call toNanos() to validate 64-bit nanosecond overflow (throws ArithmeticException). - // Suppress unused variable warning as the duration object itself is returned. - @SuppressWarnings("unused") - long unused = between.toNanos(); - } - - return ProtoTimeUtils.toProtoDuration(between); + /** Calculates the difference between two timestamps. */ + public static Duration between(Timestamp from, Timestamp to) { + return toProtoDuration( + java.time.Duration.between(toJavaInstant(checkValid(from)), toJavaInstant(checkValid(to)))); } /** diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 774f720e4..53d3788bc 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -310,6 +310,7 @@ java_library( "//common/annotations", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", + "//common/internal:proto_time_utils", "//common/values", "//common/values:cel_value_provider", "@maven//:com_google_errorprone_error_prone_annotations", diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java index 7beb40c61..8dc8a55cc 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageValueProvider.java @@ -23,6 +23,7 @@ import dev.cel.common.internal.DynamicProto; import dev.cel.common.internal.ProtoAdapter; import dev.cel.common.internal.ProtoMessageFactory; +import dev.cel.common.internal.ProtoTimeUtils; import java.util.Map; import java.util.Optional; @@ -61,7 +62,9 @@ public Optional newValue(String structType, Map fields) fieldValue.ifPresent(o -> builder.setField(fieldDescriptor, o)); } - return Optional.of(protoCelValueConverter.toRuntimeValue(builder.build())); + return Optional.of( + protoCelValueConverter.toRuntimeValue( + ProtoTimeUtils.validateWellKnownProto(builder.build()))); } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException(e.getMessage()); } diff --git a/common/src/test/java/dev/cel/common/internal/BUILD.bazel b/common/src/test/java/dev/cel/common/internal/BUILD.bazel index a70489fb4..19afc5f89 100644 --- a/common/src/test/java/dev/cel/common/internal/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/internal/BUILD.bazel @@ -23,6 +23,7 @@ java_library( "//common/internal:cel_descriptor_pools", "//common/internal:comparison_functions", "//common/internal:converter", + "//common/internal:date_time_helpers", "//common/internal:default_instance_message_factory", "//common/internal:default_lite_descriptor_pool", "//common/internal:default_message_factory", @@ -47,6 +48,7 @@ java_library( "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_testparameterinjector_test_parameter_injector", + "@maven//:com_google_truth_extensions_truth_proto_extension", "@maven//:junit_junit", "@maven_android//:com_google_protobuf_protobuf_javalite", ], diff --git a/common/src/test/java/dev/cel/common/internal/DateTimeHelpersTest.java b/common/src/test/java/dev/cel/common/internal/DateTimeHelpersTest.java new file mode 100644 index 000000000..78ca5e021 --- /dev/null +++ b/common/src/test/java/dev/cel/common/internal/DateTimeHelpersTest.java @@ -0,0 +1,188 @@ +// 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 +// +// 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.internal; + +import static com.google.common.truth.Truth.assertThat; +import static dev.cel.common.internal.ProtoTimeUtils.MAX_NANOS; +import static dev.cel.common.internal.ProtoTimeUtils.TIMESTAMP_SECONDS_MAX; +import static dev.cel.common.internal.ProtoTimeUtils.TIMESTAMP_SECONDS_MIN; +import static org.junit.Assert.assertThrows; + +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; +import java.time.Duration; +import java.time.Instant; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +// Parameterized tests construct Duration/Instant boundaries from raw seconds and nano adjustments. +@SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") +public final class DateTimeHelpersTest { + + @Test + @TestParameters("{text: '0', expectedSeconds: 0, expectedNanoAdjustment: 0}") + @TestParameters("{text: '+0', expectedSeconds: 0, expectedNanoAdjustment: 0}") + @TestParameters("{text: '-0', expectedSeconds: 0, expectedNanoAdjustment: 0}") + @TestParameters("{text: '0s', expectedSeconds: 0, expectedNanoAdjustment: 0}") + @TestParameters("{text: '+1s', expectedSeconds: 1, expectedNanoAdjustment: 0}") + @TestParameters("{text: '-1s', expectedSeconds: -1, expectedNanoAdjustment: 0}") + @TestParameters("{text: '1ns', expectedSeconds: 0, expectedNanoAdjustment: 1}") + @TestParameters("{text: '1us', expectedSeconds: 0, expectedNanoAdjustment: 1000}") + @TestParameters("{text: '1\u00b5s', expectedSeconds: 0, expectedNanoAdjustment: 1000}") + @TestParameters("{text: '1\u03bcs', expectedSeconds: 0, expectedNanoAdjustment: 1000}") + @TestParameters("{text: '1ms', expectedSeconds: 0, expectedNanoAdjustment: 1000000}") + @TestParameters("{text: '1m', expectedSeconds: 60, expectedNanoAdjustment: 0}") + @TestParameters("{text: '1h', expectedSeconds: 3600, expectedNanoAdjustment: 0}") + @TestParameters("{text: '1m2s', expectedSeconds: 62, expectedNanoAdjustment: 0}") + @TestParameters("{text: '1m.5s', expectedSeconds: 60, expectedNanoAdjustment: 500000000}") + @TestParameters("{text: '.5s', expectedSeconds: 0, expectedNanoAdjustment: 500000000}") + @TestParameters("{text: '1.s', expectedSeconds: 1, expectedNanoAdjustment: 0}") + @TestParameters("{text: '0.1234567899s', expectedSeconds: 0, expectedNanoAdjustment: 123456789}") + @TestParameters( + "{text: '-0.1234567899s', expectedSeconds: 0, expectedNanoAdjustment: -123456789}") + @TestParameters( + "{text: '0.0900000000000000000s', expectedSeconds: 0, expectedNanoAdjustment: 90000000}") + @TestParameters( + "{text: '0.999999999999999999h', expectedSeconds: 3599, expectedNanoAdjustment: 999999999}") + @TestParameters( + "{text: '-9223372036854775808ns'," + + " expectedSeconds: -9223372036, expectedNanoAdjustment: -854775808}") + @TestParameters( + "{text: '315576000000s', expectedSeconds: 315576000000, expectedNanoAdjustment: 0}") + @TestParameters( + "{text: '315576000000999999999ns'," + + " expectedSeconds: 315576000000, expectedNanoAdjustment: 999999999}") + @TestParameters( + "{text: '-315576000000999999999ns'," + + " expectedSeconds: -315576000000, expectedNanoAdjustment: -999999999}") + @TestParameters( + "{text: '315576000000.999999999s'," + + " expectedSeconds: 315576000000, expectedNanoAdjustment: 999999999}") + @TestParameters( + "{text: '-315576000000.999999999s'," + + " expectedSeconds: -315576000000, expectedNanoAdjustment: -999999999}") + public void parseDuration_valid(String text, long expectedSeconds, long expectedNanoAdjustment) { + assertThat(DateTimeHelpers.parseDuration(text)) + .isEqualTo(Duration.ofSeconds(expectedSeconds, expectedNanoAdjustment)); + } + + @Test + @TestParameters("{text: ''}") + @TestParameters("{text: '+'}") + @TestParameters("{text: '-'}") + @TestParameters("{text: '1'}") + @TestParameters("{text: '1.'}") + @TestParameters("{text: '1.5'}") + @TestParameters("{text: '.'}") + @TestParameters("{text: '00'}") + @TestParameters("{text: '0x'}") + @TestParameters("{text: '.s'}") + @TestParameters("{text: 's'}") + @TestParameters("{text: '/s'}") + @TestParameters("{text: ':s'}") + @TestParameters("{text: '-1-1s'}") + @TestParameters("{text: 'bad'}") + @TestParameters("{text: 'inf'}") + @TestParameters("{text: '1d'}") + @TestParameters("{text: '1ma'}") + @TestParameters("{text: '1xs'}") + @TestParameters("{text: '999999999999999999999x'}") + public void parseDuration_invalidFormatThrows(String text) { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> DateTimeHelpers.parseDuration(text)); + + assertThat(e).hasMessageThat().isEqualTo("invalid duration format"); + } + + @Test + @TestParameters("{text: '315576000001s'}") + @TestParameters("{text: '-315576000001s'}") + @TestParameters("{text: '315576000000s1000000000ns'}") + @TestParameters("{text: '315576000000.5s0.5s'}") + @TestParameters("{text: '315576000000.999999999s1ns'}") + @TestParameters("{text: '-315576000000.999999999s1ns'}") + @TestParameters("{text: '18446744073709551616s'}") + public void parseDuration_outOfRangeThrows(String text) { + IllegalArgumentException e = + assertThrows(IllegalArgumentException.class, () -> DateTimeHelpers.parseDuration(text)); + + assertThat(e).hasMessageThat().contains("range"); + } + + @Test + @TestParameters("{seconds: -62135596800}") + @TestParameters("{seconds: 0}") + @TestParameters("{seconds: 253402300799}") + public void fromSecondsToInstant_valid(long seconds) { + assertThat(DateTimeHelpers.fromSecondsToInstant(seconds)) + .isEqualTo(Instant.ofEpochSecond(seconds)); + } + + @Test + @TestParameters("{seconds: -62135596801}") + @TestParameters("{seconds: 253402300800}") + @TestParameters("{seconds: -9223372036854775808}") + @TestParameters("{seconds: 9223372036854775807}") + public void fromSecondsToInstant_outOfRangeThrows(long seconds) { + assertThrows( + IllegalArgumentException.class, () -> DateTimeHelpers.fromSecondsToInstant(seconds)); + } + + @Test + public void between_fullTimestampRange() { + Instant minTs = Instant.ofEpochSecond(TIMESTAMP_SECONDS_MIN); + Instant maxTs = Instant.ofEpochSecond(TIMESTAMP_SECONDS_MAX, MAX_NANOS); + + assertThat(DateTimeHelpers.between(minTs, maxTs)) + .isEqualTo(Duration.ofSeconds(TIMESTAMP_SECONDS_MAX - TIMESTAMP_SECONDS_MIN, MAX_NANOS)); + assertThat(DateTimeHelpers.between(maxTs, minTs)) + .isEqualTo(Duration.ofSeconds(TIMESTAMP_SECONDS_MIN - TIMESTAMP_SECONDS_MAX, -MAX_NANOS)); + } + + @Test + @TestParameters("{endSeconds: 315576000001}") + @TestParameters("{endSeconds: -315576000001}") + public void between_outOfRangeThrows(long endSeconds) { + Instant end = Instant.ofEpochSecond(endSeconds); + + assertThrows(IllegalArgumentException.class, () -> DateTimeHelpers.between(Instant.EPOCH, end)); + } + + @Test + @TestParameters("{seconds: 0, nanoAdjustment: 0, expected: '0s'}") + @TestParameters("{seconds: 0, nanoAdjustment: 500, expected: '0.000000500s'}") + @TestParameters("{seconds: 0, nanoAdjustment: -500, expected: '-0.000000500s'}") + @TestParameters("{seconds: -1, nanoAdjustment: 0, expected: '-1s'}") + @TestParameters("{seconds: -1, nanoAdjustment: -500, expected: '-1.000000500s'}") + @TestParameters( + "{seconds: 315576000000, nanoAdjustment: 999999999, expected: '315576000000.999999999s'}") + @TestParameters( + "{seconds: -315576000000, nanoAdjustment: -999999999, expected: '-315576000000.999999999s'}") + public void toString_duration(long seconds, long nanoAdjustment, String expected) { + assertThat(DateTimeHelpers.toString(Duration.ofSeconds(seconds, nanoAdjustment))) + .isEqualTo(expected); + } + + @Test + @TestParameters("{seconds: 315576000000, nanoAdjustment: 1000000000}") + @TestParameters("{seconds: -315576000000, nanoAdjustment: -1000000000}") + @TestParameters("{seconds: -9223372036854775808, nanoAdjustment: 0}") + public void toString_duration_outOfRangeThrows(long seconds, long nanoAdjustment) { + Duration duration = Duration.ofSeconds(seconds, nanoAdjustment); + + assertThrows(IllegalArgumentException.class, () -> DateTimeHelpers.toString(duration)); + } +} diff --git a/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java b/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java index 31ee9a9eb..d73c19e83 100644 --- a/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java +++ b/common/src/test/java/dev/cel/common/internal/ProtoTimeUtilsTest.java @@ -15,44 +15,49 @@ package dev.cel.common.internal; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static dev.cel.common.internal.ProtoTimeUtils.DURATION_SECONDS_MAX; import static dev.cel.common.internal.ProtoTimeUtils.DURATION_SECONDS_MIN; +import static dev.cel.common.internal.ProtoTimeUtils.DURATION_ZERO; +import static dev.cel.common.internal.ProtoTimeUtils.MAX_NANOS; +import static dev.cel.common.internal.ProtoTimeUtils.TIMESTAMP_SECONDS_MAX; +import static dev.cel.common.internal.ProtoTimeUtils.TIMESTAMP_SECONDS_MIN; +import static org.junit.Assert.assertThrows; import com.google.protobuf.Duration; import com.google.protobuf.Timestamp; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; import java.time.Instant; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) +// ProtoTimeUtils operates directly on protobuf Duration/Timestamp seconds and nanos fields. +@SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") public class ProtoTimeUtilsTest { @Test public void toJavaInstant_overRfc3339Range() { - Timestamp ts = - Timestamp.newBuilder().setSeconds(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX + 1).build(); + Timestamp ts = Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MAX + 1).build(); Instant instant = ProtoTimeUtils.toJavaInstant(ts); - assertThat(instant).isEqualTo(Instant.ofEpochSecond(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX + 1)); + assertThat(instant).isEqualTo(Instant.ofEpochSecond(TIMESTAMP_SECONDS_MAX + 1)); } @Test public void toJavaInstant_underRfc3339Range() { - Timestamp ts = - Timestamp.newBuilder().setSeconds(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN - 1).build(); + Timestamp ts = Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MIN - 1).build(); Instant instant = ProtoTimeUtils.toJavaInstant(ts); - assertThat(instant).isEqualTo(Instant.ofEpochSecond(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN - 1)); + assertThat(instant).isEqualTo(Instant.ofEpochSecond(TIMESTAMP_SECONDS_MIN - 1)); } @Test public void toJavaDuration_overRfc3339Range() { - Duration d = Duration.newBuilder() - .setSeconds(DURATION_SECONDS_MAX + 1) - .build(); + Duration d = Duration.newBuilder().setSeconds(DURATION_SECONDS_MAX + 1).build(); java.time.Duration duration = ProtoTimeUtils.toJavaDuration(d); @@ -61,12 +66,148 @@ public void toJavaDuration_overRfc3339Range() { @Test public void toJavaDuration_underRfc3339Range() { - Duration d = Duration.newBuilder() - .setSeconds(DURATION_SECONDS_MIN - 1) - .build(); + Duration d = Duration.newBuilder().setSeconds(DURATION_SECONDS_MIN - 1).build(); java.time.Duration duration = ProtoTimeUtils.toJavaDuration(d); assertThat(duration).isEqualTo(java.time.Duration.ofSeconds(DURATION_SECONDS_MIN - 1)); } + + @Test + @TestParameters("{seconds: -62135596800}") + @TestParameters("{seconds: 0}") + @TestParameters("{seconds: 253402300799}") + public void fromSecondsToTimestamp_valid(long seconds) { + assertThat(ProtoTimeUtils.fromSecondsToTimestamp(seconds)) + .isEqualTo(Timestamp.newBuilder().setSeconds(seconds).build()); + } + + @Test + @TestParameters("{seconds: -62135596801}") + @TestParameters("{seconds: 253402300800}") + @TestParameters("{seconds: -9223372036854775808}") + @TestParameters("{seconds: 9223372036854775807}") + public void fromSecondsToTimestamp_outOfRangeThrows(long seconds) { + assertThrows( + IllegalArgumentException.class, () -> ProtoTimeUtils.fromSecondsToTimestamp(seconds)); + } + + @Test + @TestParameters("{seconds: 0, nanos: 0, expected: '0s'}") + @TestParameters("{seconds: 1, nanos: 100000000, expected: '1.100s'}") + @TestParameters("{seconds: 1, nanos: 100000, expected: '1.000100s'}") + @TestParameters("{seconds: 1, nanos: 100, expected: '1.000000100s'}") + @TestParameters("{seconds: 0, nanos: -500000000, expected: '-0.500s'}") + @TestParameters("{seconds: 315576000000, nanos: 999999999, expected: '315576000000.999999999s'}") + @TestParameters( + "{seconds: -315576000000, nanos: -999999999, expected: '-315576000000.999999999s'}") + public void toString_duration(long seconds, int nanos, String expected) { + Duration d = Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + + assertThat(ProtoTimeUtils.toString(d)).isEqualTo(expected); + } + + @Test + @TestParameters("{seconds: -315576000001, nanos: 0}") + @TestParameters("{seconds: 315576000001, nanos: 0}") + @TestParameters("{seconds: 0, nanos: -1000000000}") + @TestParameters("{seconds: 0, nanos: 1000000000}") + @TestParameters("{seconds: 1, nanos: -1}") + @TestParameters("{seconds: -1, nanos: 1}") + public void toString_duration_invalidThrows(long seconds, int nanos) { + Duration invalid = Duration.newBuilder().setSeconds(seconds).setNanos(nanos).build(); + + assertThrows(IllegalArgumentException.class, () -> ProtoTimeUtils.toString(invalid)); + } + + @Test + public void addAndSubtract_duration_atBounds() { + Duration max = + Duration.newBuilder().setSeconds(DURATION_SECONDS_MAX).setNanos(MAX_NANOS).build(); + Duration min = + Duration.newBuilder().setSeconds(DURATION_SECONDS_MIN).setNanos(-MAX_NANOS).build(); + + assertThat(ProtoTimeUtils.add(max, DURATION_ZERO)).isEqualTo(max); + assertThat(ProtoTimeUtils.subtract(min, DURATION_ZERO)).isEqualTo(min); + } + + @Test + @TestParameters("{d1Seconds: 315576000000, d1Nanos: 999999999, d2Nanos: 1}") + @TestParameters("{d1Seconds: -315576000000, d1Nanos: -999999999, d2Nanos: -1}") + public void add_duration_outOfRangeThrows(long d1Seconds, int d1Nanos, int d2Nanos) { + Duration d1 = Duration.newBuilder().setSeconds(d1Seconds).setNanos(d1Nanos).build(); + Duration d2 = Duration.newBuilder().setNanos(d2Nanos).build(); + + assertThrows(IllegalArgumentException.class, () -> ProtoTimeUtils.add(d1, d2)); + } + + @Test + @TestParameters("{d1Seconds: -315576000000, d1Nanos: -999999999, d2Nanos: 1}") + @TestParameters("{d1Seconds: 315576000000, d1Nanos: 999999999, d2Nanos: -1}") + public void subtract_duration_outOfRangeThrows(long d1Seconds, int d1Nanos, int d2Nanos) { + Duration d1 = Duration.newBuilder().setSeconds(d1Seconds).setNanos(d1Nanos).build(); + Duration d2 = Duration.newBuilder().setNanos(d2Nanos).build(); + + assertThrows(IllegalArgumentException.class, () -> ProtoTimeUtils.subtract(d1, d2)); + } + + @Test + public void addAndSubtract_timestamp_atBounds() { + Timestamp max = + Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MAX).setNanos(MAX_NANOS).build(); + Timestamp min = Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MIN).build(); + + assertThat(ProtoTimeUtils.add(max, DURATION_ZERO)).isEqualTo(max); + assertThat(ProtoTimeUtils.subtract(min, DURATION_ZERO)).isEqualTo(min); + } + + @Test + @TestParameters("{tsSeconds: 253402300799, tsNanos: 999999999, durNanos: 1}") + @TestParameters("{tsSeconds: -62135596800, tsNanos: 0, durNanos: -1}") + public void add_timestamp_outOfRangeThrows(long tsSeconds, int tsNanos, int durNanos) { + Timestamp ts = Timestamp.newBuilder().setSeconds(tsSeconds).setNanos(tsNanos).build(); + Duration dur = Duration.newBuilder().setNanos(durNanos).build(); + + assertThrows(IllegalArgumentException.class, () -> ProtoTimeUtils.add(ts, dur)); + } + + @Test + @TestParameters("{tsSeconds: -62135596800, tsNanos: 0, durNanos: 1}") + @TestParameters("{tsSeconds: 253402300799, tsNanos: 999999999, durNanos: -1}") + public void subtract_timestamp_outOfRangeThrows(long tsSeconds, int tsNanos, int durNanos) { + Timestamp ts = Timestamp.newBuilder().setSeconds(tsSeconds).setNanos(tsNanos).build(); + Duration dur = Duration.newBuilder().setNanos(durNanos).build(); + + assertThrows(IllegalArgumentException.class, () -> ProtoTimeUtils.subtract(ts, dur)); + } + + @Test + public void between_fullTimestampRange() { + Timestamp min = Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MIN).build(); + Timestamp max = + Timestamp.newBuilder().setSeconds(TIMESTAMP_SECONDS_MAX).setNanos(MAX_NANOS).build(); + + assertThat(ProtoTimeUtils.between(min, max)) + .isEqualTo( + Duration.newBuilder() + .setSeconds(TIMESTAMP_SECONDS_MAX - TIMESTAMP_SECONDS_MIN) + .setNanos(MAX_NANOS) + .build()); + assertThat(ProtoTimeUtils.between(max, min)) + .isEqualTo( + Duration.newBuilder() + .setSeconds(TIMESTAMP_SECONDS_MIN - TIMESTAMP_SECONDS_MAX) + .setNanos(-MAX_NANOS) + .build()); + } + + @Test + @TestParameters("{fromSeconds: 0, fromNanos: 1000000000, toSeconds: 253402300799}") + @TestParameters("{fromSeconds: -62135596800, fromNanos: 0, toSeconds: 253402300800}") + public void between_invalidTimestampThrows(long fromSeconds, int fromNanos, long toSeconds) { + Timestamp from = Timestamp.newBuilder().setSeconds(fromSeconds).setNanos(fromNanos).build(); + Timestamp to = Timestamp.newBuilder().setSeconds(toSeconds).build(); + + assertThrows(IllegalArgumentException.class, () -> ProtoTimeUtils.between(from, to)); + } } diff --git a/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java index ed8fc7b2a..bc857eee4 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageValueProviderTest.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.primitives.UnsignedLong; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelOptions; import dev.cel.common.internal.CelDescriptorPool; @@ -37,6 +38,8 @@ import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) +// Tests construct Duration/Instant boundaries and parameterized bounds from raw seconds and nanos. +@SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") public class ProtoMessageValueProviderTest { private static final CelDescriptorPool DESCRIPTOR_POOL = @@ -47,6 +50,8 @@ public class ProtoMessageValueProviderTest { private static final ProtoMessageFactory MESSAGE_FACTORY = DefaultMessageFactory.create(DESCRIPTOR_POOL); private static final DynamicProto DYNAMIC_PROTO = DynamicProto.create(MESSAGE_FACTORY); + private static final ProtoMessageValueProvider VALUE_PROVIDER = + ProtoMessageValueProvider.newInstance(CelOptions.DEFAULT, DYNAMIC_PROTO); @Test public void newValue_createEmptyProtoMessage() { @@ -226,4 +231,31 @@ public void newValue_onCombinedProvider() { assertThat(protoMessageValue.isZeroValue()).isFalse(); assertThat(protoMessageValue.select("single_int32")).isEqualTo(1L); } + + @Test + public void newValue_durationAndTimestamp_atBounds() { + assertThat( + VALUE_PROVIDER.newValue( + "google.protobuf.Duration", + ImmutableMap.of("seconds", 315576000000L, "nanos", 999999999L))) + .hasValue(Duration.ofSeconds(315576000000L, 999999999L)); + assertThat( + VALUE_PROVIDER.newValue( + "google.protobuf.Timestamp", + ImmutableMap.of("seconds", 253402300799L, "nanos", 999999999L))) + .hasValue(Instant.ofEpochSecond(253402300799L, 999999999L)); + } + + @Test + @TestParameters("{typeName: 'google.protobuf.Duration', seconds: 315576000001, nanos: 0}") + @TestParameters("{typeName: 'google.protobuf.Duration', seconds: 1, nanos: -1}") + @TestParameters("{typeName: 'google.protobuf.Timestamp', seconds: 253402300800, nanos: 0}") + @TestParameters("{typeName: 'google.protobuf.Timestamp', seconds: 0, nanos: -1}") + public void newValue_durationAndTimestamp_outOfRangeThrows( + String typeName, long seconds, long nanos) { + assertThrows( + IllegalArgumentException.class, + () -> + VALUE_PROVIDER.newValue(typeName, ImmutableMap.of("seconds", seconds, "nanos", nanos))); + } } diff --git a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel index 8a455005f..ae7abe40e 100644 --- a/conformance/src/test/java/dev/cel/conformance/BUILD.bazel +++ b/conformance/src/test/java/dev/cel/conformance/BUILD.bazel @@ -137,30 +137,7 @@ _ALL_TESTS = [ "@cel_spec//tests/simple:testdata/wrappers.textproto", ] -_TESTS_TO_SKIP_SPEC_IMPORT = [ - # Skip until duration/timestamp range and proto wrapper validation are updated for spec parity. - "timestamps/duration_conversions/from_string_max", - "timestamps/duration_conversions/from_string_min", - "timestamps/duration_conversions/toString_duration_max", - "timestamps/duration_conversions/toString_duration_min", - "timestamps/timestamp_arithmetic/add_duration_to_duration_max", - "timestamps/timestamp_arithmetic/subtract_duration_from_duration_min", - "timestamps/timestamp_range/sub_time_duration_over", - "timestamps/timestamp_range/sub_time_duration_under", - "wrappers/duration/nanos_over", - "wrappers/duration/nanos_under", - "wrappers/duration/negative_seconds_positive_nanos", - "wrappers/duration/positive_seconds_negative_nanos", - "wrappers/duration/seconds_over", - "wrappers/duration/seconds_under", - "wrappers/timestamp/nanos_over", - "wrappers/timestamp/nanos_under", - "wrappers/timestamp/negative_seconds_negative_nanos", - "wrappers/timestamp/seconds_over", - "wrappers/timestamp/seconds_under", -] - -_TESTS_TO_SKIP_LEGACY = _TESTS_TO_SKIP_SPEC_IMPORT + [ +_TESTS_TO_SKIP_LEGACY = [ # Broken test cases which should be supported. # TODO: Support setting / getting enum values out of the defined enum value range. @@ -197,7 +174,7 @@ _TESTS_TO_SKIP_LEGACY = _TESTS_TO_SKIP_SPEC_IMPORT + [ "type_deductions/type_decay", ] -_TESTS_TO_SKIP_PLANNER = _TESTS_TO_SKIP_SPEC_IMPORT + [ +_TESTS_TO_SKIP_PLANNER = [ # TODO: This is actually a user experience degradation. # Not worth fixing until we see a concrete need. "basic/functions/unbound_is_runtime_error", diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index f065c750c..0851ae5e2 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -102,6 +102,7 @@ java_library( "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", + "//common/internal:proto_time_utils", "//common/types:cel_types", "//common/values:cel_byte_string", "@maven//:com_google_errorprone_error_prone_annotations", @@ -431,10 +432,11 @@ cel_android_library( "//common/exceptions:invalid_argument", "//common/exceptions:numeric_overflow", "//common/internal:converter", + "//common/internal:date_time_helpers_android", + "//common/internal:proto_time_utils_android", "//common/values:values_android", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_re2j_re2j", - "@maven//:org_threeten_threeten_extra", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], @@ -456,12 +458,13 @@ java_library( "//common/exceptions:invalid_argument", "//common/exceptions:numeric_overflow", "//common/internal:converter", + "//common/internal:date_time_helpers", + "//common/internal:proto_time_utils", "//common/values", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", "@maven//:com_google_protobuf_protobuf_java", "@maven//:com_google_re2j_re2j", - "@maven//:org_threeten_threeten_extra", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java index ecbba5e7e..cc14518d1 100644 --- a/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java +++ b/runtime/src/main/java/dev/cel/runtime/DescriptorMessageProvider.java @@ -27,6 +27,7 @@ import dev.cel.common.internal.DynamicProto; import dev.cel.common.internal.ProtoAdapter; import dev.cel.common.internal.ProtoMessageFactory; +import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.CelTypes; import dev.cel.common.values.CelByteString; import java.util.Map; @@ -89,7 +90,7 @@ public DescriptorMessageProvider(ProtoMessageFactory protoMessageFactory, CelOpt protoAdapter.adaptValueToFieldType(fieldDescriptor, entry.getValue()); fieldValue.ifPresent(o -> builder.setField(fieldDescriptor, o)); } - return protoAdapter.adaptProtoToValue(builder.build()); + return protoAdapter.adaptProtoToValue(ProtoTimeUtils.validateWellKnownProto(builder.build())); } catch (ArrayIndexOutOfBoundsException e) { throw new IllegalArgumentException(e.getMessage()); } diff --git a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java index fb2ef35d6..7e4a95ddd 100644 --- a/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java +++ b/runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java @@ -14,8 +14,6 @@ package dev.cel.runtime; -import static com.google.common.base.Preconditions.checkArgument; - import com.google.common.primitives.Ints; import com.google.common.primitives.UnsignedLong; import com.google.common.primitives.UnsignedLongs; @@ -31,12 +29,12 @@ import dev.cel.common.exceptions.CelInvalidArgumentException; import dev.cel.common.exceptions.CelNumericOverflowException; import dev.cel.common.internal.Converter; +import dev.cel.common.internal.DateTimeHelpers; +import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.values.NullValue; -import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import org.threeten.extra.AmountFormats; /** * Helper methods for common CEL related routines. @@ -47,10 +45,6 @@ @Internal public class RuntimeHelpers { - // Maximum and minimum range supported by protobuf Duration values. - private static final java.time.Duration DURATION_MAX = java.time.Duration.ofDays(3652500); - private static final java.time.Duration DURATION_MIN = DURATION_MAX.negated(); - public static RuntimeHelpers create() { return new RuntimeHelpers(); } @@ -60,22 +54,12 @@ public static RuntimeHelpers create() { /** Convert a string to a Protobuf Duration. */ public static Duration createDurationFromString(String d) { - java.time.Duration dv = createJavaDurationFromString(d); - return Duration.newBuilder().setSeconds(dv.getSeconds()).setNanos(dv.getNano()).build(); + return ProtoTimeUtils.toProtoDuration(createJavaDurationFromString(d)); } /** Convert a string to a native Java Duration. */ public static java.time.Duration createJavaDurationFromString(String d) { - try { - java.time.Duration dv = AmountFormats.parseUnitBasedDuration(d); - // Ensure that the duration value can be adequately represented within a protobuf.Duration. - checkArgument( - dv.compareTo(DURATION_MAX) <= 0 && dv.compareTo(DURATION_MIN) >= 0, - "invalid duration range"); - return dv; - } catch (DateTimeParseException e) { - throw new IllegalArgumentException("invalid duration format", e); - } + return DateTimeHelpers.parseDuration(d); } public static Pattern compileRegexPattern(String regexp, CelOptions celOptions) { diff --git a/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java index 822c3066a..f9f39e238 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/SubtractOperator.java @@ -68,33 +68,13 @@ public enum SubtractOverload implements CelStandardOverload { "subtract_timestamp_timestamp", Instant.class, Instant.class, - (Instant i1, Instant i2) -> { - java.time.Duration between = java.time.Duration.between(i2, i1); - if (celOptions.enableTimestampOverflowCheck()) { - try { - // Call toNanos() to validate 64-bit nanosecond overflow (throws - // ArithmeticException). - @SuppressWarnings("unused") - long unused = between.toNanos(); - } catch (ArithmeticException e) { - throw new CelNumericOverflowException(e); - } - } - return between; - }); + (Instant i1, Instant i2) -> DateTimeHelpers.between(i2, i1)); } else { return CelFunctionBinding.from( "subtract_timestamp_timestamp", Timestamp.class, Timestamp.class, - (Timestamp t1, Timestamp t2) -> { - try { - return ProtoTimeUtils.between( - t2, t1, celOptions.enableTimestampOverflowCheck()); - } catch (ArithmeticException e) { - throw new CelNumericOverflowException(e); - } - }); + (Timestamp t1, Timestamp t2) -> ProtoTimeUtils.between(t2, t1)); } }), SUBTRACT_TIMESTAMP_DURATION( diff --git a/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java b/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java index 00a4ed43e..ce070c3c3 100644 --- a/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java +++ b/runtime/src/main/java/dev/cel/runtime/standard/TimestampFunction.java @@ -80,11 +80,7 @@ public enum TimestampOverload implements CelStandardOverload { (celOptions, runtimeEquality) -> { if (celOptions.evaluateCanonicalTypesToNativeValues()) { return CelFunctionBinding.from( - "int64_to_timestamp", Long.class, epochSecond -> { - Instant instant = Instant.ofEpochSecond(epochSecond); - DateTimeHelpers.checkValid(instant); - return instant; - }); + "int64_to_timestamp", Long.class, DateTimeHelpers::fromSecondsToInstant); } else { return CelFunctionBinding.from( "int64_to_timestamp", Long.class, ProtoTimeUtils::fromSecondsToTimestamp); diff --git a/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java b/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java index 02c8f2b51..73b77e87c 100644 --- a/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java +++ b/runtime/src/test/java/dev/cel/runtime/DescriptorMessageProviderTest.java @@ -16,6 +16,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -27,6 +28,7 @@ import com.google.protobuf.NullValue; import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelDescriptorUtil; import dev.cel.common.CelDescriptors; import dev.cel.common.CelErrorCode; @@ -42,12 +44,16 @@ import dev.cel.expr.conformance.proto2.TestAllTypes.NestedGroup; import dev.cel.expr.conformance.proto2.TestAllTypesExtensions; import dev.cel.expr.conformance.proto2.TestAllTypesProto; +import java.time.Duration; +import java.time.Instant; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) +// Tests construct Duration/Instant boundaries and parameterized bounds from raw seconds and nanos. +@SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") public final class DescriptorMessageProviderTest { private RuntimeTypeProvider provider; @@ -252,4 +258,31 @@ public void createMessage_doubleValue_withCustomMessageProvider() { assertThat(value).isEqualTo(0.0d); } + + @Test + public void createMessage_durationAndTimestamp_atBounds() { + assertThat( + provider.createMessage( + WellKnownProto.DURATION.typeName(), + ImmutableMap.of("seconds", 315576000000L, "nanos", 999999999L))) + .isEqualTo(Duration.ofSeconds(315576000000L, 999999999L)); + assertThat( + provider.createMessage( + WellKnownProto.TIMESTAMP.typeName(), + ImmutableMap.of("seconds", 253402300799L, "nanos", 999999999L))) + .isEqualTo(Instant.ofEpochSecond(253402300799L, 999999999L)); + } + + @Test + @TestParameters("{typeName: 'google.protobuf.Duration', seconds: 315576000001, nanos: 0}") + @TestParameters("{typeName: 'google.protobuf.Duration', seconds: 1, nanos: -1}") + @TestParameters("{typeName: 'google.protobuf.Timestamp', seconds: 253402300800, nanos: 0}") + @TestParameters("{typeName: 'google.protobuf.Timestamp', seconds: 0, nanos: -1}") + public void createMessage_durationAndTimestamp_outOfRangeThrows( + String typeName, long seconds, long nanos) { + assertThrows( + IllegalArgumentException.class, + () -> + provider.createMessage(typeName, ImmutableMap.of("seconds", seconds, "nanos", nanos))); + } } diff --git a/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java index 45b050fd1..941d1d602 100644 --- a/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java +++ b/runtime/src/test/java/dev/cel/runtime/ProtoMessageRuntimeHelpersTest.java @@ -15,6 +15,7 @@ package dev.cel.runtime; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; @@ -35,6 +36,8 @@ import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; import com.google.protobuf.Value; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import com.google.testing.junit.testparameterinjector.TestParameters; import dev.cel.common.CelOptions; import dev.cel.common.exceptions.CelNumericOverflowException; import dev.cel.common.exceptions.CelRuntimeException; @@ -47,9 +50,8 @@ import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public final class ProtoMessageRuntimeHelpersTest { private static final DynamicProto DYNAMIC_PROTO = DynamicProto.create(DefaultMessageFactory.INSTANCE); @@ -57,16 +59,29 @@ public final class ProtoMessageRuntimeHelpersTest { ProtoMessageRuntimeHelpers.create(DYNAMIC_PROTO, CelOptions.DEFAULT); @Test - public void createDurationFromString() throws Exception { - assertThat(ProtoMessageRuntimeHelpers.createDurationFromString("15.11s")) - .isEqualTo(Duration.newBuilder().setSeconds(15).setNanos(110000000).build()); + @TestParameters("{text: '15.11s', expectedSeconds: 15, expectedNanos: 110000000}") + @TestParameters( + "{text: '315576000000s999999999ns'," + + " expectedSeconds: 315576000000, expectedNanos: 999999999}") + @TestParameters( + "{text: '-315576000000s999999999ns'," + + " expectedSeconds: -315576000000, expectedNanos: -999999999}") + // Testing protobuf Duration seconds and nanos fields. + @SuppressWarnings("GoodTime-ApiWithNumericTimeUnit") + public void createDurationFromString_valid(String text, long expectedSeconds, int expectedNanos) { + assertThat(ProtoMessageRuntimeHelpers.createDurationFromString(text)) + .isEqualTo( + Duration.newBuilder().setSeconds(expectedSeconds).setNanos(expectedNanos).build()); } @Test - public void createDurationFromString_outOfRange() throws Exception { + @TestParameters("{text: '-315576000000s1000000000ns'}") + @TestParameters("{text: '315576000000s1000000000ns'}") + @TestParameters("{text: 'bad'}") + public void createDurationFromString_invalidOrOutOfRangeThrows(String text) { assertThrows( IllegalArgumentException.class, - () -> ProtoMessageRuntimeHelpers.createDurationFromString("-320000000000s")); + () -> ProtoMessageRuntimeHelpers.createDurationFromString(text)); } @Test diff --git a/runtime/src/test/resources/wrappers.baseline b/runtime/src/test/resources/wrappers.baseline index d8212059b..bd34a84fb 100644 --- a/runtime/src/test/resources/wrappers.baseline +++ b/runtime/src/test/resources/wrappers.baseline @@ -198,4 +198,22 @@ result: true Source: google.protobuf.Timestamp{ seconds: 253402300800 } =====> bindings: {} +error: evaluation error at test_location:25: Timestamp is not valid. See proto definition for valid values. Seconds (253402300800) must be in range [-62,135,596,800, +253,402,300,799]. Nanos (0) must be in range [0, +999,999,999]. +error_code: INTERNAL_ERROR + +Source: google.protobuf.Duration{ seconds: 315576000001 } +=====> +bindings: {} +error: evaluation error at test_location:24: Duration is not valid. See proto definition for valid values. Seconds (315576000001) must be in range [-315,576,000,000, +315,576,000,000]. Nanos (0) must be in range [-999,999,999, +999,999,999]. Nanos must have the same sign as seconds +error_code: INTERNAL_ERROR + +Source: x.single_timestamp +declare x { + value cel.expr.conformance.proto3.TestAllTypes +} +=====> +bindings: {x=single_timestamp { + seconds: 253402300800 +} +} result: +10000-01-01T00:00:00Z diff --git a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java index bda56a19e..b944f3e36 100644 --- a/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java +++ b/testing/src/main/java/dev/cel/testing/BaseInterpreterTest.java @@ -2154,10 +2154,20 @@ public void wrappers() throws Exception { .buildOrThrow()); clearAllDeclarations(); - // Currently allowed, but will be an error - // See https://github.com/google/cel-spec/pull/501 source = "google.protobuf.Timestamp{ seconds: 253402300800 }"; runTest(); + + source = "google.protobuf.Duration{ seconds: 315576000001 }"; + runTest(); + + declareVariable("x", StructTypeReference.create(TestAllTypes.getDescriptor().getFullName())); + source = "x.single_timestamp"; + runTest( + ImmutableMap.of( + "x", + TestAllTypes.newBuilder() + .setSingleTimestamp(Timestamp.newBuilder().setSeconds(253402300800L)) + .build())); } @Test diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java index 571a005e5..56d8e9844 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3CounterexampleGenerator.java @@ -117,10 +117,15 @@ private String formatValue(Expr expr) { + Instant.ofEpochSecond(seconds.longValue(), nanoAdjustment.longValue()) + "\")"; } else if (decl.equals(typeSystem.durationCons().ConstructorDecl())) { - BigInteger nanos = ((IntNum) args[0]).getBigInteger(); - return nanos.remainder(NANOS_PER_SECOND).signum() == 0 - ? "duration(\"" + nanos.divide(NANOS_PER_SECOND) + "s\")" - : "duration(\"" + nanos + "ns\")"; + BigInteger[] parts = ((IntNum) args[0]).getBigInteger().divideAndRemainder(NANOS_PER_SECOND); + if (!isInRange( + parts[0], ProtoTimeUtils.DURATION_SECONDS_MIN, ProtoTimeUtils.DURATION_SECONDS_MAX)) { + // No CEL duration is out of range, so no literal exists; show the seconds. + return "duration(\"" + parts[0] + "s\")"; + } + return "duration(\"" + + ProtoTimeUtils.formatDuration(parts[0].longValue(), parts[1].intValue()) + + "\")"; } else if (decl.equals(typeSystem.listCons().ConstructorDecl())) { return formatList(args[0]); } else if (decl.equals(typeSystem.mapCons().ConstructorDecl())) { diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3StructTranslator.java b/verifier/src/main/java/dev/cel/verifier/CelZ3StructTranslator.java index c98877185..90b941a67 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3StructTranslator.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3StructTranslator.java @@ -25,6 +25,7 @@ import com.microsoft.z3.IntExpr; import dev.cel.common.CelAbstractSyntaxTree; import dev.cel.common.ast.CelExpr; +import dev.cel.common.internal.ProtoTimeUtils; import dev.cel.common.types.CelKind; import dev.cel.common.types.CelType; import dev.cel.common.types.CelTypeProvider; @@ -293,7 +294,7 @@ private TranslatedValue translateDurationWktStruct( celExpr, createStruct, ast, - CelZ3TypeSystem.MIN_DURATION_NANOS, + -ProtoTimeUtils.MAX_NANOS, (seconds, nanos, totalNanos) -> { BoolExpr signMismatch = ctx.mkOr( diff --git a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java index d164505c4..1e92aed35 100644 --- a/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java +++ b/verifier/src/main/java/dev/cel/verifier/CelZ3TypeSystem.java @@ -56,21 +56,14 @@ public final class CelZ3TypeSystem { /** Number of nanoseconds per second. */ public static final long NANOS_PER_SECOND = 1_000_000_000L; - /** Minimum valid nanosecond component for a protobuf Duration. */ - public static final int MIN_DURATION_NANOS = -999_999_999; - - private static final long MAX_TIMESTAMP_NANOS_PART = 999_999_999L; - private static final String MIN_TIMESTAMP_NANOS = - BigInteger.valueOf(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN) - .multiply(BigInteger.valueOf(NANOS_PER_SECOND)) - .toString(); - + totalNanosString(ProtoTimeUtils.TIMESTAMP_SECONDS_MIN, 0); private static final String MAX_TIMESTAMP_NANOS = - BigInteger.valueOf(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX) - .multiply(BigInteger.valueOf(NANOS_PER_SECOND)) - .add(BigInteger.valueOf(MAX_TIMESTAMP_NANOS_PART)) - .toString(); + totalNanosString(ProtoTimeUtils.TIMESTAMP_SECONDS_MAX, ProtoTimeUtils.MAX_NANOS); + private static final String MIN_TOTAL_DURATION_NANOS = + totalNanosString(ProtoTimeUtils.DURATION_SECONDS_MIN, -ProtoTimeUtils.MAX_NANOS); + private static final String MAX_TOTAL_DURATION_NANOS = + totalNanosString(ProtoTimeUtils.DURATION_SECONDS_MAX, ProtoTimeUtils.MAX_NANOS); private static final String TYPE_CEL_VALUE = "CelValue"; private static final String CONS_BOOL = "Bool"; @@ -775,11 +768,13 @@ public BoolExpr checkTimestampOverflow(ArithExpr result) { } /** - * Checks if the given nanosecond arithmetic expression overflows CEL Duration bounds, which are - * confined to a signed 64-bit integer of nanoseconds (matching Go's {@code time.Duration}). + * Checks if the given nanosecond arithmetic expression overflows CEL Duration bounds ({@code + * [-315576000000.999999999s, 315576000000.999999999s]}). */ public BoolExpr checkDurationOverflow(ArithExpr result) { - return checkIntOverflow(result); + return ctx.mkOr( + ctx.mkGt(result, ctx.mkInt(MAX_TOTAL_DURATION_NANOS)), + ctx.mkLt(result, ctx.mkInt(MIN_TOTAL_DURATION_NANOS))); } /** Checks if the given arithmetic expression overflows a 64-bit unsigned integer. */ @@ -794,6 +789,13 @@ public SeqExpr mkConcatSafe(Expr arg1, Expr arg2) { return ctx.mkConcat((Expr>) arg1, (Expr>) arg2); } + private static String totalNanosString(long seconds, int nanos) { + return BigInteger.valueOf(seconds) + .multiply(BigInteger.valueOf(NANOS_PER_SECOND)) + .add(BigInteger.valueOf(nanos)) + .toString(); + } + @AutoValue abstract static class FuncDeclKey { abstract String name(); diff --git a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java index 4438b4245..5856d6086 100644 --- a/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java +++ b/verifier/src/test/java/dev/cel/verifier/CelVerifierZ3ImplTest.java @@ -203,8 +203,8 @@ private enum IsSatisfiableTestCase { "ts = timestamp\\(\"1969-12-31T23:59:59\\.000000500Z\"\\)"), DURATION_SUBSECOND_SATISFIABLE( "dur == google.protobuf.Duration{seconds: 1, nanos: 500}", - "dur = duration\\(\"1000000500ns\"\\)"), - // Timestamps nested in dyn aren't range constrained, and RFC 3339 can't express these. + "dur = duration\\(\"1\\.000000500s\"\\)"), + // Timestamps and durations nested in dyn aren't range constrained. TIMESTAMP_ABOVE_RANGE_SATISFIABLE( "type(dyn_var) == google.protobuf.Timestamp" + " && dyn_var > google.protobuf.Timestamp{seconds: 253402300799, nanos: 999999999}", @@ -213,6 +213,14 @@ private enum IsSatisfiableTestCase { "type(dyn_var) == google.protobuf.Timestamp" + " && dyn_var < google.protobuf.Timestamp{seconds: -62135596800}", "dyn_var = timestamp\\(-\\d+\\)"), + DURATION_ABOVE_RANGE_SATISFIABLE( + "type(dyn_var) == google.protobuf.Duration" + + " && dyn_var > google.protobuf.Duration{seconds: 315576000000, nanos: 999999999}", + "dyn_var = duration\\(\"\\d+s\"\\)"), + DURATION_BELOW_RANGE_SATISFIABLE( + "type(dyn_var) == google.protobuf.Duration" + + " && dyn_var < google.protobuf.Duration{seconds: -315576000000, nanos: -999999999}", + "dyn_var = duration\\(\"-\\d+s\"\\)"), ; private final String expr; @@ -275,7 +283,17 @@ private enum WitnessTestCase { SUBSECOND_DURATION( "dur == google.protobuf.Duration{seconds: -1, nanos: -500}", "dur", - "duration(\"-1000000500ns\")"), + "duration(\"-1.000000500s\")"), + NEGATIVE_SUBSECOND_DURATION( + "dur == google.protobuf.Duration{nanos: -500}", "dur", "duration(\"-0.000000500s\")"), + MIN_DURATION( + "dur == google.protobuf.Duration{seconds: -315576000000, nanos: -999999999}", + "dur", + "duration(\"-315576000000.999999999s\")"), + MAX_DURATION( + "dur == google.protobuf.Duration{seconds: 315576000000, nanos: 999999999}", + "dur", + "duration(\"315576000000.999999999s\")"), NULL("dyn_var == null", "dyn_var", "null"), EMPTY_LIST("int_list == []", "int_list", "[]"), LIST_WITH_NULL("dyn_list == [true, null]", "dyn_list", "[true, null]"), @@ -666,8 +684,8 @@ private enum IsAlwaysTrueTestCase { + " nanos: 999999999}"), TIMESTAMP_VARIABLE_SECONDS_BOUNDS("int(ts) >= -62135596800 && int(ts) <= 253402300799"), DURATION_VARIABLE_BOUNDS( - "dur >= google.protobuf.Duration{seconds: -9223372036, nanos: -854775808} && dur <=" - + " google.protobuf.Duration{seconds: 9223372036, nanos: 854775807}"), + "dur >= google.protobuf.Duration{seconds: -315576000000, nanos: -999999999} && dur <=" + + " google.protobuf.Duration{seconds: 315576000000, nanos: 999999999}"), CYCLIC_MACRO_SHADOWING_SAFETY("[1].all(x, [x].all(x, x == 1))"), TAUTOLOGY("x > 5 || x <= 5"), LIST_VARIABLE_CONSTRAINED("1 in int_list || !(1 in int_list)"), @@ -2376,14 +2394,18 @@ private enum EquivalenceTestCase { "int(google.protobuf.Timestamp{seconds: 0, nanos: 0} -" + " google.protobuf.Duration{nanos: 1})", "-1"), - DURATION_STRUCT_MAX_GO_DURATION_VALID( - "google.protobuf.Duration{seconds: 9223372036, nanos: 854775807} >" + DURATION_STRUCT_MAX_VALID( + "google.protobuf.Duration{seconds: 315576000000, nanos: 999999999} >" + " google.protobuf.Duration{}", "true"), - DURATION_STRUCT_MIN_GO_DURATION_VALID( - "google.protobuf.Duration{seconds: -9223372036, nanos: -854775808} <" + DURATION_STRUCT_MIN_VALID( + "google.protobuf.Duration{seconds: -315576000000, nanos: -999999999} <" + " google.protobuf.Duration{}", "true"), + TIMESTAMP_SUBTRACT_MAX_RANGE_VALID( + "(timestamp(253402300799) - timestamp(-62135596800)) ==" + + " google.protobuf.Duration{seconds: 315537897599}", + "true"), TIMESTAMP_STRUCT_MIN_VALID( "google.protobuf.Timestamp{seconds: -62135596800, nanos: 0}", "timestamp(-62135596800)"), TIMESTAMP_STRUCT_MAX_VALID( @@ -2577,16 +2599,15 @@ private enum EquivalenceViolationTestCase { "false"), TIMESTAMP_STRUCT_ABOVE_MAX_FAILS( "google.protobuf.Timestamp{seconds: 253402300800, nanos: 0} == timestamp(0)", "false"), - DURATION_STRUCT_EXCEEDS_MAX_GO_DURATION_FAILS( - "google.protobuf.Duration{seconds: 9223372036, nanos: 854775808} ==" - + " google.protobuf.Duration{}", - "false"), - DURATION_STRUCT_EXCEEDS_MIN_GO_DURATION_FAILS( - "google.protobuf.Duration{seconds: -9223372036, nanos: -854775809} ==" - + " google.protobuf.Duration{}", + DURATION_STRUCT_SECONDS_UNDERFLOW_FAILS( + "google.protobuf.Duration{seconds: -315576000001} == google.protobuf.Duration{}", "false"), + DURATION_ADD_EXCEEDS_MAX_FAILS( + "(google.protobuf.Duration{seconds: 315576000000, nanos: 999999999} +" + + " google.protobuf.Duration{nanos: 1}) == google.protobuf.Duration{}", "false"), - TIMESTAMP_SUBTRACT_EXCEEDS_GO_DURATION_FAILS( - "(timestamp(253402300799) - timestamp(-62135596800)) == google.protobuf.Duration{}", + DURATION_SUBTRACT_EXCEEDS_MIN_FAILS( + "(google.protobuf.Duration{seconds: -315576000000, nanos: -999999999} -" + + " google.protobuf.Duration{nanos: 1}) == google.protobuf.Duration{}", "false"); final String exprA;