diff --git a/cel-c/internal/BUILD b/cel-c/internal/BUILD index ae5266c..8e2aaff 100644 --- a/cel-c/internal/BUILD +++ b/cel-c/internal/BUILD @@ -659,6 +659,12 @@ cc_test( deps = [ ":config", ":value", + "@googletest//:gtest_main", + "@abseil-cpp//absl/cleanup", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/log:die_if_null", + "@abseil-cpp//absl/strings:string_view", "//cel-c:alloc", "//cel-c:arena", "//cel-c:ast", @@ -677,15 +683,12 @@ cc_test( "//cel-c/internal/testing:compiler", "//cel-c/internal/testing:def_pool", "//cel-c/internal/testing:parser", - "@abseil-cpp//absl/cleanup", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:die_if_null", - "@abseil-cpp//absl/strings:string_view", "@cel-cpp//internal:testing_descriptor_pool", "@cel-cpp//internal:testing_message_factory", - "@googletest//:gtest_main", "@protobuf", + # Embeds the full IANA time zone database so that `absl::LoadTimeZone` + # works on platforms without a system zoneinfo database (e.g. Windows). + "//third_party/tz:zoneinfo_embedded", "@protobuf//upb/message", "@protobuf//upb/reflection", "@protobuf//upb/wire", diff --git a/cel-c/internal/runtime/interpretable.cc b/cel-c/internal/runtime/interpretable.cc index af95cdc..93519c5 100644 --- a/cel-c/internal/runtime/interpretable.cc +++ b/cel-c/internal/runtime/interpretable.cc @@ -326,6 +326,19 @@ _cel_Interpretable_NewTimestampOverflowError( return error; } +CEL_ATTRIBUTE_NODISCARD +static CEL_INLINE cel_Error* cel_nonnull +_cel_Interpretable_NewInvalidTimeZoneError( + _cel_Interpretable* cel_nonnull interp) { + cel_Error* error = cel_Error_New(interp->arena); + if (CEL_UNLIKELY(error == cel_nullptr)) { + _cel_Interpretable_ThrowOutOfMemory(interp); + } + cel_Error_SetMessage(error, cel_StringView_From("cel: invalid time zone")); + cel_Error_SetCanonicalCode(error, cel_ErrorCode_kInvalidArgument); + return error; +} + CEL_ATTRIBUTE_NODISCARD static CEL_INLINE cel_Error* cel_nonnull _cel_Interpretable_NewDuplicateKeyError( @@ -2817,39 +2830,74 @@ static inline int64_t _cel_Interpretable_CivilTimeComponent( // // (google.protobuf.Duration) -> int // (google.protobuf.Timestamp) -> int +// (google.protobuf.Timestamp, string) -> int // -// Timestamps are broken down in UTC. +// For timestamps, the optional string is the time zone, otherwise UTC is used. static inline void _cel_Interpretable_CallTimeAccessor( _cel_Interpretable* cel_nonnull interp, const _cel_InstrData* cel_nonnull instr, _cel_TimeAccessor accessor) { CEL_ASSERT_NOT_NULL(interp); CEL_ASSERT_NOT_NULL(instr); - CEL_ASSERT(instr->call.args == 1); + CEL_ASSERT(instr->call.args == 1 || instr->call.args == 2); + + cel_Value* const top = + _cel_Interpretable_TopN(interp, instr, instr->call.args); + cel_Value* const tz = instr->call.args == 2 ? top + 1 : cel_nullptr; - cel_Value* const top = _cel_Interpretable_Top(interp, instr); switch (cel_Value_Kind(top)) { case cel_ValueKind_kError: break; case cel_ValueKind_kDuration: + if (tz != cel_nullptr) { + // There is no time zone overload for durations. + if (cel_Value_IsError(tz)) { + *top = *tz; + } else { + cel_Value_SetError(top, + _cel_Interpretable_NewNoSuchOverloadError(interp)); + } + break; + } cel_Value_SetInt(top, _cel_Interpretable_DurationComponent( cel_Value_GetDuration(top), accessor)); break; case cel_ValueKind_kTimestamp: { + cel_StringView tz_name = cel_StringView_From(""); + if (tz != cel_nullptr) { + if (cel_Value_IsError(tz)) { + *top = *tz; + break; + } + if (cel_Value_Kind(tz) != cel_ValueKind_kString) { + cel_Value_SetError(top, + _cel_Interpretable_NewNoSuchOverloadError(interp)); + break; + } + tz_name = cel_Value_GetString(tz); + } _cel_CivilTime civil_time; - if (CEL_UNLIKELY(!_cel_Timestamp_ToCivilTime(cel_Value_GetTimestamp(top), - cel_StringView_From(""), - &civil_time))) { - // An empty time zone means UTC, which is always valid. - CEL_UNREACHABLE(); + if (!_cel_Timestamp_ToCivilTime(cel_Value_GetTimestamp(top), tz_name, + &civil_time)) { + cel_Value_SetError(top, + _cel_Interpretable_NewInvalidTimeZoneError(interp)); + break; } cel_Value_SetInt( top, _cel_Interpretable_CivilTimeComponent(&civil_time, accessor)); } break; default: - cel_Value_SetError(top, - _cel_Interpretable_NewNoSuchOverloadError(interp)); + if (tz != cel_nullptr && cel_Value_IsError(tz)) { + *top = *tz; + } else { + cel_Value_SetError(top, + _cel_Interpretable_NewNoSuchOverloadError(interp)); + } break; } + + if (tz != cel_nullptr) { + _cel_Interpretable_Pop(interp, instr); + } } static inline void _cel_Interpretable_Index( diff --git a/cel-c/internal/runtime/interpreter.cc b/cel-c/internal/runtime/interpreter.cc index 49d23f8..474fa45 100644 --- a/cel-c/internal/runtime/interpreter.cc +++ b/cel-c/internal/runtime/interpreter.cc @@ -1565,7 +1565,8 @@ static void _cel_Interpreter_PreVisitCallArgExpr( // Returns the instruction kind for the duration/timestamp accessor `fn_name`, // or `_cel_InstrKind_kUnreachable` if `fn_name` is not an accessor. Accessors -// accept (google.protobuf.Duration) or (google.protobuf.Timestamp). +// accept (google.protobuf.Duration), (google.protobuf.Timestamp) or +// (google.protobuf.Timestamp, string). CEL_ATTRIBUTE_NODISCARD static _cel_InstrKind _cel_Interpreter_TimeAccessorKind( cel_StringView fn_name) { @@ -1645,7 +1646,7 @@ static void _cel_Interpreter_PostVisitCallExpr( } } - if (!found && instr->data.call.args == 1) { + if (!found && (instr->data.call.args == 1 || instr->data.call.args == 2)) { _cel_InstrKind kind = _cel_Interpreter_TimeAccessorKind(fn_name); if (kind != _cel_InstrKind_kUnreachable) { instr->kind = kind; diff --git a/cel-c/internal/runtime_test.cc b/cel-c/internal/runtime_test.cc index cfeed4c..2326624 100644 --- a/cel-c/internal/runtime_test.cc +++ b/cel-c/internal/runtime_test.cc @@ -1137,6 +1137,168 @@ TEST_F(RuntimeTest, CallGetMillisecondsTimestamp) { EXPECT_EQ(cel_Value_GetInt(&value), 0); } +TEST_F(RuntimeTest, CallGetHoursTimestampWithTimeZone) { + // Empty and UTC time zones match the single argument overloads. + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours(""))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = + Parse(R"cel(getHours(timestamp("2025-12-01T10:20:30.456Z"), "UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + // IANA time zones. + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2); + + prog = Parse( + R"cel(timestamp("2025-07-01T10:20:30.456Z").getHours("America/Los_Angeles"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 3); + + prog = Parse( + R"cel(timestamp("2025-12-01T02:00:00Z").getHours("America/New_York"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 21); + + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours("Asia/Kolkata"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 15); + + // Fixed UTC offsets. + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 15); + + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours("-08:00"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2); + + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours("-01:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 8); + + // Time zones before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T23:59:58Z").getHours("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 5); +} + +TEST_F(RuntimeTest, CallGetMinutesTimestampWithTimeZone) { + // IANA time zones. + _cel_ProgramPtr prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getMinutes("Asia/Kolkata"))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 50); + + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getMinutes("Asia/Kathmandu"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 5); + + // Fixed UTC offsets. + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getMinutes("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 50); + + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getMinutes("-01:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 50); +} + +TEST_F(RuntimeTest, CallGetSecondsTimestampWithTimeZone) { + // IANA time zones. + _cel_ProgramPtr prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getSeconds("Asia/Kolkata"))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); + + // Fixed UTC offsets. + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getSeconds("-01:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); +} + +TEST_F(RuntimeTest, CallGetMillisecondsTimestampWithTimeZone) { + // IANA time zones. + _cel_ProgramPtr prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getMilliseconds("Asia/Kolkata"))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 456); + + // Fixed UTC offsets. + prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30.456Z").getMilliseconds("-01:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 456); + + // Time zones before the Unix epoch. + prog = Parse( + R"cel(timestamp("1969-12-31T23:59:58Z").getMilliseconds("+05:30"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); +} + TEST_F(RuntimeTest, CallTimeAccessorsNoSuchOverload) { _cel_ProgramPtr prog = Parse(R"cel(getHours(1))cel"); _cel_TestingVariableResolver var_resolver; @@ -1180,6 +1342,70 @@ TEST_F(RuntimeTest, CallTimeAccessorsNoSuchOverload) { ASSERT_TRUE(cel_Value_IsError(&value)); EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), cel_StringView_From("cel: no such overload")); + + // Durations do not have a time zone overload. + prog = Parse(R"cel(duration("1h").getHours("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getMinutes(duration("1h"), "UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + // The time zone must be a string. + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getSeconds(1))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(1.getMilliseconds("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); +} + +TEST_F(RuntimeTest, CallTimeAccessorsInvalidTimeZone) { + _cel_ProgramPtr prog = Parse( + R"cel(timestamp("2025-12-01T10:20:30Z").getHours("Invalid/Zone"))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getMinutes("foo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getSeconds("+aa:bb"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); + + prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getMilliseconds(":"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: invalid time zone")); } TEST_F(RuntimeTest, CallTimeAccessorsPropagatesError) { @@ -1211,6 +1437,35 @@ TEST_F(RuntimeTest, CallTimeAccessorsPropagatesError) { ASSERT_TRUE(cel_Value_IsError(&value)); EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), cel_StringView_From("cel: divide by zero")); + + // Errors in either argument of the time zone overload propagate. + prog = Parse(R"cel((1 / 0).getHours("UTC"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30Z").getHours(1 / 0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel(duration("1h").getMinutes(1 / 0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel(true.getSeconds(1 / 0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); } // TODO: Add addition tests for duration/timestamp