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