use std::net::IpAddr;
use super::{
expr::{Comparison, FilterExpr},
types::{SocketState, TcpState},
};
const INET_DIAG_BC_JMP: u8 = 1;
const INET_DIAG_BC_S_GE: u8 = 2;
const INET_DIAG_BC_S_LE: u8 = 3;
const INET_DIAG_BC_D_GE: u8 = 4;
const INET_DIAG_BC_D_LE: u8 = 5;
const INET_DIAG_BC_S_COND: u8 = 7;
const INET_DIAG_BC_D_COND: u8 = 8;
const AF_INET: u8 = 2;
const AF_INET6: u8 = 10;
pub const INET_DIAG_REQ_BYTECODE: u16 = 1;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct CompiledFilter {
pub states: Option<u32>,
pub bytecode: Option<Vec<u8>>,
pub exact: bool,
}
#[derive(Debug, Clone, Copy)]
enum HostAddr {
V4([u8; 4]),
V6([u8; 16]),
}
impl HostAddr {
fn len(&self) -> usize {
match self {
Self::V4(_) => 4,
Self::V6(_) => 16,
}
}
}
#[derive(Debug, Clone, Copy)]
enum Prim {
Port { code: u8, port: u16 },
Host {
code: u8,
family: u8,
prefix_len: u8,
addr: HostAddr,
},
Jmp,
}
impl Prim {
fn size(&self) -> usize {
match self {
Self::Port { .. } => 8,
Self::Host { addr, .. } => 4 + 8 + addr.len(),
Self::Jmp => 4,
}
}
}
#[derive(Debug)]
enum Nnf {
And(Vec<Nnf>),
Or(Vec<Nnf>),
Leaf(Prim),
NotHost(Prim),
}
fn flip(cmp: Comparison) -> Comparison {
match cmp {
Comparison::Eq => Comparison::Ne,
Comparison::Ne => Comparison::Eq,
Comparison::Lt => Comparison::Ge,
Comparison::Ge => Comparison::Lt,
Comparison::Le => Comparison::Gt,
Comparison::Gt => Comparison::Le,
}
}
fn lower_port_cmp(is_source: bool, cmp: Comparison, port: u16) -> Option<Nnf> {
let (ge, le) = if is_source {
(INET_DIAG_BC_S_GE, INET_DIAG_BC_S_LE)
} else {
(INET_DIAG_BC_D_GE, INET_DIAG_BC_D_LE)
};
let leaf = |code, port| Nnf::Leaf(Prim::Port { code, port });
Some(match cmp {
Comparison::Eq => Nnf::And(vec![leaf(ge, port), leaf(le, port)]),
Comparison::Ge => leaf(ge, port),
Comparison::Le => leaf(le, port),
Comparison::Gt => leaf(ge, port.checked_add(1)?),
Comparison::Lt => leaf(le, port.checked_sub(1)?),
Comparison::Ne => match (port.checked_sub(1), port.checked_add(1)) {
(Some(lo), Some(hi)) => Nnf::Or(vec![leaf(le, lo), leaf(ge, hi)]),
(None, Some(hi)) => leaf(ge, hi),
(Some(lo), None) => leaf(le, lo),
(None, None) => unreachable!("u16 has more than one value"),
},
})
}
fn lower_host(is_source: bool, addr: &IpAddr, prefix_len: u8) -> Prim {
let code = if is_source {
INET_DIAG_BC_S_COND
} else {
INET_DIAG_BC_D_COND
};
match addr {
IpAddr::V4(v4) => Prim::Host {
code,
family: AF_INET,
prefix_len: prefix_len.min(32),
addr: HostAddr::V4(v4.octets()),
},
IpAddr::V6(v6) => Prim::Host {
code,
family: AF_INET6,
prefix_len: prefix_len.min(128),
addr: HostAddr::V6(v6.octets()),
},
}
}
fn lower(expr: &FilterExpr, negated: bool) -> Option<Nnf> {
match expr {
FilterExpr::And(a, b) => {
let (a, b) = (lower(a, negated)?, lower(b, negated)?);
Some(if negated {
Nnf::Or(vec![a, b]) } else {
Nnf::And(vec![a, b])
})
}
FilterExpr::Or(a, b) => {
let (a, b) = (lower(a, negated)?, lower(b, negated)?);
Some(if negated {
Nnf::And(vec![a, b]) } else {
Nnf::Or(vec![a, b])
})
}
FilterExpr::Not(inner) => lower(inner, !negated),
FilterExpr::Sport(cmp, port) => {
let cmp = if negated { flip(*cmp) } else { *cmp };
lower_port_cmp(true, cmp, *port)
}
FilterExpr::Dport(cmp, port) => {
let cmp = if negated { flip(*cmp) } else { *cmp };
lower_port_cmp(false, cmp, *port)
}
FilterExpr::Src(addr, plen) => {
let prim = lower_host(true, addr, *plen);
Some(if negated {
Nnf::NotHost(prim)
} else {
Nnf::Leaf(prim)
})
}
FilterExpr::Dst(addr, plen) => {
let prim = lower_host(false, addr, *plen);
Some(if negated {
Nnf::NotHost(prim)
} else {
Nnf::Leaf(prim)
})
}
FilterExpr::State(_) => None,
}
}
type LabelId = usize;
#[derive(Debug, Clone, Copy)]
enum Target {
Reject,
Label(LabelId),
}
struct Program {
instrs: Vec<(Prim, Target)>,
labels: Vec<usize>,
}
impl Program {
fn new() -> Self {
Self {
instrs: Vec::new(),
labels: Vec::new(),
}
}
fn new_label(&mut self) -> LabelId {
self.labels.push(usize::MAX);
self.labels.len() - 1
}
fn bind(&mut self, label: LabelId) {
self.labels[label] = self.instrs.len();
}
fn emit_nnf(&mut self, nnf: &Nnf, fail: Target) {
match nnf {
Nnf::And(parts) => {
for p in parts {
self.emit_nnf(p, fail);
}
}
Nnf::Or(parts) => {
let done = self.new_label();
for (i, alt) in parts.iter().enumerate() {
if i + 1 == parts.len() {
self.emit_nnf(alt, fail);
} else {
let next_alt = self.new_label();
self.emit_nnf(alt, Target::Label(next_alt));
self.instrs.push((Prim::Jmp, Target::Label(done)));
self.bind(next_alt);
}
}
self.bind(done);
}
Nnf::Leaf(p) => self.instrs.push((*p, fail)),
Nnf::NotHost(p) => {
let cont = self.new_label();
self.instrs.push((*p, Target::Label(cont)));
self.instrs.push((Prim::Jmp, fail));
self.bind(cont);
}
}
}
fn resolve(&self) -> Option<Vec<u8>> {
let n = self.instrs.len();
let mut offsets = Vec::with_capacity(n + 1);
let mut off = 0usize;
for (prim, _) in &self.instrs {
offsets.push(off);
off += prim.size();
}
offsets.push(off);
let total = off;
if total > u16::MAX as usize - 8 {
return None;
}
let mut bc = Vec::with_capacity(total);
for (i, (prim, target)) in self.instrs.iter().enumerate() {
let here = offsets[i];
let no = match target {
Target::Reject => total - here + 4,
Target::Label(l) => {
let idx = self.labels[*l];
let dest = if idx == usize::MAX || idx >= n {
total
} else {
offsets[idx]
};
dest - here
}
};
let no = u16::try_from(no).ok()?;
let yes = u8::try_from(prim.size()).ok()?;
match prim {
Prim::Port { code, port } => {
bc.push(*code);
bc.push(yes);
bc.extend_from_slice(&no.to_ne_bytes());
bc.push(0);
bc.push(0);
bc.extend_from_slice(&port.to_ne_bytes());
}
Prim::Host {
code,
family,
prefix_len,
addr,
} => {
bc.push(*code);
bc.push(yes);
bc.extend_from_slice(&no.to_ne_bytes());
bc.push(*family);
bc.push(*prefix_len);
bc.extend_from_slice(&[0, 0]); bc.extend_from_slice(&(-1i32).to_ne_bytes());
match addr {
HostAddr::V4(o) => bc.extend_from_slice(o),
HostAddr::V6(o) => bc.extend_from_slice(o),
}
}
Prim::Jmp => {
bc.push(INET_DIAG_BC_JMP);
bc.push(yes);
bc.extend_from_slice(&no.to_ne_bytes());
}
}
}
Some(bc)
}
}
fn nnf_is_exact(nnf: &Nnf) -> bool {
match nnf {
Nnf::And(parts) | Nnf::Or(parts) => parts.iter().all(nnf_is_exact),
Nnf::Leaf(Prim::Port { .. }) => true,
Nnf::Leaf(_) | Nnf::NotHost(_) => false,
}
}
fn state_mask(state: &SocketState) -> u32 {
match state {
SocketState::Tcp(s) => s.mask(),
SocketState::Close => TcpState::Close.mask(),
SocketState::Established => TcpState::Established.mask(),
SocketState::Listen => TcpState::Listen.mask(),
}
}
fn eval_state_mask(expr: &FilterExpr) -> Option<u32> {
match expr {
FilterExpr::State(s) => Some(state_mask(s)),
FilterExpr::And(a, b) => Some(eval_state_mask(a)? & eval_state_mask(b)?),
FilterExpr::Or(a, b) => Some(eval_state_mask(a)? | eval_state_mask(b)?),
FilterExpr::Not(inner) => Some(!eval_state_mask(inner)? & TcpState::all_mask()),
_ => None,
}
}
fn contains_state(expr: &FilterExpr) -> bool {
match expr {
FilterExpr::State(_) => true,
FilterExpr::And(a, b) | FilterExpr::Or(a, b) => contains_state(a) || contains_state(b),
FilterExpr::Not(inner) => contains_state(inner),
_ => false,
}
}
fn conjuncts(expr: &FilterExpr) -> Vec<&FilterExpr> {
match expr {
FilterExpr::And(a, b) => {
let mut out = conjuncts(a);
out.extend(conjuncts(b));
out
}
other => vec![other],
}
}
pub fn compile_filter(expr: &FilterExpr) -> CompiledFilter {
let mut states: Option<u32> = None;
let mut bc_parts: Vec<&FilterExpr> = Vec::new();
let mut exact = true;
for c in conjuncts(expr) {
if let Some(mask) = eval_state_mask(c) {
states = Some(states.unwrap_or(u32::MAX) & mask);
} else if !contains_state(c) {
bc_parts.push(c);
} else {
exact = false;
}
}
let bytecode = if bc_parts.is_empty() {
None
} else {
let nnf = bc_parts
.iter()
.map(|c| lower(c, false))
.collect::<Option<Vec<_>>>()
.map(Nnf::And);
match nnf {
Some(nnf) => {
if !nnf_is_exact(&nnf) {
exact = false;
}
let mut prog = Program::new();
prog.emit_nnf(&nnf, Target::Reject);
let bytes = prog.resolve();
if bytes.is_none() {
exact = false;
}
bytes
}
None => {
exact = false;
None
}
}
};
CompiledFilter {
states,
bytecode,
exact,
}
}
pub fn compile(expr: &FilterExpr) -> Option<Vec<u8>> {
let nnf = lower(expr, false)?;
let mut prog = Program::new();
prog.emit_nnf(&nnf, Target::Reject);
prog.resolve()
}
pub fn for_ports(sport: Option<u16>, dport: Option<u16>) -> Option<Vec<u8>> {
let mut parts = Vec::new();
if let Some(s) = sport {
parts.push(lower_port_cmp(true, Comparison::Eq, s)?);
}
if let Some(d) = dport {
parts.push(lower_port_cmp(false, Comparison::Eq, d)?);
}
if parts.is_empty() {
return None;
}
let mut prog = Program::new();
prog.emit_nnf(&Nnf::And(parts), Target::Reject);
prog.resolve()
}
#[cfg(test)]
mod tests {
use super::*;
fn op(bytes: &[u8], i: usize) -> (u8, u8, u16) {
let b = &bytes[i * 4..i * 4 + 4];
(b[0], b[1], u16::from_ne_bytes([b[2], b[3]]))
}
fn op_at(bytes: &[u8], at: usize) -> (u8, u8, u16) {
(
bytes[at],
bytes[at + 1],
u16::from_ne_bytes([bytes[at + 2], bytes[at + 3]]),
)
}
fn audit(prog: &[u8]) -> Result<(), String> {
let min_len = |code: u8, remaining: usize, at: usize| -> Result<usize, String> {
Ok(match code {
1 => 4, 2..=5 => 8, 7 | 8 => {
if remaining < 12 {
return Err(format!("truncated hostcond at {at}"));
}
let family = prog[at + 4];
let addr_len: usize = match family {
0 => 0,
2 => 4,
10 => 16,
f => return Err(format!("bad family {f} at {at}")),
};
let prefix = prog[at + 5];
if prefix as usize > 8 * addr_len {
return Err(format!("prefix {prefix} too long at {at}"));
}
4 + 8 + addr_len
}
c => return Err(format!("unknown code {c} at {at}")),
})
};
let len = prog.len();
let mut yes_chain = Vec::new();
let mut at = 0usize;
while at < len {
yes_chain.push(at);
if len - at < 4 {
return Err(format!("trailing garbage at {at}"));
}
let (code, yes, no) = (
prog[at],
prog[at + 1] as usize,
u16::from_ne_bytes([prog[at + 2], prog[at + 3]]) as usize,
);
let ml = min_len(code, len - at, at)?;
if yes < ml || yes > len - at || yes % 4 != 0 {
return Err(format!("bad yes {yes} at {at} (min {ml})"));
}
if no < ml || no > len - at + 4 || no % 4 != 0 {
return Err(format!("bad no {no} at {at} (min {ml})"));
}
at += yes;
}
for &start in &yes_chain {
let no = u16::from_ne_bytes([prog[start + 2], prog[start + 3]]) as usize;
let dest = start + no;
if dest < len && !yes_chain.contains(&dest) {
return Err(format!("no-target {dest} from {start} not on yes-chain"));
}
if dest > len + 4 {
return Err(format!("no-target {dest} overshoots past len+4"));
}
}
Ok(())
}
fn assert_audited(expr: &str) -> Vec<u8> {
let bc = compile(&FilterExpr::parse(expr).unwrap())
.unwrap_or_else(|| panic!("`{expr}` must compile"));
audit(&bc).unwrap_or_else(|e| panic!("`{expr}` fails kernel audit: {e}\n{bc:02x?}"));
bc
}
#[test]
fn sport_eq_compiles_to_ge_le_range() {
let bc = assert_audited("sport = :22");
assert_eq!(bc.len(), 16);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_GE, 8, 20));
assert_eq!(op(&bc, 1), (0, 0, 22));
assert_eq!(op(&bc, 2), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op(&bc, 3), (0, 0, 22));
}
#[test]
fn dport_gt_uses_ge_plus_one() {
let bc = assert_audited("dport > :1024");
assert_eq!(bc.len(), 8);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_D_GE, 8, 12));
assert_eq!(op(&bc, 1), (0, 0, 1025));
}
#[test]
fn sport_lt_uses_le_minus_one() {
let bc = assert_audited("sport < :1024");
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op(&bc, 1), (0, 0, 1023));
}
#[test]
fn and_chain_offsets_decrease() {
let bc = assert_audited("sport = :22 and dport >= :80");
assert_eq!(bc.len(), 24);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_GE, 8, 28));
assert_eq!(op(&bc, 2), (INET_DIAG_BC_S_LE, 8, 20));
assert_eq!(op(&bc, 4), (INET_DIAG_BC_D_GE, 8, 12));
for i in [0usize, 2, 4] {
assert_eq!(op(&bc, i).2 % 4, 0);
}
}
#[test]
fn for_ports_builds_exact_match_program() {
let bc = for_ports(Some(22), None).unwrap();
audit(&bc).unwrap();
assert_eq!(bc.len(), 16);
assert_eq!(op(&bc, 0), (INET_DIAG_BC_S_GE, 8, 20));
assert_eq!(op(&bc, 2), (INET_DIAG_BC_S_LE, 8, 12));
let bc = for_ports(Some(22), Some(443)).unwrap();
audit(&bc).unwrap();
assert_eq!(bc.len(), 32);
assert_eq!(op(&bc, 0).0, INET_DIAG_BC_S_GE);
assert_eq!(op(&bc, 4).0, INET_DIAG_BC_D_GE);
assert_eq!(op(&bc, 6), (INET_DIAG_BC_D_LE, 8, 12));
assert!(for_ports(None, None).is_none());
}
#[test]
fn boundary_ports_that_cannot_match_return_none() {
assert!(compile(&FilterExpr::parse("dport > :65535").unwrap()).is_none());
assert!(compile(&FilterExpr::parse("sport < :0").unwrap()).is_none());
}
#[test]
fn src_v4_prefix_emits_hostcond() {
let bc = assert_audited("src 10.0.0.0/8");
assert_eq!(bc.len(), 16);
assert_eq!(op_at(&bc, 0), (INET_DIAG_BC_S_COND, 16, 20));
assert_eq!(bc[4], AF_INET);
assert_eq!(bc[5], 8); assert_eq!(&bc[6..8], &[0, 0]); assert_eq!(i32::from_ne_bytes(bc[8..12].try_into().unwrap()), -1);
assert_eq!(&bc[12..16], &[10, 0, 0, 0]);
}
#[test]
fn dst_v6_emits_28_byte_hostcond() {
let bc = assert_audited("dst ::1");
assert_eq!(bc.len(), 28);
assert_eq!(op_at(&bc, 0), (INET_DIAG_BC_D_COND, 28, 32));
assert_eq!(bc[4], AF_INET6);
assert_eq!(bc[5], 128);
let mut want = [0u8; 16];
want[15] = 1;
assert_eq!(&bc[12..28], &want);
}
#[test]
fn prefix_len_clamps_to_family_max() {
let a = compile(&FilterExpr::parse("src 10.0.0.1/40").unwrap()).unwrap();
let b = compile(&FilterExpr::parse("src 10.0.0.1/32").unwrap()).unwrap();
assert_eq!(a, b);
}
#[test]
fn or_of_two_sports_lowers_with_jump() {
let bc = assert_audited("sport = :22 or sport = :80");
assert_eq!(bc.len(), 36);
assert_eq!(op_at(&bc, 0), (INET_DIAG_BC_S_GE, 8, 20));
assert_eq!(op_at(&bc, 8), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op_at(&bc, 16), (INET_DIAG_BC_JMP, 4, 20));
assert_eq!(op_at(&bc, 20), (INET_DIAG_BC_S_GE, 8, 20));
assert_eq!(op_at(&bc, 28), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op_at(&bc, 28).2 as usize + 28, bc.len() + 4);
}
#[test]
fn sport_ne_lowers_to_le_or_ge() {
let bc = assert_audited("sport != :22");
assert_eq!(bc.len(), 20);
assert_eq!(op_at(&bc, 0), (INET_DIAG_BC_S_LE, 8, 12));
assert_eq!(op_at(&bc, 4).2, 21);
assert_eq!(op_at(&bc, 8), (INET_DIAG_BC_JMP, 4, 12));
assert_eq!(op_at(&bc, 12), (INET_DIAG_BC_S_GE, 8, 12));
assert_eq!(op_at(&bc, 16).2, 23);
}
#[test]
fn not_sport_eq_equals_ne() {
let a = compile(&FilterExpr::parse("not sport = :22").unwrap()).unwrap();
let b = compile(&FilterExpr::parse("sport != :22").unwrap()).unwrap();
assert_eq!(a, b);
}
#[test]
fn ne_edge_ports_collapse_to_single_bound() {
let bc = assert_audited("sport != :0");
assert_eq!(bc.len(), 8);
assert_eq!(op_at(&bc, 0).0, INET_DIAG_BC_S_GE);
assert_eq!(op_at(&bc, 4).2, 1);
let bc = assert_audited("sport != :65535");
assert_eq!(bc.len(), 8);
assert_eq!(op_at(&bc, 0).0, INET_DIAG_BC_S_LE);
assert_eq!(op_at(&bc, 4).2, 65534);
}
#[test]
fn not_host_inverts_via_skip_jump() {
let bc = assert_audited("not dst 10.0.0.0/8");
assert_eq!(bc.len(), 20);
assert_eq!(op_at(&bc, 0), (INET_DIAG_BC_D_COND, 16, 20));
assert_eq!(op_at(&bc, 16), (INET_DIAG_BC_JMP, 4, 8));
}
#[test]
fn de_morgan_pushes_not_through_and() {
let a = compile(&FilterExpr::parse("not ( sport = :22 and dst 10.0.0.0/8 )").unwrap())
.unwrap();
let b =
compile(&FilterExpr::parse("sport != :22 or not dst 10.0.0.0/8").unwrap()).unwrap();
assert_eq!(a, b);
audit(&a).unwrap();
}
#[test]
fn nested_or_and_chains_pass_audit() {
for expr in [
"( sport = :22 or sport = :80 ) and dst 10.0.0.0/8",
"src 127.0.0.1 or src ::1",
"not ( sport = :22 or dport = :443 )",
"( src 10.0.0.0/8 or src 192.168.0.0/16 ) and not dport = :53",
"sport >= :1024 and sport <= :2048 and dst 10.1.2.3",
] {
assert_audited(expr);
}
}
#[test]
fn huge_or_chain_overflows_to_none() {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(|| {
let mut s = String::from("sport = :1");
for p in 2..5000u32 {
s.push_str(&format!(" or sport = :{p}"));
}
assert!(compile(&FilterExpr::parse(&s).unwrap()).is_none());
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn pure_state_hoists_to_mask_only() {
let c = compile_filter(&FilterExpr::parse("state established").unwrap());
assert_eq!(c.states, Some(TcpState::Established.mask()));
assert!(c.bytecode.is_none());
assert!(c.exact);
}
#[test]
fn state_or_state_unions_masks() {
let c = compile_filter(&FilterExpr::parse("state established or state time-wait").unwrap());
assert_eq!(
c.states,
Some(TcpState::Established.mask() | TcpState::TimeWait.mask())
);
assert!(c.bytecode.is_none());
assert!(c.exact);
}
#[test]
fn not_state_complements_within_all_mask() {
let c = compile_filter(&FilterExpr::parse("not state listen").unwrap());
assert_eq!(
c.states,
Some(TcpState::all_mask() & !TcpState::Listen.mask())
);
assert!(c.exact);
}
#[test]
fn state_and_port_split_between_mask_and_bytecode() {
let c = compile_filter(&FilterExpr::parse("state established and sport = :22").unwrap());
assert_eq!(c.states, Some(TcpState::Established.mask()));
let bc = c.bytecode.expect("port half compiles");
audit(&bc).unwrap();
assert!(c.exact);
}
#[test]
fn state_under_or_with_port_is_client_side_only() {
let c = compile_filter(&FilterExpr::parse("sport = :22 or state established").unwrap());
assert_eq!(c.states, None);
assert!(c.bytecode.is_none());
assert!(!c.exact);
}
#[test]
fn mixed_or_conjunct_drops_but_other_conjuncts_compile() {
let c = compile_filter(
&FilterExpr::parse("( sport = :22 or state listen ) and dport = :443").unwrap(),
);
assert_eq!(c.states, None);
let bc = c.bytecode.expect("dport conjunct still compiles");
audit(&bc).unwrap();
assert_eq!(bc.len(), 16);
assert_eq!(op_at(&bc, 0).0, INET_DIAG_BC_D_GE);
assert!(!c.exact);
}
#[test]
fn host_conditions_are_inexact() {
let c = compile_filter(&FilterExpr::parse("src 10.0.0.0/8").unwrap());
assert!(c.bytecode.is_some());
assert!(!c.exact);
let c = compile_filter(&FilterExpr::parse("sport = :22").unwrap());
assert!(c.bytecode.is_some());
assert!(c.exact, "pure port programs are exact");
}
#[test]
fn compile_filter_of_port_expr_matches_compile() {
let expr = FilterExpr::parse("sport = :22 and dport >= :80").unwrap();
let c = compile_filter(&expr);
assert_eq!(c.bytecode, compile(&expr));
assert_eq!(c.states, None);
}
#[test]
fn audit_oracle_rejects_malformed_programs() {
assert!(audit(&[9, 8, 0, 0]).is_err()); assert!(audit(&[2, 8, 3, 0, 0, 0, 22, 0]).is_err()); let mut bad = compile(&FilterExpr::parse("sport = :22").unwrap()).unwrap();
bad[1] = 4; assert!(audit(&bad).is_err());
}
}