diff --git a/Cargo.lock b/Cargo.lock index ab170a96..6ed6c974 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -305,6 +305,15 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "ascii-canvas" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1e3e699d84ab1b0911a1010c5c106aa34ae89aeac103be5ce0c3859db1e891" +dependencies = [ + "term", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -498,6 +507,21 @@ dependencies = [ "syn 2.0.89", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -595,6 +619,33 @@ dependencies = [ "shlex", ] +[[package]] +name = "cel-interpreter" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5675bdc8ece076b0a4f5ac6c20724e7373919ed698e00dbf33825682a7b3a679" +dependencies = [ + "cel-parser", + "chrono", + "nom", + "paste", + "regex", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "cel-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1df6c220727ff1f7b52a41699d8c71ec52e8ae58d01a9768469a916e1f22eb" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", + "thiserror 1.0.69", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -950,6 +1001,15 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1286,6 +1346,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -1727,6 +1796,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -1747,6 +1825,38 @@ dependencies = [ "libc", ] +[[package]] +name = "lalrpop" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06093b57658c723a21da679530e061a8c25340fa5a6f98e313b542268c7e2a1f" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools 0.13.0", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax 0.8.5", + "sha3", + "string_cache", + "term", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feee752d43abd0f4807a921958ab4131f692a44d4d599733d4419c5d586176ce" +dependencies = [ + "regex-automata 0.4.9", + "rustversion", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1825,6 +1935,8 @@ version = "0.8.0-dev" dependencies = [ "async-trait", "base64 0.22.1", + "cel-interpreter", + "cel-parser", "cfg-if", "criterion", "dashmap", @@ -2109,6 +2221,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -2507,6 +2625,21 @@ dependencies = [ "indexmap 2.6.0", ] +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + [[package]] name = "pin-project" version = "1.1.7" @@ -2607,6 +2740,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.25" @@ -3146,6 +3285,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -3170,6 +3319,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "sketches-ddsketch" version = "0.2.2" @@ -3216,6 +3371,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3319,6 +3487,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "term" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4175de05129f31b80458c6df371a15e7fc3fd367272e6bf938e5c351c7ea0" +dependencies = [ + "home", + "windows-sys 0.52.0", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/limitador/Cargo.toml b/limitador/Cargo.toml index d9240b5f..a2dc0033 100644 --- a/limitador/Cargo.toml +++ b/limitador/Cargo.toml @@ -54,6 +54,8 @@ tonic = { version = "0.12.3", optional = true } tonic-reflection = { version = "0.12.3", optional = true } prost = { version = "0.13.3", optional = true } prost-types = { version = "0.13.3", optional = true } +cel-parser = "0.8.0" +cel-interpreter = { version = "0.9.0", features = ["chrono", "regex"] } [dev-dependencies] serial_test = "3.0" diff --git a/limitador/src/limit.rs b/limitador/src/limit.rs index 94b95b1b..0df643d5 100644 --- a/limitador/src/limit.rs +++ b/limitador/src/limit.rs @@ -855,6 +855,12 @@ mod conditions { } } +pub use cel::Expression as CelExpression; +pub use cel::ParseError; +pub use cel::Predicate as CelPredicate; + +pub(super) mod cel; + #[cfg(test)] mod tests { use super::*; diff --git a/limitador/src/limit/cel.rs b/limitador/src/limit/cel.rs new file mode 100644 index 00000000..7b08cdb9 --- /dev/null +++ b/limitador/src/limit/cel.rs @@ -0,0 +1,162 @@ +use crate::limit::cel::errors::EvaluationError; +use cel_interpreter::{ExecutionError, Value}; +pub use errors::ParseError; + +pub(super) mod errors { + use cel_interpreter::ExecutionError; + use std::error::Error; + use std::fmt::{Display, Formatter}; + + #[derive(Debug, PartialEq)] + pub enum EvaluationError { + UnexpectedValueType(String), + ExecutionError(ExecutionError), + } + + impl Display for EvaluationError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EvaluationError::UnexpectedValueType(value) => { + write!(f, "unexpected value of type {}", value) + } + EvaluationError::ExecutionError(error) => error.fmt(f), + } + } + } + + impl Error for EvaluationError {} + + #[derive(Debug)] + pub struct ParseError { + input: String, + source: Box, + } + + impl ParseError { + pub fn from(source: cel_parser::ParseError, input: String) -> Self { + Self { + input, + source: Box::new(source), + } + } + } + + impl Display for ParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "couldn't parse {}: {}", self.input, self.source) + } + } + + impl Error for ParseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self.source.as_ref()) + } + } + + impl From for EvaluationError { + fn from(err: ExecutionError) -> Self { + EvaluationError::ExecutionError(err) + } + } +} + +pub struct Context {} + +pub struct Expression { + expression: cel_parser::Expression, +} + +impl Expression { + pub fn parse(source: &str) -> Result { + match cel_parser::parse(source) { + Ok(expression) => Ok(Self { expression }), + Err(err) => Err(ParseError::from(err, source.to_string())), + } + } + + pub fn eval(&self, ctx: &Context) -> Result { + match self.resolve(ctx)? { + Value::Int(i) => Ok(i.to_string()), + Value::UInt(i) => Ok(i.to_string()), + Value::Float(f) => Ok(f.to_string()), + Value::String(s) => Ok(s.to_string()), + Value::Null => Ok("null".to_owned()), + Value::Bool(b) => Ok(b.to_string()), + val => Err(err_on_value(val)), + } + } + + pub fn resolve(&self, _ctx: &Context) -> Result { + let ctx = cel_interpreter::Context::default(); + Value::resolve(&self.expression, &ctx) + } +} + +fn err_on_value(val: Value) -> EvaluationError { + match val { + Value::List(list) => EvaluationError::UnexpectedValueType(format!("list: `{:?}`", *list)), + Value::Map(map) => EvaluationError::UnexpectedValueType(format!("map: `{:?}`", *map.map)), + Value::Function(ident, _) => { + EvaluationError::UnexpectedValueType(format!("function: `{}`", *ident)) + } + Value::Bytes(b) => EvaluationError::UnexpectedValueType(format!("function: `{:?}`", *b)), + Value::Duration(d) => EvaluationError::UnexpectedValueType(format!("duration: `{d}`")), + Value::Timestamp(ts) => EvaluationError::UnexpectedValueType(format!("timestamp: `{ts}`")), + Value::Int(i) => EvaluationError::UnexpectedValueType(format!("integer: `{i}`")), + Value::UInt(u) => EvaluationError::UnexpectedValueType(format!("unsigned integer: `{u}`")), + Value::Float(f) => EvaluationError::UnexpectedValueType(format!("float: `{f}`")), + Value::String(s) => EvaluationError::UnexpectedValueType(format!("string: `{s}`")), + Value::Bool(b) => EvaluationError::UnexpectedValueType(format!("bool: `{b}`")), + Value::Null => EvaluationError::UnexpectedValueType("null".to_owned()), + } +} + +pub struct Predicate(Expression); + +impl Predicate { + pub fn parse(source: &str) -> Result { + Expression::parse(source).map(Self) + } + + pub fn test(&self, ctx: &Context) -> Result { + match self.0.resolve(ctx)? { + Value::Bool(b) => Ok(b), + v => Err(err_on_value(v)), + } + } +} + +#[cfg(test)] +mod tests { + use super::{Context, Expression, Predicate}; + + #[test] + fn expression() { + let exp = Expression::parse("100").expect("failed to parse"); + assert_eq!(exp.eval(&Context {}), Ok(String::from("100"))); + } + + #[test] + fn unexpected_value_type_expression() { + let exp = Expression::parse("['100']").expect("failed to parse"); + assert_eq!( + exp.eval(&Context {}).map_err(|e| format!("{e}")), + Err("unexpected value of type list: `[String(\"100\")]`".to_string()) + ); + } + + #[test] + fn predicate() { + let pred = Predicate::parse("42 == uint('42')").expect("failed to parse"); + assert_eq!(pred.test(&Context {}), Ok(true)); + } + + #[test] + fn unexpected_value_predicate() { + let pred = Predicate::parse("42").expect("failed to parse"); + assert_eq!( + pred.test(&Context {}).map_err(|e| format!("{e}")), + Err("unexpected value of type integer: `42`".to_string()) + ); + } +}