From 37905b182ead04bbb15756f3d572f034c3f71d7f Mon Sep 17 00:00:00 2001 From: David Papp Date: Tue, 9 Jun 2026 12:08:23 +0200 Subject: [PATCH] feat(functions): wire up six unregistered functions Five complete function implementations existed on disk but were never declared in functions/mod.rs, so they were uncompiled, unexported, and unregistered: upper, to_string, remove_query_args, json_lookup_string, json_lookup_integer. ends_with was exported from the engine but had no FFI dispatch arm. - Declare + export the five modules in functions/mod.rs and engine lib.rs - Register all six in the FFI add_function dispatch (JSON lookups use the Cloudflare names lookup_json_string / lookup_json_integer) - Promote serde_json from dev-dependency to a regular dependency, since the JSON lookup implementations use it at runtime - Drop now-unused test-only imports surfaced by compiling these files --- engine/Cargo.toml | 2 +- engine/src/functions/json_lookup_integer.rs | 8 ++- engine/src/functions/json_lookup_string.rs | 26 ++++++---- engine/src/functions/mod.rs | 10 ++++ engine/src/functions/remove_query_args.rs | 6 +-- engine/src/functions/to_string.rs | 16 +++--- engine/src/functions/upper.rs | 34 +++++++++---- engine/src/lib.rs | 9 ++-- ffi/src/lib.rs | 54 +++++++++++++++++++-- 9 files changed, 123 insertions(+), 42 deletions(-) diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 1655ce13..0ba7a226 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -37,6 +37,7 @@ outer-regex.workspace = true rand.workspace = true regex-automata = { workspace = true, optional = true } serde.workspace = true +serde_json.workspace = true simdutf8.workspace = true sliceslice.workspace = true thiserror.workspace = true @@ -46,7 +47,6 @@ wildcard.workspace = true [dev-dependencies] criterion.workspace = true indoc.workspace = true -serde_json.workspace = true [target.'cfg(target_family = "wasm")'.dependencies] # By default, getrandom doesn't have any source of randomness on wasm32-unknown. diff --git a/engine/src/functions/json_lookup_integer.rs b/engine/src/functions/json_lookup_integer.rs index c81f3f91..5eff2082 100644 --- a/engine/src/functions/json_lookup_integer.rs +++ b/engine/src/functions/json_lookup_integer.rs @@ -1,9 +1,6 @@ -use std::iter; - -use crate::lhs_types::Bytes; -use crate::{LhsValue, Type}; - use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; +use crate::{LhsValue, Type}; +use std::iter; /// Returns the integer value associated with the supplied key in `field`. /// @@ -134,6 +131,7 @@ impl FunctionDefinition for JsonLookupIntegerFunction { #[cfg(test)] mod tests { use super::*; + use crate::lhs_types::Bytes; #[test] fn test_lookup_json_integer_basic() { diff --git a/engine/src/functions/json_lookup_string.rs b/engine/src/functions/json_lookup_string.rs index 4daeee54..db24b99c 100644 --- a/engine/src/functions/json_lookup_string.rs +++ b/engine/src/functions/json_lookup_string.rs @@ -1,9 +1,7 @@ -use std::iter; - +use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; use crate::lhs_types::Bytes; use crate::{LhsValue, Type}; - -use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; +use std::iter; /// Returns the string value associated with the supplied key in `field`. /// @@ -144,7 +142,9 @@ mod tests { .into_iter(); assert_eq!( json_lookup_string_impl(&mut args), - Some(LhsValue::Bytes(Bytes::Owned(b"cloudflare".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"cloudflare".to_vec().into_boxed_slice() + ))) ); } @@ -159,7 +159,9 @@ mod tests { .into_iter(); assert_eq!( json_lookup_string_impl(&mut args), - Some(LhsValue::Bytes(Bytes::Owned(b"cloudflare".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"cloudflare".to_vec().into_boxed_slice() + ))) ); } @@ -173,7 +175,9 @@ mod tests { .into_iter(); assert_eq!( json_lookup_string_impl(&mut args), - Some(LhsValue::Bytes(Bytes::Owned(b"cloudflare".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"cloudflare".to_vec().into_boxed_slice() + ))) ); } @@ -188,7 +192,9 @@ mod tests { .into_iter(); assert_eq!( json_lookup_string_impl(&mut args), - Some(LhsValue::Bytes(Bytes::Owned(b"cloudflare".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"cloudflare".to_vec().into_boxed_slice() + ))) ); } @@ -203,7 +209,9 @@ mod tests { .into_iter(); assert_eq!( json_lookup_string_impl(&mut args), - Some(LhsValue::Bytes(Bytes::Owned(b"cloudflare".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"cloudflare".to_vec().into_boxed_slice() + ))) ); } } diff --git a/engine/src/functions/mod.rs b/engine/src/functions/mod.rs index a1ef9736..6580f79d 100644 --- a/engine/src/functions/mod.rs +++ b/engine/src/functions/mod.rs @@ -4,12 +4,17 @@ pub(crate) mod cidr; pub(crate) mod concat; pub(crate) mod decode_base64; pub(crate) mod ends_with; +pub(crate) mod json_lookup_integer; +pub(crate) mod json_lookup_string; pub(crate) mod len; pub(crate) mod lower; pub(crate) mod regex_replace; pub(crate) mod remove_bytes; +pub(crate) mod remove_query_args; pub(crate) mod starts_with; pub(crate) mod substring; +pub(crate) mod to_string; +pub(crate) mod upper; pub(crate) mod url_decode; pub(crate) mod uuid4; pub(crate) mod wildcard_replace; @@ -25,16 +30,21 @@ pub use cidr::CIDRFunction; pub use concat::ConcatFunction; pub use decode_base64::DecodeBase64Function; pub use ends_with::EndsWithFunction; +pub use json_lookup_integer::JsonLookupIntegerFunction; +pub use json_lookup_string::JsonLookupStringFunction; pub use len::LenFunction; pub use lower::LowerFunction; pub use regex_replace::RegexReplaceFunction; pub use remove_bytes::RemoveBytesFunction; +pub use remove_query_args::RemoveQueryArgsFunction; pub use starts_with::StartsWithFunction; use std::any::Any; use std::fmt::{self, Debug}; use std::iter::once; pub use substring::SubstringFunction; use thiserror::Error; +pub use to_string::ToStringFunction; +pub use upper::UpperFunction; pub use url_decode::UrlDecodeFunction; pub use uuid4::UUID4Function; pub use wildcard_replace::WildcardReplaceFunction; diff --git a/engine/src/functions/remove_query_args.rs b/engine/src/functions/remove_query_args.rs index 856232df..0703a79d 100644 --- a/engine/src/functions/remove_query_args.rs +++ b/engine/src/functions/remove_query_args.rs @@ -1,9 +1,7 @@ -use std::collections::HashSet; - +use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; use crate::lhs_types::Bytes; use crate::{LhsValue, Type}; - -use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; +use std::collections::HashSet; /// Removes one or more query string parameters from a URI query string. /// diff --git a/engine/src/functions/to_string.rs b/engine/src/functions/to_string.rs index a6855041..5edceb7e 100644 --- a/engine/src/functions/to_string.rs +++ b/engine/src/functions/to_string.rs @@ -1,8 +1,7 @@ +use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; use crate::lhs_types::Bytes; use crate::{LhsValue, Type}; -use super::{FunctionArgKind, FunctionArgs, FunctionDefinition}; - /// Convert an Integer, Boolean, or IP LHS value into its textual representation. /// /// Usage: @@ -39,9 +38,15 @@ fn to_string_impl<'a>(args: FunctionArgs<'_, 'a>) -> Option> { } match arg { - Ok(LhsValue::Int(i)) => Some(LhsValue::Bytes(Bytes::Owned(i.to_string().into_boxed_str().into_boxed_bytes()))), - Ok(LhsValue::Bool(b)) => Some(LhsValue::Bytes(Bytes::Owned(b.to_string().into_boxed_str().into_boxed_bytes()))), - Ok(LhsValue::Ip(ip)) => Some(LhsValue::Bytes(Bytes::Owned(ip.to_string().into_boxed_str().into_boxed_bytes()))), + Ok(LhsValue::Int(i)) => Some(LhsValue::Bytes(Bytes::Owned( + i.to_string().into_boxed_str().into_boxed_bytes(), + ))), + Ok(LhsValue::Bool(b)) => Some(LhsValue::Bytes(Bytes::Owned( + b.to_string().into_boxed_str().into_boxed_bytes(), + ))), + Ok(LhsValue::Ip(ip)) => Some(LhsValue::Bytes(Bytes::Owned( + ip.to_string().into_boxed_str().into_boxed_bytes(), + ))), Err(Type::Int) | Err(Type::Bool) | Err(Type::Ip) => None, _ => unreachable!(), } @@ -93,7 +98,6 @@ impl FunctionDefinition for ToStringFunction { #[cfg(test)] mod tests { use super::*; - use std::borrow::Cow; fn owned(s: &str) -> LhsValue<'_> { LhsValue::Bytes(Bytes::Owned(s.as_bytes().to_vec().into_boxed_slice())) diff --git a/engine/src/functions/upper.rs b/engine/src/functions/upper.rs index 02242c02..2681cad0 100644 --- a/engine/src/functions/upper.rs +++ b/engine/src/functions/upper.rs @@ -1,7 +1,6 @@ -use std::iter; - use crate::lhs_types::Bytes; use crate::{FunctionArgKind, FunctionArgs, FunctionDefinition, LhsValue, Type}; +use std::iter; /// Converts a string field to uppercase. Only lowercase ASCII bytes are converted. All other bytes are unaffected. /// For example, if http.host is "www.cloudflare.com", then upper(http.host) will return "WWW.CLOUDFLARE.COM". @@ -20,7 +19,9 @@ fn upper_impl<'a>(args: FunctionArgs<'_, 'a>) -> Option> { Ok(LhsValue::Bytes(bytes)) => { let bytes_upper: Vec = bytes.into_owned().to_vec(); let bytes_upper = bytes_upper.to_ascii_uppercase(); - Some(LhsValue::Bytes(Bytes::Owned(bytes_upper.into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + bytes_upper.into_boxed_slice(), + ))) } Err(Type::Bytes) => None, _ => unreachable!(), @@ -78,21 +79,28 @@ mod tests { let mut args_lower = vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"hello world")))].into_iter(); assert_eq!( upper_impl(&mut args_lower), - Some(LhsValue::Bytes(Bytes::Owned(b"HELLO WORLD".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"HELLO WORLD".to_vec().into_boxed_slice() + ))) ); // Test with a mixed-case string let mut args_mixed = vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"MiXeD CaSe")))].into_iter(); assert_eq!( upper_impl(&mut args_mixed), - Some(LhsValue::Bytes(Bytes::Owned(b"MIXED CASE".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"MIXED CASE".to_vec().into_boxed_slice() + ))) ); // Test with an already uppercase string - let mut args_upper = vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"ALREADY UPPER")))].into_iter(); + let mut args_upper = + vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"ALREADY UPPER")))].into_iter(); assert_eq!( upper_impl(&mut args_upper), - Some(LhsValue::Bytes(Bytes::Owned(b"ALREADY UPPER".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"ALREADY UPPER".to_vec().into_boxed_slice() + ))) ); // Test with the example from the specification: "www.cloudflare.com" @@ -100,14 +108,18 @@ mod tests { vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"www.cloudflare.com")))].into_iter(); assert_eq!( upper_impl(&mut args_example), - Some(LhsValue::Bytes(Bytes::Owned(b"WWW.CLOUDFLARE.COM".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"WWW.CLOUDFLARE.COM".to_vec().into_boxed_slice() + ))) ); // Test with an empty string let mut args_empty = vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"")))].into_iter(); assert_eq!( upper_impl(&mut args_empty), - Some(LhsValue::Bytes(Bytes::Owned(b"".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"".to_vec().into_boxed_slice() + ))) ); // Test with missing field @@ -119,7 +131,9 @@ mod tests { vec![Ok(LhsValue::Bytes(Bytes::Borrowed(b"hello\xc3\xa9world")))].into_iter(); assert_eq!( upper_impl(&mut args_non_ascii), - Some(LhsValue::Bytes(Bytes::Owned(b"HELLO\xc3\xa9WORLD".to_vec().into_boxed_slice()))) + Some(LhsValue::Bytes(Bytes::Owned( + b"HELLO\xc3\xa9WORLD".to_vec().into_boxed_slice() + ))) ); } diff --git a/engine/src/lib.rs b/engine/src/lib.rs index 5595bb3f..5f8bfbe9 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -98,11 +98,12 @@ pub use self::filter::{ pub use self::functions::{ AllFunction, AnyFunction, CIDRFunction, ConcatFunction, DecodeBase64Function, EndsWithFunction, FunctionArgInvalidConstantError, FunctionArgKind, FunctionArgKindMismatchError, FunctionArgs, - FunctionDefinition, FunctionDefinitionContext, FunctionParam, FunctionParamError, LenFunction, - LowerFunction, RegexReplaceFunction, RemoveBytesFunction, SimpleFunctionArgKind, + FunctionDefinition, FunctionDefinitionContext, FunctionParam, FunctionParamError, + JsonLookupIntegerFunction, JsonLookupStringFunction, LenFunction, LowerFunction, + RegexReplaceFunction, RemoveBytesFunction, RemoveQueryArgsFunction, SimpleFunctionArgKind, SimpleFunctionDefinition, SimpleFunctionImpl, SimpleFunctionOptParam, SimpleFunctionParam, - StartsWithFunction, SubstringFunction, UUID4Function, UrlDecodeFunction, - WildcardReplaceFunction, + StartsWithFunction, SubstringFunction, ToStringFunction, UUID4Function, UpperFunction, + UrlDecodeFunction, WildcardReplaceFunction, }; pub use self::lex::LexErrorKind; pub use self::lhs_types::{Array, Bytes, Map, MapIter, TypedArray, TypedMap}; diff --git a/ffi/src/lib.rs b/ffi/src/lib.rs index 3a66dc53..6562b6a7 100644 --- a/ffi/src/lib.rs +++ b/ffi/src/lib.rs @@ -17,9 +17,10 @@ use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use wirefilter::{ AllFunction, AlwaysList, AnyFunction, CIDRFunction, ConcatFunction, DecodeBase64Function, - GetType, LenFunction, LowerFunction, NeverList, RegexReplaceFunction, RemoveBytesFunction, - StartsWithFunction, SubstringFunction, Type, UUID4Function, UrlDecodeFunction, - WildcardReplaceFunction, catch_panic, + EndsWithFunction, GetType, JsonLookupIntegerFunction, JsonLookupStringFunction, LenFunction, + LowerFunction, NeverList, RegexReplaceFunction, RemoveBytesFunction, RemoveQueryArgsFunction, + StartsWithFunction, SubstringFunction, ToStringFunction, Type, UUID4Function, UpperFunction, + UrlDecodeFunction, WildcardReplaceFunction, catch_panic, }; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -406,6 +407,53 @@ pub extern "C" fn wirefilter_add_function_to_scheme( false } }, + "ends_with" => match builder.add_function(name, EndsWithFunction::default()) { + Ok(_) => true, + Err(err) => { + write_last_error!("{}", err); + false + } + }, + "upper" => match builder.add_function(name, UpperFunction::default()) { + Ok(_) => true, + Err(err) => { + write_last_error!("{}", err); + false + } + }, + "to_string" => match builder.add_function(name, ToStringFunction::default()) { + Ok(_) => true, + Err(err) => { + write_last_error!("{}", err); + false + } + }, + "remove_query_args" => match builder.add_function(name, RemoveQueryArgsFunction::default()) + { + Ok(_) => true, + Err(err) => { + write_last_error!("{}", err); + false + } + }, + "lookup_json_string" => { + match builder.add_function(name, JsonLookupStringFunction::default()) { + Ok(_) => true, + Err(err) => { + write_last_error!("{}", err); + false + } + } + } + "lookup_json_integer" => { + match builder.add_function(name, JsonLookupIntegerFunction::default()) { + Ok(_) => true, + Err(err) => { + write_last_error!("{}", err); + false + } + } + } _ => { write_last_error!("Unknown function name provided: {}", name); false