Skip to main content

protovalidate_buffa/
cel.rs

1//! Helpers used by compile-time-expanded CEL rules emitted by the plugin.
2//!
3//! CEL rules are transpiled to native Rust at codegen time, so this module
4//! is a narrow support layer rather than an interpreter: scalar widening
5//! (`CelScalar`), Duration/Timestamp conversion
6//! (`duration_from_secs_nanos`, `timestamp_from_secs_nanos`), and the
7//! per-evaluation `now` binding (`now_local`).
8
9/// Current wall-clock time as a `chrono::DateTime<chrono::FixedOffset>`.
10///
11/// Used to seed the `now` binding inside compile-time-expanded CEL bodies.
12/// Lives in this crate so generated code doesn't need to depend on `chrono`
13/// directly.
14#[must_use]
15pub fn now_local() -> chrono::DateTime<chrono::FixedOffset> {
16    chrono::Utc::now().fixed_offset()
17}
18
19/// Width-converts a proto scalar (or enum wrapper) into CEL's wide types
20/// (`i64` / `u64` / `f64`).
21///
22/// Used by codegen-emitted native CEL bodies so a single emitted
23/// comparison/arithmetic expression works regardless of the underlying Rust
24/// representation (`i32`, `u32`, `i64`, `u64`, `f32`, `f64`, or
25/// `buffa::EnumValue<E>`).
26pub trait CelScalar: Copy {
27    /// Coerce to CEL's `int` wide type. Numeric `as`-casts; for
28    /// `EnumValue<E>` returns `i64::from(self.to_i32())`.
29    fn cel_int(self) -> i64;
30    /// Coerce to CEL's `uint` wide type. Numeric `as`-casts; for floats
31    /// the cast truncates toward zero.
32    fn cel_uint(self) -> u64;
33    /// Coerce to CEL's `double` wide type.
34    fn cel_double(self) -> f64;
35}
36
37macro_rules! impl_cel_scalar_int {
38    ($($t:ty),*) => {
39        $(
40            impl CelScalar for $t {
41                #[inline]
42                #[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss, clippy::cast_lossless, clippy::cast_precision_loss)]
43                fn cel_int(self) -> i64 { self as i64 }
44                #[inline]
45                #[allow(clippy::cast_sign_loss, clippy::cast_lossless)]
46                fn cel_uint(self) -> u64 { self as u64 }
47                #[inline]
48                #[allow(clippy::cast_lossless, clippy::cast_precision_loss)]
49                fn cel_double(self) -> f64 { self as f64 }
50            }
51        )*
52    };
53}
54impl_cel_scalar_int!(i32, i64, u32, u64);
55
56impl CelScalar for f32 {
57    #[inline]
58    #[allow(clippy::cast_possible_truncation)]
59    fn cel_int(self) -> i64 {
60        self as i64
61    }
62    #[inline]
63    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
64    fn cel_uint(self) -> u64 {
65        self as u64
66    }
67    #[inline]
68    fn cel_double(self) -> f64 {
69        f64::from(self)
70    }
71}
72
73impl CelScalar for f64 {
74    #[inline]
75    #[allow(clippy::cast_possible_truncation)]
76    fn cel_int(self) -> i64 {
77        self as i64
78    }
79    #[inline]
80    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
81    fn cel_uint(self) -> u64 {
82        self as u64
83    }
84    #[inline]
85    fn cel_double(self) -> f64 {
86        self
87    }
88}
89
90impl<E: buffa::Enumeration + Copy> CelScalar for buffa::EnumValue<E> {
91    #[inline]
92    fn cel_int(self) -> i64 {
93        i64::from(self.to_i32())
94    }
95    #[inline]
96    #[allow(clippy::cast_sign_loss)]
97    fn cel_uint(self) -> u64 {
98        self.to_i32() as u64
99    }
100    #[inline]
101    fn cel_double(self) -> f64 {
102        f64::from(self.to_i32())
103    }
104}
105
106/// Construct a `chrono::Duration` from a protobuf-shaped
107/// `(seconds, nanos)` pair (the wire format of
108/// `google.protobuf.Duration`).
109///
110/// Used by codegen-emitted CEL bodies when binding `this` to a
111/// `MessageField<Duration>` value or when constructing a literal via
112/// `duration("…")`.
113#[must_use]
114pub fn duration_from_secs_nanos(seconds: i64, nanos: i32) -> chrono::Duration {
115    chrono::Duration::seconds(seconds) + chrono::Duration::nanoseconds(i64::from(nanos))
116}
117
118/// Construct a `chrono::DateTime<chrono::FixedOffset>` from a
119/// protobuf-shaped `(seconds, nanos)` pair (the wire format of
120/// `google.protobuf.Timestamp`).
121///
122/// # Panics
123///
124/// Panics only if the fallback `from_timestamp(0, 0)` fails, which is
125/// impossible (Unix epoch is always representable).
126#[must_use]
127pub fn timestamp_from_secs_nanos(
128    seconds: i64,
129    nanos: i32,
130) -> chrono::DateTime<chrono::FixedOffset> {
131    let nanos_u32 = u32::try_from(nanos.max(0)).unwrap_or(0);
132    let s = chrono::DateTime::<chrono::Utc>::from_timestamp(seconds, nanos_u32)
133        .unwrap_or_else(|| chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).unwrap());
134    s.fixed_offset()
135}
136
137/// Parse a CEL `duration("…")` string into a `chrono::Duration`.
138///
139/// Accepts the protobuf duration grammar: an optional sign, a decimal
140/// number, and one of the suffixes `ns` / `us` / `µs` / `ms` / `s` / `m`
141/// / `h`. Returns `None` on any parse error so the caller can decide
142/// whether to map that to a CEL runtime error.
143///
144/// Used by codegen for `duration(this.field)` where `this.field` isn't
145/// a compile-time-known string literal; literal-arg paths fold the parse
146/// into `duration_from_secs_nanos(secs, nanos)` at codegen time and do
147/// not call this.
148#[must_use]
149#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
150pub fn parse_duration(s: &str) -> Option<chrono::Duration> {
151    let s = s.trim();
152    if s.is_empty() {
153        return None;
154    }
155    // Split into sign + magnitude + unit. We accept a leading `+` or `-`.
156    let (sign, rest) = match s.as_bytes()[0] {
157        b'-' => (-1i64, &s[1..]),
158        b'+' => (1i64, &s[1..]),
159        _ => (1i64, s),
160    };
161    // Find the unit suffix.
162    let (num_str, unit) = if let Some(stripped) = rest.strip_suffix("ns") {
163        (stripped, "ns")
164    } else if let Some(stripped) = rest.strip_suffix("us") {
165        (stripped, "us")
166    } else if let Some(stripped) = rest.strip_suffix("µs") {
167        (stripped, "us")
168    } else if let Some(stripped) = rest.strip_suffix("ms") {
169        (stripped, "ms")
170    } else if let Some(stripped) = rest.strip_suffix('s') {
171        (stripped, "s")
172    } else if let Some(stripped) = rest.strip_suffix('m') {
173        (stripped, "m")
174    } else if let Some(stripped) = rest.strip_suffix('h') {
175        (stripped, "h")
176    } else {
177        return None;
178    };
179    let value: f64 = num_str.parse().ok()?;
180    let nanos_total: f64 = match unit {
181        "ns" => value,
182        "us" => value * 1_000.0,
183        "ms" => value * 1_000_000.0,
184        "s" => value * 1_000_000_000.0,
185        "m" => value * 60.0 * 1_000_000_000.0,
186        "h" => value * 3600.0 * 1_000_000_000.0,
187        _ => return None,
188    };
189    if !nanos_total.is_finite() {
190        return None;
191    }
192    let signed = (nanos_total as i64).checked_mul(sign)?;
193    Some(chrono::Duration::nanoseconds(signed))
194}
195
196/// Parse a CEL `timestamp("…")` string (RFC3339).
197///
198/// Returns `None` on any parse error. Used by codegen for
199/// `timestamp(this.field)` where the argument isn't a compile-time
200/// literal — the literal-arg path bakes the result into a cached
201/// `OnceLock<DateTime>` at codegen time and doesn't call this.
202#[must_use]
203pub fn parse_timestamp(s: &str) -> Option<chrono::DateTime<chrono::FixedOffset>> {
204    chrono::DateTime::parse_from_rfc3339(s).ok()
205}