use super::{
compile::{Compiled, Group, Node},
fields::{self, Request},
};
enum Bits {
Word(u64),
Words(Vec<u64>),
}
impl Bits {
fn new(len: usize) -> Self {
if len <= 64 {
Bits::Word(0)
} else {
Bits::Words(vec![0; len.div_ceil(64)])
}
}
#[inline]
fn get(&self, index: usize) -> bool {
match self {
Bits::Word(word) => word & (1 << index) != 0,
Bits::Words(words) => words
.get(index / 64)
.is_some_and(|word| word & (1 << (index % 64)) != 0),
}
}
#[inline]
fn set(&mut self, index: usize, value: bool) {
let (word, bit) = match self {
Bits::Word(word) => (word, index),
Bits::Words(words) => match words.get_mut(index / 64) {
Some(word) => (word, index % 64),
None => return,
},
};
if value {
*word |= 1 << bit;
} else {
*word &= !(1 << bit);
}
}
fn clear(&mut self) {
match self {
Bits::Word(word) => *word = 0,
Bits::Words(words) => words.fill(0),
}
}
}
pub(super) struct State {
results: Bits,
read: Bits,
known: Bits,
}
impl State {
pub fn new(compiled: &Compiled) -> Self {
Self {
results: Bits::new(compiled.test_at.len()),
read: Bits::new(compiled.groups.len()),
known: Bits::new(compiled.test_at.len()),
}
}
pub fn forget(&mut self) {
self.read.clear();
self.known.clear();
}
#[inline]
pub fn holds(&mut self, compiled: &Compiled, node: &Node, request: &impl Request) -> bool {
if let Node::Test(test) = node {
return self.test(compiled, *test, request);
}
self.holds_tree(compiled, node, request)
}
fn holds_tree(&mut self, compiled: &Compiled, node: &Node, request: &impl Request) -> bool {
match node {
Node::Test(test) => self.test(compiled, *test, request),
Node::All(nodes) => nodes.iter().all(|node| self.holds(compiled, node, request)),
Node::Any(nodes) => nodes.iter().any(|node| self.holds(compiled, node, request)),
Node::Not(node) => !self.holds(compiled, node, request),
}
}
#[inline]
fn test(&mut self, compiled: &Compiled, test: u32, request: &impl Request) -> bool {
let Some((group_index, group, written)) = compiled.test(test) else {
return false;
};
let index = test as usize;
if group.lazy {
if !self.known.get(index) {
let holds = fields::evaluate_one(&group.field, written, request);
self.results.set(index, holds);
self.known.set(index, true);
}
} else if !self.read.get(group_index) {
self.read_group(group_index, group, request);
}
self.results.get(index)
}
#[cold]
fn read_group(&mut self, index: usize, group: &Group, request: &impl Request) {
let results = &mut self.results;
fields::evaluate(group, request, |test, value| {
results.set(test as usize, value);
});
self.read.set(index, true);
}
}
#[cfg(test)]
pub(super) mod tests {
use std::{borrow::Cow, net::IpAddr};
use http::{HeaderMap, HeaderName, Method};
use serde_json::json;
use crate::rules::{compile::compile, fields::Request, model::Rule};
use super::State;
pub struct TestRequest {
pub method: Method,
pub host: String,
pub path: String,
pub query: Option<String>,
pub headers: HeaderMap,
pub client: Option<IpAddr>,
}
impl TestRequest {
pub fn new(method: &str, host: &str, uri: &str) -> Self {
let (path, query) = match uri.split_once('?') {
Some((path, query)) => (path, Some(query.to_string())),
None => (uri, None),
};
Self {
method: Method::from_bytes(method.as_bytes()).unwrap(),
host: host.to_string(),
path: path.to_string(),
query,
headers: HeaderMap::new(),
client: Some("203.0.113.9".parse().unwrap()),
}
}
pub fn header(mut self, name: &'static str, value: &str) -> Self {
self.headers.append(name, value.parse().unwrap());
self
}
}
impl Request for TestRequest {
fn method(&self) -> &Method {
&self.method
}
fn host(&self) -> &str {
&self.host
}
fn path(&self) -> &str {
&self.path
}
fn query(&self) -> Option<&str> {
self.query.as_deref()
}
fn header(&self, name: &HeaderName) -> Option<Cow<'_, str>> {
let mut values = self.headers.get_all(name).iter();
let first = values.next()?.to_str().ok()?;
match values.next() {
None => Some(Cow::Borrowed(first)),
Some(second) => Some(Cow::Owned(format!("{first}, {}", second.to_str().ok()?))),
}
}
fn client(&self) -> Option<IpAddr> {
self.client
}
}
fn matching(rules: &[serde_json::Value], request: &TestRequest) -> Vec<String> {
let compiled = compile(&rules.iter().cloned().map(Rule).collect::<Vec<_>>()).unwrap();
let mut state = State::new(&compiled);
compiled
.rules
.iter()
.filter(|rule| state.holds(&compiled, &rule.condition, request))
.map(|rule| rule.name.clone())
.collect()
}
fn rule(name: &str, condition: serde_json::Value) -> serde_json::Value {
let mut rule = crate::rules::compile::tests::in_and(condition);
rule["name"] = json!(name);
rule["then"] = json!({ "respond": { "status": 403 } });
rule
}
#[test]
fn test_fields_and_operators() {
let rules = [
rule("get", json!({ "method": { "eq": "get" } })),
rule("not get", json!({ "method": { "neq": "GET" } })),
rule(
"writes",
json!({ "method": { "not_in": ["GET", "HEAD", "OPTIONS"] } }),
),
rule("host", json!({ "host": { "eq": "Old.Example.com" } })),
rule(
"subdomain",
json!({ "host": { "wildcard": "*.example.com" } }),
),
rule("api", json!({ "path": { "prefix": "/api/" } })),
rule("normalized", json!({ "path": { "eq": "/api/users" } })),
rule("json", json!({ "path": { "suffix": ".json" } })),
rule("debug", json!({ "query": { "debug": { "eq": "1" } } })),
rule(
"no debug",
json!({ "query": { "debug": { "exists": false } } }),
),
rule(
"bot",
json!({ "header": { "user-agent": { "matches": "(?i)bot" } } }),
),
rule("beta", json!({ "cookie": { "beta": { "eq": "1" } } })),
rule("office", json!({ "client": { "in": ["203.0.113.0/24"] } })),
rule(
"not office",
json!({ "client": { "not_in": ["203.0.113.0/24"] } }),
),
];
let request = TestRequest::new("GET", "old.example.com", "//api/./users?debug=1")
.header("user-agent", "GoodBot/1.0")
.header("cookie", "a=b; beta=1");
assert_eq!(
matching(&rules, &request),
[
"get",
"host",
"subdomain",
"api",
"normalized",
"debug",
"bot",
"beta",
"office"
]
);
let mut request = TestRequest::new("DELETE", "example.com", "/data.json");
request.client = Some("::ffff:203.0.113.5".parse().unwrap());
assert_eq!(
matching(&rules, &request),
["not get", "writes", "json", "no debug", "office"]
);
}
#[test]
fn test_and_or_not() {
let rules = [rule(
"legacy",
json!({
"or": {
"and": [
{ "method": { "eq": "GET" }, "host": { "eq": "old.example.com" } },
{
"header": { "x-client": { "eq": "legacy" } },
"not": { "path": { "prefix": "/health" } },
},
],
},
}),
)];
let matches = |request: TestRequest| !matching(&rules, &request).is_empty();
assert!(matches(TestRequest::new("GET", "old.example.com", "/")));
assert!(!matches(TestRequest::new("POST", "old.example.com", "/")));
assert!(matches(
TestRequest::new("POST", "a.test", "/").header("x-client", "legacy")
));
assert!(!matches(
TestRequest::new("POST", "a.test", "/health").header("x-client", "legacy")
));
}
#[test]
fn test_ranges_repeat_a_field() {
let rules = [rule(
"middle",
json!({ "and": { "path": [{ "prefix": "/a" }, { "suffix": "/z" }] } }),
)];
assert_eq!(
matching(&rules, &TestRequest::new("GET", "a.test", "/a/b/z")),
["middle"]
);
assert!(matching(&rules, &TestRequest::new("GET", "a.test", "/a/b")).is_empty());
}
}