Skip to main content

laser_wire/filter/
eval.rs

1use super::codecs::PayloadDecoders;
2use crate::content::ContentType;
3use crate::control::SchemaDef;
4use crate::error::InvalidError;
5use crate::filter::coerce::{ExactDecimal, TimestampFormat};
6use crate::filter::expr::{
7    Coerce, ConsumerFilter, FaultPolicy, FilterCodec, FilterExpr, RecordPolicy,
8};
9use crate::filter::path::{FieldPath, PathSegment};
10use crate::filter::read::{
11    ExplainNode, FaultReason, FilterExplanation, FilterHeader, HeaderScalar, Truth, Verdict,
12};
13use crate::filter::text::TextMatcher;
14use crate::headers::CONTENT_TYPE;
15use crate::limits::MAX_FILTER_PARSE_DEPTH;
16use crate::query::CmpOp;
17use crate::schema::{Digest32, TypedValue};
18use crate::validate::{Validate, json_depth_within};
19use crate::writer_schema::Selection;
20use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor};
21use serde::{Deserialize, Deserializer};
22use serde_json::{Map, Value};
23use std::borrow::Cow;
24use std::cmp::Ordering;
25use std::fmt;
26
27pub use crate::writer_schema::DecodeLimits;
28
29/// One record as the evaluator sees it. Headers are read only when the filter
30/// has header predicates, so a caller may pass them lazily decoded. Header
31/// keys and values are borrowed, so a caller does not copy them per record.
32#[derive(Clone, Copy, Debug)]
33pub struct FilterRecord<'a> {
34    pub payload: &'a [u8],
35    pub headers: &'a [HeaderRef<'a>],
36}
37
38/// One user header of a record, borrowed from wherever the caller holds it.
39#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct HeaderRef<'a> {
41    pub key: &'a str,
42    pub value: HeaderValueRef<'a>,
43}
44
45/// A typed user header value, borrowed. Mirrors [`HeaderScalar`].
46#[derive(Clone, Copy, Debug, PartialEq)]
47pub enum HeaderValueRef<'a> {
48    Bool(bool),
49    Int(i64),
50    Uint(u64),
51    Float(f64),
52    String(&'a str),
53    Raw(&'a [u8]),
54}
55
56impl<'a> From<&'a FilterHeader> for HeaderRef<'a> {
57    fn from(header: &'a FilterHeader) -> Self {
58        Self {
59            key: &header.key,
60            value: match &header.value {
61                HeaderScalar::Bool(value) => HeaderValueRef::Bool(*value),
62                HeaderScalar::Int(value) => HeaderValueRef::Int(*value),
63                HeaderScalar::Uint(value) => HeaderValueRef::Uint(*value),
64                HeaderScalar::Float(value) => HeaderValueRef::Float(*value),
65                HeaderScalar::String(value) => HeaderValueRef::String(value),
66                HeaderScalar::Raw(bytes) => HeaderValueRef::Raw(bytes),
67            },
68        }
69    }
70}
71
72/// What a caller decodes from a record's header block before evaluating.
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum HeaderNeed {
75    None,
76    /// Only `agdx.ct`, to keep a record in another codec out of the decoder.
77    ContentType,
78    All,
79}
80
81/// A validated filter compiled for repeated evaluation. Compile once per
82/// digest, evaluate per record.
83#[derive(Clone, Debug)]
84pub struct CompiledFilter {
85    filter: ConsumerFilter,
86    digest: Digest32,
87    root: Node,
88    paths: PathTrie,
89    reads_headers: bool,
90    decoders: PayloadDecoders,
91}
92
93/// The payload paths a filter reads. Decoding keeps only the values on these
94/// paths, so a filter that reads four fields of a wide change record does not
95/// build the rest of the document. Skipped values are still parsed, so a
96/// malformed payload stays a fault.
97// Children are a short vector searched in order. A filter reads a handful of
98// keys per level, so this beats hashing every key of every record.
99#[derive(Clone, Debug, Default)]
100pub(super) struct PathTrie {
101    pub(super) whole: bool,
102    indices: Vec<(u32, PathTrie)>,
103    keys: Vec<(String, PathTrie)>,
104}
105
106#[derive(Clone, Debug)]
107struct Node {
108    kind: NodeKind,
109    reads_payload: bool,
110}
111
112#[derive(Clone, Debug)]
113enum NodeKind {
114    All(Vec<Node>),
115    Any(Vec<Node>),
116    Not(Box<Node>),
117    Compare {
118        path: FieldPath,
119        op: CmpOp,
120        literal: Literal,
121    },
122    Coerced {
123        path: FieldPath,
124        op: CmpOp,
125        coerce: Coerce,
126        literal: CoercedLiteral,
127    },
128    Present(FieldPath),
129    Absent(FieldPath),
130    Header {
131        key: String,
132        op: CmpOp,
133        literal: Literal,
134    },
135    Text {
136        path: FieldPath,
137        matcher: TextMatcher,
138    },
139    HeaderText {
140        key: String,
141        matcher: TextMatcher,
142    },
143}
144
145#[derive(Clone, Debug, PartialEq)]
146enum Literal {
147    Null,
148    Bool(bool),
149    Int(i128),
150    Float(f64),
151    Text(String),
152    List(Vec<Literal>),
153}
154
155#[derive(Clone, Debug, PartialEq)]
156enum CoercedLiteral {
157    Instant(i64),
158    Decimal(ExactDecimal),
159    List(Vec<CoercedLiteral>),
160}
161
162#[derive(Clone, Debug, PartialEq)]
163enum Comparable {
164    Instant(i64),
165    Decimal(ExactDecimal),
166}
167
168/// A value read from the payload or a header, ready to compare.
169enum Scalar<'a> {
170    Null,
171    Bool(bool),
172    Int(i128),
173    Float(f64),
174    Text(&'a str),
175    Composite,
176}
177
178impl CompiledFilter {
179    /// Validate and compile.
180    pub fn compile(filter: &ConsumerFilter) -> Result<Self, InvalidError> {
181        Self::compile_with_schemas(filter, &[])
182    }
183
184    /// Compile against immutable writer schemas loaded by the caller.
185    pub fn compile_with_schemas(
186        filter: &ConsumerFilter,
187        schemas: &[SchemaDef],
188    ) -> Result<Self, InvalidError> {
189        filter.validate()?;
190        let decoders = PayloadDecoders::compile(filter, schemas)?;
191        let mut paths = PathTrie::default();
192        collect_paths(&filter.expr, &mut paths)?;
193        Ok(Self {
194            filter: filter.clone(),
195            digest: filter.digest(),
196            root: compile_node(&filter.expr)?,
197            paths,
198            reads_headers: filter.expr.reads_headers() || !filter.schema_refs.is_empty(),
199            decoders,
200        })
201    }
202
203    pub fn digest(&self) -> &Digest32 {
204        &self.digest
205    }
206
207    pub fn filter(&self) -> &ConsumerFilter {
208        &self.filter
209    }
210
211    pub fn fault_policy(&self) -> FaultPolicy {
212        self.filter.fault_policy
213    }
214
215    /// The policy a record that faulted for `reason` follows. A record in
216    /// another format follows the foreign policy, a type mismatch the mismatch
217    /// policy, and a payload broken in the filter's own codec the fault policy.
218    /// A `reject` record policy never reaches a fault, so it maps to `drop`.
219    pub fn policy_for(&self, reason: FaultReason) -> FaultPolicy {
220        match self.record_policy(reason) {
221            Some(RecordPolicy::Reject) => FaultPolicy::Drop,
222            Some(RecordPolicy::Pass) => FaultPolicy::Pass,
223            None => self.filter.fault_policy,
224        }
225    }
226
227    /// The record policy that covers `reason`, or `None` for a decode fault.
228    pub fn record_policy(&self, reason: FaultReason) -> Option<RecordPolicy> {
229        if reason.is_foreign() {
230            Some(self.filter.foreign_policy)
231        } else if reason == FaultReason::TypeMismatch {
232            Some(self.filter.mismatch_policy)
233        } else {
234            None
235        }
236    }
237
238    pub fn reads_payload(&self) -> bool {
239        self.root.reads_payload
240    }
241
242    pub fn reads_headers(&self) -> bool {
243        self.reads_headers
244    }
245
246    /// Which headers a caller has to decode before evaluating. A payload filter
247    /// without header predicates needs only `agdx.ct`, which
248    /// [`headers::content_type`](crate::filter::headers::content_type) reads
249    /// without decoding the rest of the block.
250    pub fn header_need(&self) -> HeaderNeed {
251        if self.reads_headers {
252            HeaderNeed::All
253        } else if self.root.reads_payload {
254            HeaderNeed::ContentType
255        } else {
256            HeaderNeed::None
257        }
258    }
259
260    /// Whether the record is selected, rejected, or could not be decoded.
261    /// Header predicates run before the payload is touched, and the payload is
262    /// decoded at most once, only when the headers do not already decide.
263    pub fn evaluate(&self, record: &FilterRecord<'_>, limits: &DecodeLimits) -> Verdict {
264        self.evaluate_with_fault(record, limits).0
265    }
266
267    /// Evaluate once and retain the decode fault without parsing the payload again.
268    pub fn evaluate_with_fault(
269        &self,
270        record: &FilterRecord<'_>,
271        limits: &DecodeLimits,
272    ) -> (Verdict, Option<FaultReason>) {
273        let mut context = Context::new(
274            record,
275            limits,
276            self.filter.codec,
277            &self.paths,
278            &self.decoders,
279        );
280        let truth = context.truth(&self.root);
281        self.evaluated(truth, context.mismatched)
282    }
283
284    /// Report each node's truth, for sample tests and previews. The verdict is
285    /// the delivery verdict of [`evaluate`](Self::evaluate), so headers that
286    /// decide the record still win over an undecodable payload. The tree
287    /// evaluates every node, and payload nodes carry no truth when the payload
288    /// does not decode.
289    pub fn explain(&self, record: &FilterRecord<'_>, limits: &DecodeLimits) -> FilterExplanation {
290        let mut context = Context::new(
291            record,
292            limits,
293            self.filter.codec,
294            &self.paths,
295            &self.decoders,
296        );
297        let truth = context.truth(&self.root);
298        let (verdict, fault) = self.evaluated(truth, context.mismatched);
299        let root = context.explain(&self.root);
300        FilterExplanation {
301            verdict,
302            fault,
303            root,
304        }
305    }
306}
307
308impl CompiledFilter {
309    // A record policy of `reject` decides here: the record is rejected, never
310    // counted as a fault, and the reason travels with the verdict so a preview
311    // or sample test can show why it was skipped. `pass` and every decode
312    // fault surface as a fault with the reason, which the caller resolves
313    // through `policy_for`.
314    fn evaluated(
315        &self,
316        truth: Result<Truth, FaultReason>,
317        mismatched: bool,
318    ) -> (Verdict, Option<FaultReason>) {
319        let reason = match truth {
320            Ok(Truth::Match) => return (Verdict::Selected, None),
321            Ok(Truth::NoMatch) => return (Verdict::Rejected, None),
322            Ok(Truth::Unknown) if !mismatched => return (Verdict::Rejected, None),
323            Ok(Truth::Unknown) => FaultReason::TypeMismatch,
324            Err(reason) => reason,
325        };
326        match self.record_policy(reason) {
327            Some(RecordPolicy::Reject) => (Verdict::Rejected, Some(reason)),
328            Some(RecordPolicy::Pass) | None => (Verdict::Fault, Some(reason)),
329        }
330    }
331}
332
333fn compile_node(expr: &FilterExpr) -> Result<Node, InvalidError> {
334    let kind = match expr {
335        FilterExpr::All(children) => NodeKind::All(
336            children
337                .iter()
338                .map(compile_node)
339                .collect::<Result<_, _>>()?,
340        ),
341        FilterExpr::Any(children) => NodeKind::Any(
342            children
343                .iter()
344                .map(compile_node)
345                .collect::<Result<_, _>>()?,
346        ),
347        FilterExpr::Not(child) => NodeKind::Not(Box::new(compile_node(child)?)),
348        FilterExpr::Pred(predicate) => NodeKind::Compare {
349            path: FieldPath::parse(&predicate.field)?,
350            op: predicate.op,
351            literal: compile_literal(&predicate.value)?,
352        },
353        FilterExpr::PredAs(coerced) => NodeKind::Coerced {
354            path: FieldPath::parse(&coerced.pred.field)?,
355            op: coerced.pred.op,
356            coerce: coerced.coerce,
357            literal: compile_coerced_literal(coerced.coerce, &coerced.pred.value)?,
358        },
359        FilterExpr::Present(path) => NodeKind::Present(path.clone()),
360        FilterExpr::Absent(path) => NodeKind::Absent(path.clone()),
361        FilterExpr::Header(header) => NodeKind::Header {
362            key: header.key.clone(),
363            op: header.op,
364            literal: compile_literal(&header.value)?,
365        },
366        FilterExpr::Text(predicate) => NodeKind::Text {
367            path: FieldPath::parse(&predicate.field)?,
368            matcher: TextMatcher::compile(predicate)?,
369        },
370        FilterExpr::HeaderText(predicate) => NodeKind::HeaderText {
371            key: predicate.field.clone(),
372            matcher: TextMatcher::compile(predicate)?,
373        },
374    };
375    Ok(Node {
376        kind,
377        reads_payload: expr.reads_payload(),
378    })
379}
380
381fn compile_literal(value: &TypedValue) -> Result<Literal, InvalidError> {
382    Ok(match value {
383        TypedValue::Null => Literal::Null,
384        TypedValue::Boolean(value) => Literal::Bool(*value),
385        TypedValue::Int(value) => Literal::Int(i128::from(*value)),
386        TypedValue::Long(value) => Literal::Int(i128::from(*value)),
387        TypedValue::Double(value) => Literal::Float(*value),
388        TypedValue::String(value) => Literal::Text(value.clone()),
389        TypedValue::List(items) => Literal::List(
390            items
391                .iter()
392                .map(compile_literal)
393                .collect::<Result<_, _>>()?,
394        ),
395        other => {
396            return Err(InvalidError::new(format!(
397                "unsupported filter literal {}",
398                other.diagnostic_text()
399            )));
400        }
401    })
402}
403
404fn compile_coerced_literal(
405    coerce: Coerce,
406    value: &TypedValue,
407) -> Result<CoercedLiteral, InvalidError> {
408    if let TypedValue::List(items) = value {
409        return items
410            .iter()
411            .map(|item| compile_coerced_literal(coerce, item))
412            .collect::<Result<_, _>>()
413            .map(CoercedLiteral::List);
414    }
415    let comparable = match (coerce, value) {
416        (Coerce::Timestamp { format }, TypedValue::String(text)) => {
417            format.micros_from_text(text).map(Comparable::Instant)
418        }
419        (Coerce::Timestamp { format }, TypedValue::Int(number)) => format
420            .micros_from_integer(i128::from(*number))
421            .map(Comparable::Instant),
422        (Coerce::Timestamp { format }, TypedValue::Long(number)) => format
423            .micros_from_integer(i128::from(*number))
424            .map(Comparable::Instant),
425        (Coerce::Number, TypedValue::String(text)) => {
426            ExactDecimal::parse(text).map(Comparable::Decimal)
427        }
428        (Coerce::Number, TypedValue::Int(number)) => Some(Comparable::Decimal(
429            ExactDecimal::from_integer(i128::from(*number)),
430        )),
431        (Coerce::Number, TypedValue::Long(number)) => Some(Comparable::Decimal(
432            ExactDecimal::from_integer(i128::from(*number)),
433        )),
434        _ => None,
435    };
436    match comparable {
437        Some(Comparable::Instant(micros)) => Ok(CoercedLiteral::Instant(micros)),
438        Some(Comparable::Decimal(decimal)) => Ok(CoercedLiteral::Decimal(decimal)),
439        None => Err(InvalidError::new(format!(
440            "literal {} does not parse under its coercion",
441            value.diagnostic_text()
442        ))),
443    }
444}
445
446struct Context<'a> {
447    record: &'a FilterRecord<'a>,
448    limits: &'a DecodeLimits,
449    codec: FilterCodec,
450    paths: &'a PathTrie,
451    decoders: &'a PayloadDecoders,
452    payload: Option<Result<Value, FaultReason>>,
453    // A predicate saw a value of a type it cannot compare.
454    mismatched: bool,
455}
456
457impl<'a> Context<'a> {
458    fn new(
459        record: &'a FilterRecord<'a>,
460        limits: &'a DecodeLimits,
461        codec: FilterCodec,
462        paths: &'a PathTrie,
463        decoders: &'a PayloadDecoders,
464    ) -> Self {
465        Self {
466            record,
467            limits,
468            codec,
469            paths,
470            decoders,
471            payload: None,
472            mismatched: false,
473        }
474    }
475
476    fn payload(&mut self) -> Result<&Value, FaultReason> {
477        if self.payload.is_none() {
478            let decoded = if self.declares_foreign_codec() {
479                Err(FaultReason::ForeignCodec)
480            } else {
481                self.decode()
482            };
483            self.payload = Some(decoded);
484        }
485        match self.payload.as_ref().expect("the payload was just decoded") {
486            Ok(value) => Ok(value),
487            Err(reason) => Err(*reason),
488        }
489    }
490
491    // `any` and codes this build does not know decode as usual: the content
492    // type dictionary grows, and an unknown code never rejects a record.
493    fn declares_foreign_codec(&self) -> bool {
494        let Some(header) = self.header(CONTENT_TYPE) else {
495            return false;
496        };
497        let code = match header.value {
498            HeaderValueRef::Uint(code) => u8::try_from(code).ok(),
499            HeaderValueRef::Int(code) => u8::try_from(code).ok(),
500            _ => None,
501        };
502        let declared = match code.and_then(ContentType::from_code) {
503            None | Some(ContentType::Any) => return false,
504            Some(declared) => declared,
505        };
506        let expected = match self.codec {
507            FilterCodec::Json => ContentType::Json,
508            FilterCodec::Cbor => ContentType::Cbor,
509            FilterCodec::Avro => ContentType::Avro,
510            FilterCodec::Protobuf => ContentType::Protobuf,
511            FilterCodec::HeadersOnly | FilterCodec::Unknown => return false,
512        };
513        declared != expected
514    }
515
516    fn decode(&self) -> Result<Value, FaultReason> {
517        match self.codec {
518            FilterCodec::Json => decode_json_paths(self.record.payload, self.limits, self.paths),
519            FilterCodec::Cbor | FilterCodec::Avro | FilterCodec::Protobuf => self
520                .decoders
521                .decode_paths(self.codec, self.record, self.limits, self.paths),
522            FilterCodec::HeadersOnly | FilterCodec::Unknown => Err(FaultReason::Malformed),
523        }
524    }
525
526    fn truth(&mut self, node: &Node) -> Result<Truth, FaultReason> {
527        match &node.kind {
528            NodeKind::All(children) => self.combine(children, Truth::NoMatch, Truth::Match),
529            NodeKind::Any(children) => self.combine(children, Truth::Match, Truth::NoMatch),
530            NodeKind::Not(child) => self.truth(child).map(negate),
531            NodeKind::Header { key, op, literal } => {
532                let (truth, mismatched) = self.header_truth(key, *op, literal);
533                self.mismatched |= mismatched;
534                Ok(truth)
535            }
536            NodeKind::HeaderText { key, matcher } => {
537                let (truth, mismatched) = self.header_text_truth(key, matcher);
538                self.mismatched |= mismatched;
539                Ok(truth)
540            }
541            leaf => {
542                let value = self.payload()?;
543                let (truth, mismatched) = payload_truth(leaf, value);
544                self.mismatched |= mismatched;
545                Ok(truth)
546            }
547        }
548    }
549
550    // `decisive` ends the combination at once (`no_match` for all, `match` for
551    // any). Children that read no payload run first, so headers can decide the
552    // node before the payload is ever decoded.
553    fn combine(
554        &mut self,
555        children: &[Node],
556        decisive: Truth,
557        otherwise: Truth,
558    ) -> Result<Truth, FaultReason> {
559        let mut unknown = false;
560        let header_first = children
561            .iter()
562            .filter(|child| !child.reads_payload)
563            .chain(children.iter().filter(|child| child.reads_payload));
564        for child in header_first {
565            match self.truth(child)? {
566                truth if truth == decisive => return Ok(decisive),
567                Truth::Unknown => unknown = true,
568                _ => {}
569            }
570        }
571        Ok(if unknown { Truth::Unknown } else { otherwise })
572    }
573
574    // The truth, and whether the header exists with a type the predicate
575    // cannot compare.
576    fn header_truth(&self, key: &str, op: CmpOp, literal: &Literal) -> (Truth, bool) {
577        let Some(header) = self.header(key) else {
578            return (Truth::Unknown, false);
579        };
580        let scalar = header_scalar(header.value);
581        let truth = compare_scalar(&scalar, None, op, literal);
582        (
583            truth,
584            truth == Truth::Unknown && !comparable(&scalar, None, op, literal),
585        )
586    }
587
588    fn header_text_truth(&self, key: &str, matcher: &TextMatcher) -> (Truth, bool) {
589        let Some(header) = self.header(key) else {
590            return (Truth::Unknown, false);
591        };
592        match header_scalar(header.value) {
593            Scalar::Text(text) => (truth_of(matcher.matches(text)), false),
594            _ => (Truth::Unknown, true),
595        }
596    }
597
598    // The last entry with a key wins, as the header decoder and the schema id
599    // lookup already resolve duplicates.
600    fn header(&self, key: &str) -> Option<&HeaderRef<'a>> {
601        self.record
602            .headers
603            .iter()
604            .rev()
605            .find(|header| header.key == key)
606    }
607
608    fn explain(&mut self, node: &Node) -> ExplainNode {
609        let (label, children) = match &node.kind {
610            NodeKind::All(children) => ("all".to_owned(), children.as_slice()),
611            NodeKind::Any(children) => ("any".to_owned(), children.as_slice()),
612            NodeKind::Not(child) => ("not".to_owned(), std::slice::from_ref(child.as_ref())),
613            leaf => {
614                let truth = match leaf {
615                    NodeKind::Header { key, op, literal } => {
616                        Some(self.header_truth(key, *op, literal).0)
617                    }
618                    NodeKind::HeaderText { key, matcher } => {
619                        Some(self.header_text_truth(key, matcher).0)
620                    }
621                    _ => self
622                        .payload()
623                        .ok()
624                        .map(|value| payload_truth(leaf, value).0),
625                };
626                return ExplainNode {
627                    label: leaf_label(leaf),
628                    truth,
629                    children: Vec::new(),
630                };
631            }
632        };
633        let explained: Vec<ExplainNode> =
634            children.iter().map(|child| self.explain(child)).collect();
635        let truths: Option<Vec<Truth>> = explained.iter().map(|child| child.truth).collect();
636        let truth = truths.map(|truths| match &node.kind {
637            NodeKind::All(_) => fold(&truths, Truth::NoMatch, Truth::Match),
638            NodeKind::Any(_) => fold(&truths, Truth::Match, Truth::NoMatch),
639            _ => negate(truths[0]),
640        });
641        ExplainNode {
642            label,
643            truth,
644            children: explained,
645        }
646    }
647}
648
649fn fold(truths: &[Truth], decisive: Truth, otherwise: Truth) -> Truth {
650    if truths.contains(&decisive) {
651        decisive
652    } else if truths.contains(&Truth::Unknown) {
653        Truth::Unknown
654    } else {
655        otherwise
656    }
657}
658
659const fn negate(truth: Truth) -> Truth {
660    match truth {
661        Truth::Match => Truth::NoMatch,
662        Truth::NoMatch => Truth::Match,
663        Truth::Unknown => Truth::Unknown,
664    }
665}
666
667// The truth, and whether the field exists, is not null, and has a type the
668// predicate cannot compare. A missing or null field is plain unknown.
669fn payload_truth(leaf: &NodeKind, value: &Value) -> (Truth, bool) {
670    match leaf {
671        NodeKind::Compare { path, op, literal } => match resolve(value, path) {
672            None => (missing_truth(*op, literal), false),
673            Some(field) => {
674                let scalar = scalar_of(field);
675                let truth = compare_scalar(&scalar, Some(field), *op, literal);
676                let mismatched =
677                    truth == Truth::Unknown && !comparable(&scalar, Some(field), *op, literal);
678                (truth, mismatched)
679            }
680        },
681        NodeKind::Coerced {
682            path,
683            op,
684            coerce,
685            literal,
686        } => match resolve(value, path) {
687            None => (Truth::Unknown, false),
688            Some(field) => match coerce_value(*coerce, field) {
689                Some(comparable) => (compare_coerced(&comparable, *op, literal), false),
690                None => (Truth::Unknown, !field.is_null()),
691            },
692        },
693        NodeKind::Text { path, matcher } => match resolve(value, path) {
694            None | Some(Value::Null) => (Truth::Unknown, false),
695            Some(Value::String(text)) => (truth_of(matcher.matches(text)), false),
696            Some(_) => (Truth::Unknown, true),
697        },
698        NodeKind::Present(path) => (truth_of(resolve(value, path).is_some()), false),
699        NodeKind::Absent(path) => (truth_of(resolve(value, path).is_none()), false),
700        NodeKind::All(_)
701        | NodeKind::Any(_)
702        | NodeKind::Not(_)
703        | NodeKind::Header { .. }
704        | NodeKind::HeaderText { .. } => unreachable!("only payload leaves read the payload"),
705    }
706}
707
708fn header_scalar(value: HeaderValueRef<'_>) -> Scalar<'_> {
709    match value {
710        HeaderValueRef::Bool(value) => Scalar::Bool(value),
711        HeaderValueRef::Int(value) => Scalar::Int(i128::from(value)),
712        HeaderValueRef::Uint(value) => Scalar::Int(i128::from(value)),
713        HeaderValueRef::Float(value) => Scalar::Float(value),
714        HeaderValueRef::String(value) => Scalar::Text(value),
715        // Raw bytes that spell UTF-8 compare as text, pinned by the shared
716        // corpus. Anything else has no scalar meaning here.
717        HeaderValueRef::Raw(bytes) => match std::str::from_utf8(bytes) {
718            Ok(text) => Scalar::Text(text),
719            Err(_) => Scalar::Composite,
720        },
721    }
722}
723
724const fn truth_of(matched: bool) -> Truth {
725    if matched {
726        Truth::Match
727    } else {
728        Truth::NoMatch
729    }
730}
731
732// Whether the value's kind can be compared with the literal's kind at all. A
733// comparison that stays unknown between comparable kinds, such as an integer
734// beyond 2^53 against a double or a float header holding NaN, is undecidable,
735// not a type mismatch. A null value is never a mismatch.
736fn comparable(scalar: &Scalar<'_>, field: Option<&Value>, op: CmpOp, literal: &Literal) -> bool {
737    match (scalar, literal) {
738        (Scalar::Null, _) | (_, Literal::Null) => true,
739        (Scalar::Text(_), Literal::Text(_)) | (Scalar::Bool(_), Literal::Bool(_)) => true,
740        (Scalar::Int(_) | Scalar::Float(_), Literal::Int(_) | Literal::Float(_)) => true,
741        (scalar, Literal::List(items)) => {
742            items.iter().any(|item| comparable(scalar, field, op, item))
743        }
744        (Scalar::Composite, _) => op == CmpOp::Contains && matches!(field, Some(Value::Array(_))),
745        _ => false,
746    }
747}
748
749// A missing field is unknown, except for the null tests: `eq null` matches a
750// missing field and `ne null` does not.
751fn missing_truth(op: CmpOp, literal: &Literal) -> Truth {
752    match (op, literal) {
753        (CmpOp::Eq, Literal::Null) => Truth::Match,
754        (CmpOp::Ne, Literal::Null) => Truth::NoMatch,
755        _ => Truth::Unknown,
756    }
757}
758
759fn compare_scalar(
760    scalar: &Scalar<'_>,
761    field: Option<&Value>,
762    op: CmpOp,
763    literal: &Literal,
764) -> Truth {
765    match (scalar, literal) {
766        (Scalar::Null, Literal::Null) => return truth_of(op == CmpOp::Eq),
767        (Scalar::Null, _) => return Truth::Unknown,
768        (_, Literal::Null) => return truth_of(op == CmpOp::Ne),
769        _ => {}
770    }
771    match op {
772        CmpOp::Eq => scalar_eq(scalar, literal).map_or(Truth::Unknown, truth_of),
773        CmpOp::Ne => scalar_eq(scalar, literal).map_or(Truth::Unknown, |equal| truth_of(!equal)),
774        CmpOp::Lt | CmpOp::Lte | CmpOp::Gt | CmpOp::Gte => match scalar_cmp(scalar, literal) {
775            Some(ordering) => truth_of(match op {
776                CmpOp::Lt => ordering == Ordering::Less,
777                CmpOp::Lte => ordering != Ordering::Greater,
778                CmpOp::Gt => ordering == Ordering::Greater,
779                _ => ordering != Ordering::Less,
780            }),
781            None => Truth::Unknown,
782        },
783        CmpOp::In => {
784            let Literal::List(items) = literal else {
785                return Truth::Unknown;
786            };
787            let mut unknown = false;
788            for item in items {
789                match scalar_eq(scalar, item) {
790                    Some(true) => return Truth::Match,
791                    Some(false) => {}
792                    None => unknown = true,
793                }
794            }
795            if unknown {
796                Truth::Unknown
797            } else {
798                Truth::NoMatch
799            }
800        }
801        CmpOp::Contains => match (scalar, literal, field) {
802            (Scalar::Text(text), Literal::Text(needle), _) => {
803                truth_of(text.contains(needle.as_str()))
804            }
805            (Scalar::Composite, _, Some(Value::Array(elements))) => truth_of(
806                elements
807                    .iter()
808                    .any(|element| scalar_eq(&scalar_of(element), literal) == Some(true)),
809            ),
810            _ => Truth::Unknown,
811        },
812        CmpOp::Prefix => match (scalar, literal) {
813            (Scalar::Text(text), Literal::Text(prefix)) => {
814                truth_of(text.starts_with(prefix.as_str()))
815            }
816            _ => Truth::Unknown,
817        },
818    }
819}
820
821fn scalar_eq(scalar: &Scalar<'_>, literal: &Literal) -> Option<bool> {
822    match (scalar, literal) {
823        (Scalar::Bool(left), Literal::Bool(right)) => Some(left == right),
824        (Scalar::Text(left), Literal::Text(right)) => Some(*left == right.as_str()),
825        _ => scalar_cmp(scalar, literal).map(|ordering| ordering == Ordering::Equal),
826    }
827}
828
829fn scalar_cmp(scalar: &Scalar<'_>, literal: &Literal) -> Option<Ordering> {
830    match (scalar, literal) {
831        (Scalar::Int(left), Literal::Int(right)) => Some(left.cmp(right)),
832        (Scalar::Float(left), Literal::Float(right)) => left.partial_cmp(right),
833        (Scalar::Int(left), Literal::Float(right)) => exact_double(*left)?.partial_cmp(right),
834        (Scalar::Float(left), Literal::Int(right)) => left.partial_cmp(&exact_double(*right)?),
835        (Scalar::Text(left), Literal::Text(right)) => Some(left.as_bytes().cmp(right.as_bytes())),
836        _ => None,
837    }
838}
839
840#[allow(clippy::cast_precision_loss)]
841fn exact_double(value: i128) -> Option<f64> {
842    let magnitude = value.unsigned_abs();
843    let significant_bits = u128::BITS - magnitude.leading_zeros();
844    (significant_bits <= 53 || magnitude.trailing_zeros() >= significant_bits - 53)
845        .then_some(value as f64)
846}
847
848fn compare_coerced(comparable: &Comparable, op: CmpOp, literal: &CoercedLiteral) -> Truth {
849    let ordering = |literal: &CoercedLiteral| match (comparable, literal) {
850        (Comparable::Instant(left), CoercedLiteral::Instant(right)) => Some(left.cmp(right)),
851        (Comparable::Decimal(left), CoercedLiteral::Decimal(right)) => Some(left.cmp(right)),
852        _ => None,
853    };
854    match (op, literal) {
855        (CmpOp::In, CoercedLiteral::List(items)) => truth_of(
856            items
857                .iter()
858                .any(|item| ordering(item) == Some(Ordering::Equal)),
859        ),
860        (_, literal) => match ordering(literal) {
861            Some(ordering) => truth_of(match op {
862                CmpOp::Eq => ordering == Ordering::Equal,
863                CmpOp::Ne => ordering != Ordering::Equal,
864                CmpOp::Lt => ordering == Ordering::Less,
865                CmpOp::Lte => ordering != Ordering::Greater,
866                CmpOp::Gt => ordering == Ordering::Greater,
867                CmpOp::Gte => ordering != Ordering::Less,
868                CmpOp::In | CmpOp::Contains | CmpOp::Prefix => return Truth::Unknown,
869            }),
870            None => Truth::Unknown,
871        },
872    }
873}
874
875fn coerce_value(coerce: Coerce, field: &Value) -> Option<Comparable> {
876    match coerce {
877        Coerce::Timestamp { format } => {
878            let micros = match (format, field) {
879                (_, Value::String(text)) => format.micros_from_text(text),
880                (TimestampFormat::Rfc3339, _) => None,
881                (_, Value::Number(number)) => {
882                    let integer = number
883                        .as_i64()
884                        .map(i128::from)
885                        .or_else(|| number.as_u64().map(i128::from))?;
886                    format.micros_from_integer(integer)
887                }
888                _ => None,
889            }?;
890            Some(Comparable::Instant(micros))
891        }
892        Coerce::Number => {
893            let decimal = match field {
894                Value::String(text) => ExactDecimal::parse(text),
895                Value::Number(number) => number
896                    .as_i64()
897                    .map(|value| ExactDecimal::from_integer(i128::from(value)))
898                    .or_else(|| {
899                        number
900                            .as_u64()
901                            .map(|value| ExactDecimal::from_integer(i128::from(value)))
902                    })
903                    .or_else(|| number.as_f64().and_then(ExactDecimal::from_f64)),
904                _ => None,
905            }?;
906            Some(Comparable::Decimal(decimal))
907        }
908    }
909}
910
911fn scalar_of(value: &Value) -> Scalar<'_> {
912    match value {
913        Value::Null => Scalar::Null,
914        Value::Bool(value) => Scalar::Bool(*value),
915        Value::Number(number) => number
916            .as_i64()
917            .map(|value| Scalar::Int(i128::from(value)))
918            .or_else(|| number.as_u64().map(|value| Scalar::Int(i128::from(value))))
919            .or_else(|| number.as_f64().map(Scalar::Float))
920            .unwrap_or(Scalar::Composite),
921        Value::String(text) => Scalar::Text(text),
922        Value::Array(_) | Value::Object(_) => Scalar::Composite,
923    }
924}
925
926fn resolve<'v>(value: &'v Value, path: &FieldPath) -> Option<&'v Value> {
927    path.segments()
928        .iter()
929        .try_fold(value, |current, segment| match (segment, current) {
930            (PathSegment::Key(key), Value::Object(map)) => map.get(key),
931            (PathSegment::Index(index), Value::Array(items)) => items.get(*index as usize),
932            _ => None,
933        })
934}
935
936/// Decode a JSON payload within `limits`. The depth is checked by a scan over
937/// the raw bytes before any value is built, so a hostile payload cannot make
938/// the parser recurse or allocate past the limit.
939pub fn decode_json(payload: &[u8], limits: &DecodeLimits) -> Result<Value, FaultReason> {
940    decode_json_paths(payload, limits, &PathTrie::whole())
941}
942
943// The depth scan also bounds subtrees that the filter does not retain.
944fn decode_json_paths(
945    payload: &[u8],
946    limits: &DecodeLimits,
947    paths: &PathTrie,
948) -> Result<Value, FaultReason> {
949    if payload.len() > limits.max_payload_bytes {
950        return Err(FaultReason::TooLarge);
951    }
952    if !json_depth_within(payload, limits.max_depth.min(MAX_FILTER_PARSE_DEPTH)) {
953        return Err(FaultReason::TooDeep);
954    }
955    let mut deserializer = serde_json::Deserializer::from_slice(payload);
956    let value = Pruned(paths)
957        .deserialize(&mut deserializer)
958        .map_err(|_| FaultReason::Malformed)?;
959    deserializer.end().map_err(|_| FaultReason::Malformed)?;
960    Ok(value)
961}
962
963impl PathTrie {
964    pub(super) fn whole() -> Self {
965        Self {
966            whole: true,
967            ..Self::default()
968        }
969    }
970
971    pub(super) fn child(&self, key: &str) -> Option<&PathTrie> {
972        self.keys
973            .iter()
974            .find_map(|(name, child)| (name == key).then_some(child))
975    }
976
977    fn child_mut(&mut self, key: &str) -> &mut PathTrie {
978        let index = match self.keys.iter().position(|(name, _)| name == key) {
979            Some(index) => index,
980            None => {
981                self.keys.push((key.to_owned(), PathTrie::default()));
982                self.keys.len() - 1
983            }
984        };
985        &mut self.keys[index].1
986    }
987
988    fn index(&self, index: u32) -> Option<&PathTrie> {
989        self.indices
990            .iter()
991            .find_map(|(number, child)| (*number == index).then_some(child))
992    }
993
994    fn index_mut(&mut self, index: u32) -> &mut PathTrie {
995        let position = match self.indices.iter().position(|(number, _)| *number == index) {
996            Some(position) => position,
997            None => {
998                self.indices.push((index, Self::default()));
999                self.indices.len() - 1
1000            }
1001        };
1002        &mut self.indices[position].1
1003    }
1004
1005    fn insert(&mut self, path: &FieldPath) {
1006        let mut node = self;
1007        for segment in path.segments() {
1008            if node.whole {
1009                return;
1010            }
1011            match segment {
1012                PathSegment::Key(key) => node = node.child_mut(key),
1013                PathSegment::Index(index) => node = node.index_mut(*index),
1014            }
1015        }
1016        node.whole = true;
1017        node.keys.clear();
1018        node.indices.clear();
1019    }
1020}
1021
1022impl Selection for PathTrie {
1023    fn field(&self, key: &str) -> Option<&Self> {
1024        if self.whole {
1025            Some(self)
1026        } else {
1027            self.child(key)
1028        }
1029    }
1030
1031    fn item(&self, index: u32) -> Option<&Self> {
1032        if self.whole {
1033            Some(self)
1034        } else {
1035            self.index(index)
1036        }
1037    }
1038}
1039
1040fn collect_paths(expr: &FilterExpr, paths: &mut PathTrie) -> Result<(), InvalidError> {
1041    match expr {
1042        FilterExpr::All(children) | FilterExpr::Any(children) => {
1043            for child in children {
1044                collect_paths(child, paths)?;
1045            }
1046        }
1047        FilterExpr::Not(child) => collect_paths(child, paths)?,
1048        FilterExpr::Pred(predicate) => paths.insert(&FieldPath::parse(&predicate.field)?),
1049        FilterExpr::PredAs(coerced) => paths.insert(&FieldPath::parse(&coerced.pred.field)?),
1050        FilterExpr::Present(path) | FilterExpr::Absent(path) => paths.insert(path),
1051        FilterExpr::Text(predicate) => paths.insert(&FieldPath::parse(&predicate.field)?),
1052        FilterExpr::Header(_) | FilterExpr::HeaderText(_) => {}
1053    }
1054    Ok(())
1055}
1056
1057// Builds a `serde_json::Value` that holds only the values on the trie's paths.
1058// Values off the paths are validated without retention. Array positions stay
1059// stable, and a repeated key keeps its last value, as `Value` does.
1060struct Pruned<'t>(&'t PathTrie);
1061
1062impl<'de> DeserializeSeed<'de> for Pruned<'_> {
1063    type Value = Value;
1064
1065    fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Value, D::Error> {
1066        if self.0.whole {
1067            return Value::deserialize(deserializer);
1068        }
1069        deserializer.deserialize_any(self)
1070    }
1071}
1072
1073impl<'de> Visitor<'de> for Pruned<'_> {
1074    type Value = Value;
1075
1076    fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1077        f.write_str("a JSON value")
1078    }
1079
1080    fn visit_bool<E>(self, value: bool) -> Result<Value, E> {
1081        Ok(Value::Bool(value))
1082    }
1083
1084    fn visit_i64<E>(self, value: i64) -> Result<Value, E> {
1085        Ok(Value::from(value))
1086    }
1087
1088    fn visit_u64<E>(self, value: u64) -> Result<Value, E> {
1089        Ok(Value::from(value))
1090    }
1091
1092    fn visit_f64<E>(self, value: f64) -> Result<Value, E> {
1093        Ok(serde_json::Number::from_f64(value).map_or(Value::Null, Value::Number))
1094    }
1095
1096    fn visit_str<E>(self, _value: &str) -> Result<Value, E> {
1097        Ok(Value::Null)
1098    }
1099
1100    fn visit_unit<E>(self) -> Result<Value, E> {
1101        Ok(Value::Null)
1102    }
1103
1104    fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
1105        let mut items = Vec::new();
1106        let ignored = PathTrie::default();
1107        let mut index = 0_u32;
1108        while let Some(item) =
1109            seq.next_element_seed(Pruned(self.0.index(index).unwrap_or(&ignored)))?
1110        {
1111            if self.0.index(index).is_some() {
1112                items.resize(index as usize, Value::Null);
1113                items.push(item);
1114            }
1115            index = index.saturating_add(1);
1116        }
1117        Ok(Value::Array(items))
1118    }
1119
1120    fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
1121        let mut object = Map::new();
1122        let ignored = PathTrie::default();
1123        while let Some(JsonKey(key)) = map.next_key::<JsonKey<'_>>()? {
1124            match self.0.child(key.as_ref()) {
1125                Some(child) => {
1126                    let value = map.next_value_seed(Pruned(child))?;
1127                    object.insert(key.into_owned(), value);
1128                }
1129                None => {
1130                    // IgnoredAny accepts overflowing numbers and lone
1131                    // surrogates. Unused fields must obey the same JSON
1132                    // rules as fields the filter reads.
1133                    map.next_value_seed(Pruned(&ignored))?;
1134                }
1135            }
1136        }
1137        Ok(Value::Object(object))
1138    }
1139}
1140
1141struct JsonKey<'a>(Cow<'a, str>);
1142
1143impl<'de> Deserialize<'de> for JsonKey<'de> {
1144    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1145        struct KeyVisitor;
1146        impl<'de> Visitor<'de> for KeyVisitor {
1147            type Value = JsonKey<'de>;
1148            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1149                formatter.write_str("an object key")
1150            }
1151            fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E> {
1152                Ok(JsonKey(Cow::Borrowed(value)))
1153            }
1154            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
1155                Ok(JsonKey(Cow::Owned(value.to_owned())))
1156            }
1157            fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
1158                Ok(JsonKey(Cow::Owned(value)))
1159            }
1160        }
1161        deserializer.deserialize_str(KeyVisitor)
1162    }
1163}
1164
1165// String bodies are skipped with memchr, which finds the next quote or
1166// backslash many bytes at a time. Most payload bytes sit inside strings.
1167fn leaf_label(leaf: &NodeKind) -> String {
1168    match leaf {
1169        NodeKind::Compare { path, op, literal } => {
1170            format!("{path} {op} {}", literal_label(literal))
1171        }
1172        NodeKind::Coerced {
1173            path,
1174            op,
1175            coerce,
1176            literal,
1177        } => {
1178            let coerce = match coerce {
1179                Coerce::Timestamp { format } => format!("timestamp({format})"),
1180                Coerce::Number => "number".to_owned(),
1181            };
1182            format!("{coerce}({path}) {op} {}", coerced_label(literal))
1183        }
1184        NodeKind::Present(path) => format!("{path} is present"),
1185        NodeKind::Absent(path) => format!("{path} is absent"),
1186        NodeKind::Header { key, op, literal } => {
1187            format!("header {key} {op} {}", literal_label(literal))
1188        }
1189        NodeKind::Text { path, matcher } => {
1190            format!("{path} {} {}", matcher.kind(), matcher.label())
1191        }
1192        NodeKind::HeaderText { key, matcher } => {
1193            format!("header {key} {} {}", matcher.kind(), matcher.label())
1194        }
1195        NodeKind::All(_) | NodeKind::Any(_) | NodeKind::Not(_) => unreachable!("not a leaf"),
1196    }
1197}
1198
1199fn literal_label(literal: &Literal) -> String {
1200    match literal {
1201        Literal::Null => "null".to_owned(),
1202        Literal::Bool(value) => value.to_string(),
1203        Literal::Int(value) => value.to_string(),
1204        Literal::Float(value) => value.to_string(),
1205        Literal::Text(value) => format!("{value:?}"),
1206        Literal::List(items) => format!(
1207            "[{}]",
1208            items
1209                .iter()
1210                .map(literal_label)
1211                .collect::<Vec<_>>()
1212                .join(", ")
1213        ),
1214    }
1215}
1216
1217fn coerced_label(literal: &CoercedLiteral) -> String {
1218    match literal {
1219        CoercedLiteral::Instant(micros) => format!("{micros}us"),
1220        CoercedLiteral::Decimal(decimal) => format!("{decimal:?}"),
1221        CoercedLiteral::List(items) => format!(
1222            "[{}]",
1223            items
1224                .iter()
1225                .map(coerced_label)
1226                .collect::<Vec<_>>()
1227                .join(", ")
1228        ),
1229    }
1230}
1231
1232#[cfg(test)]
1233mod tests {
1234    use super::*;
1235    use crate::filter::text::TextMatch;
1236
1237    const SAFE_MODE: &str = r#"{"op":"u","table":"satellites","changed":["mode"],"after":{"id":"sat-042","name":"Kestrel-42","mode":"safe","orbit":"leo","battery_pct":61}}"#;
1238    const SAFE_MODE_VALUES: &str = r#"{"op":"u","table":"satellites","after":{"id":"sat-042","name":"Kestrel-42","mode":"safe","orbit":"leo","battery_pct":61}}"#;
1239    const DECOMMISSION: &str = r#"{"op":"d","table":"satellites","before":{"id":"sat-042"}}"#;
1240    const TELEMETRY: &str = r#"{"event":"satellite.telemetry_changed","satellite_id":"sat-042","fields":{"mode":null}}"#;
1241    const GROUND_STATION: &str = r#"{"op":"u","table":"ground_stations","changed":["status"],"after":{"id":"svalbard","status":"online"}}"#;
1242
1243    fn verdict(filter: &ConsumerFilter, payload: &str) -> Verdict {
1244        CompiledFilter::compile(filter).expect("compiles").evaluate(
1245            &FilterRecord {
1246                payload: payload.as_bytes(),
1247                headers: &[],
1248            },
1249            &DecodeLimits::default(),
1250        )
1251    }
1252
1253    fn strict() -> ConsumerFilter {
1254        ConsumerFilter::json(FilterExpr::any([
1255            FilterExpr::all([
1256                FilterExpr::pred("table", CmpOp::Eq, "satellites"),
1257                FilterExpr::pred("op", CmpOp::Eq, "u"),
1258                FilterExpr::pred("changed", CmpOp::Contains, "mode"),
1259                FilterExpr::pred("after.mode", CmpOp::Eq, "safe"),
1260            ]),
1261            FilterExpr::all([
1262                FilterExpr::pred("table", CmpOp::Eq, "satellites"),
1263                FilterExpr::pred("op", CmpOp::Eq, "d"),
1264            ]),
1265            FilterExpr::all([
1266                FilterExpr::pred("event", CmpOp::Eq, "satellite.telemetry_changed"),
1267                FilterExpr::present("fields.mode"),
1268            ]),
1269        ]))
1270    }
1271
1272    fn legacy() -> ConsumerFilter {
1273        ConsumerFilter::json(FilterExpr::all([
1274            FilterExpr::pred("table", CmpOp::Eq, "satellites"),
1275            FilterExpr::pred("op", CmpOp::Eq, "u"),
1276            FilterExpr::pred("after.mode", CmpOp::Eq, "safe"),
1277        ]))
1278    }
1279
1280    #[test]
1281    fn given_an_indexed_array_path_when_decoding_then_should_retain_only_the_requested_field_and_validate_the_rest()
1282     {
1283        let mut paths = PathTrie::default();
1284        paths.insert(&FieldPath::parse("items[1].id").expect("path"));
1285        let value = decode_json_paths(
1286            br#"{"items":[{"other":"unused"},{"id":"chosen","unused":[1,2,3]},{"id":"unread"}]}"#,
1287            &DecodeLimits::default(),
1288            &paths,
1289        )
1290        .expect("decoded");
1291        assert_eq!(value, serde_json::json!({"items":[null,{"id":"chosen"}]}));
1292        assert_eq!(
1293            decode_json_paths(
1294                br#"{"items":[{"bad":"\ud800"},{"id":"chosen"}]}"#,
1295                &DecodeLimits::default(),
1296                &paths
1297            ),
1298            Err(FaultReason::Malformed)
1299        );
1300    }
1301
1302    #[test]
1303    fn given_the_safe_mode_fixtures_when_evaluated_then_should_match_the_scenario_table() {
1304        let expected = [
1305            (SAFE_MODE, Verdict::Selected, Verdict::Selected),
1306            (SAFE_MODE_VALUES, Verdict::Rejected, Verdict::Selected),
1307            (DECOMMISSION, Verdict::Selected, Verdict::Rejected),
1308            (TELEMETRY, Verdict::Selected, Verdict::Rejected),
1309            (GROUND_STATION, Verdict::Rejected, Verdict::Rejected),
1310        ];
1311        for (payload, strict_verdict, legacy_verdict) in expected {
1312            assert_eq!(
1313                verdict(&strict(), payload),
1314                strict_verdict,
1315                "strict on {payload}"
1316            );
1317            assert_eq!(
1318                verdict(&legacy(), payload),
1319                legacy_verdict,
1320                "legacy on {payload}"
1321            );
1322        }
1323    }
1324
1325    #[test]
1326    fn given_missing_and_null_fields_when_compared_then_should_follow_the_null_rules() {
1327        let eq_null =
1328            ConsumerFilter::json(FilterExpr::pred("missing", CmpOp::Eq, TypedValue::Null));
1329        let ne_null =
1330            ConsumerFilter::json(FilterExpr::pred("missing", CmpOp::Ne, TypedValue::Null));
1331        let not_eq = ConsumerFilter::json(FilterExpr::negate(FilterExpr::pred(
1332            "missing",
1333            CmpOp::Eq,
1334            "x",
1335        )));
1336        let present_null = ConsumerFilter::json(FilterExpr::present("value"));
1337        let ne_null_on_null =
1338            ConsumerFilter::json(FilterExpr::pred("value", CmpOp::Ne, TypedValue::Null));
1339        assert_eq!(verdict(&eq_null, r#"{"value":null}"#), Verdict::Selected);
1340        assert_eq!(verdict(&ne_null, r#"{"value":null}"#), Verdict::Rejected);
1341        assert_eq!(
1342            verdict(&not_eq, r#"{}"#),
1343            Verdict::Rejected,
1344            "not(unknown) is unknown"
1345        );
1346        assert_eq!(
1347            verdict(&present_null, r#"{"value":null}"#),
1348            Verdict::Selected
1349        );
1350        assert_eq!(
1351            verdict(&ne_null_on_null, r#"{"value":null}"#),
1352            Verdict::Rejected
1353        );
1354    }
1355
1356    #[test]
1357    fn given_numbers_when_compared_then_should_stay_exact() {
1358        let big =
1359            ConsumerFilter::json(FilterExpr::pred("id", CmpOp::Eq, 9_007_199_254_740_993_i64));
1360        assert_eq!(
1361            verdict(&big, r#"{"id":9007199254740993}"#),
1362            Verdict::Selected
1363        );
1364        assert_eq!(
1365            verdict(&big, r#"{"id":9007199254740992}"#),
1366            Verdict::Rejected
1367        );
1368        let mixed = ConsumerFilter::json(FilterExpr::pred("port", CmpOp::Gte, 5061.0));
1369        assert_eq!(verdict(&mixed, r#"{"port":5061}"#), Verdict::Selected);
1370        let inexact = ConsumerFilter::json(FilterExpr::pred("id", CmpOp::Eq, 1.0));
1371        assert_eq!(
1372            verdict(&inexact, r#"{"id":9007199254740993}"#),
1373            Verdict::Rejected
1374        );
1375        let text_vs_number = ConsumerFilter::json(FilterExpr::pred("port", CmpOp::Eq, "5061"));
1376        assert_eq!(
1377            verdict(&text_vs_number, r#"{"port":5061}"#),
1378            Verdict::Rejected
1379        );
1380    }
1381
1382    #[test]
1383    fn given_contains_and_prefix_when_compared_then_should_be_literal() {
1384        let contains = ConsumerFilter::json(FilterExpr::pred("name", CmpOp::Contains, "%"));
1385        assert_eq!(verdict(&contains, r#"{"name":"100%"}"#), Verdict::Selected);
1386        assert_eq!(verdict(&contains, r#"{"name":"100"}"#), Verdict::Rejected);
1387        let array = ConsumerFilter::json(FilterExpr::pred("tags", CmpOp::Contains, 2));
1388        assert_eq!(verdict(&array, r#"{"tags":[1,2,3]}"#), Verdict::Selected);
1389        assert_eq!(verdict(&array, r#"{"tags":["2"]}"#), Verdict::Rejected);
1390        let prefix = ConsumerFilter::json(FilterExpr::pred("name", CmpOp::Prefix, "eci_"));
1391        assert_eq!(verdict(&prefix, r#"{"name":"eci_x"}"#), Verdict::Selected);
1392        assert_eq!(verdict(&prefix, r#"{"name":"ecix"}"#), Verdict::Rejected);
1393    }
1394
1395    #[test]
1396    fn given_in_lists_when_compared_then_should_match_any_item() {
1397        let filter = ConsumerFilter::json(FilterExpr::pred(
1398            "table",
1399            CmpOp::In,
1400            TypedValue::List(vec!["satellites".into(), "ground_stations".into()]),
1401        ));
1402        assert_eq!(
1403            verdict(&filter, r#"{"table":"ground_stations"}"#),
1404            Verdict::Selected
1405        );
1406        assert_eq!(
1407            verdict(&filter, r#"{"table":"launches"}"#),
1408            Verdict::Rejected
1409        );
1410    }
1411
1412    #[test]
1413    fn given_coercions_when_compared_then_should_use_instants_and_exact_decimals() {
1414        let after = ConsumerFilter::json(FilterExpr::pred_as(
1415            "at",
1416            CmpOp::Gt,
1417            "2026-09-21T18:00:00Z",
1418            Coerce::Timestamp {
1419                format: TimestampFormat::Rfc3339,
1420            },
1421        ));
1422        assert_eq!(
1423            verdict(&after, r#"{"at":"2026-09-21T20:04:12+02:00"}"#),
1424            Verdict::Selected
1425        );
1426        assert_eq!(
1427            verdict(&after, r#"{"at":"2026-09-21T19:59:59+02:00"}"#),
1428            Verdict::Rejected
1429        );
1430        assert_eq!(
1431            verdict(&after, r#"{"at":"2026-09-21T18:04:12"}"#),
1432            Verdict::Rejected,
1433            "no zone is unknown"
1434        );
1435        let millis = ConsumerFilter::json(FilterExpr::pred_as(
1436            "timestamp_ms",
1437            CmpOp::Lt,
1438            "2026-09-21T16:04:13Z",
1439            Coerce::Timestamp {
1440                format: TimestampFormat::EpochMillis,
1441            },
1442        ));
1443        assert!(
1444            CompiledFilter::compile(&millis).is_err(),
1445            "an epoch literal must be an integer"
1446        );
1447        let cpu = ConsumerFilter::json(FilterExpr::pred_as(
1448            "cpu",
1449            CmpOp::Gt,
1450            "9007199254740992",
1451            Coerce::Number,
1452        ));
1453        assert_eq!(
1454            verdict(&cpu, r#"{"cpu":"9007199254740993"}"#),
1455            Verdict::Selected
1456        );
1457        assert_eq!(
1458            verdict(&cpu, r#"{"cpu":"9007199254740992.0"}"#),
1459            Verdict::Rejected
1460        );
1461        assert_eq!(verdict(&cpu, r#"{"cpu":"abc"}"#), Verdict::Rejected);
1462    }
1463
1464    #[test]
1465    fn given_header_predicates_when_they_decide_then_should_skip_the_payload() {
1466        let filter = ConsumerFilter::json(FilterExpr::all([
1467            FilterExpr::pred("op", CmpOp::Eq, "d"),
1468            FilterExpr::header("table", CmpOp::Eq, "satellites"),
1469        ]));
1470        let compiled = CompiledFilter::compile(&filter).expect("compiles");
1471        let ground_station = [HeaderRef {
1472            key: "table",
1473            value: HeaderValueRef::String("ground_stations"),
1474        }];
1475        let rejected = compiled.evaluate(
1476            &FilterRecord {
1477                payload: b"not json at all",
1478                headers: &ground_station,
1479            },
1480            &DecodeLimits::default(),
1481        );
1482        assert_eq!(
1483            rejected,
1484            Verdict::Rejected,
1485            "the header decides before any decode"
1486        );
1487        let satellite = [HeaderRef {
1488            key: "table",
1489            value: HeaderValueRef::String("satellites"),
1490        }];
1491        let fault = compiled.evaluate(
1492            &FilterRecord {
1493                payload: b"not json at all",
1494                headers: &satellite,
1495            },
1496            &DecodeLimits::default(),
1497        );
1498        assert_eq!(
1499            fault,
1500            Verdict::Fault,
1501            "the payload is needed and does not decode"
1502        );
1503    }
1504
1505    #[test]
1506    fn given_payload_limits_when_exceeded_then_should_fault_without_parsing() {
1507        let filter = ConsumerFilter::json(FilterExpr::present("a"));
1508        let compiled = CompiledFilter::compile(&filter).expect("compiles");
1509        let limits = DecodeLimits {
1510            max_payload_bytes: 64,
1511            max_depth: 3,
1512        };
1513        let deep = r#"{"a":{"b":{"c":{"d":1}}}}"#;
1514        let record = FilterRecord {
1515            payload: deep.as_bytes(),
1516            headers: &[],
1517        };
1518        assert_eq!(compiled.evaluate(&record, &limits), Verdict::Fault);
1519        assert_eq!(
1520            compiled.evaluate_with_fault(&record, &limits).1,
1521            Some(FaultReason::TooDeep)
1522        );
1523        let braces_in_strings = r#"{"a":"{{{{{{{{"}"#;
1524        let record = FilterRecord {
1525            payload: braces_in_strings.as_bytes(),
1526            headers: &[],
1527        };
1528        assert_eq!(compiled.evaluate(&record, &limits), Verdict::Selected);
1529        let large = format!(r#"{{"a":"{}"}}"#, "x".repeat(100));
1530        let record = FilterRecord {
1531            payload: large.as_bytes(),
1532            headers: &[],
1533        };
1534        assert_eq!(
1535            compiled.evaluate_with_fault(&record, &limits).1,
1536            Some(FaultReason::TooLarge)
1537        );
1538    }
1539
1540    #[test]
1541    fn given_a_record_when_explained_then_should_report_every_node() {
1542        let compiled = CompiledFilter::compile(&strict()).expect("compiles");
1543        let explanation = compiled.explain(
1544            &FilterRecord {
1545                payload: SAFE_MODE_VALUES.as_bytes(),
1546                headers: &[],
1547            },
1548            &DecodeLimits::default(),
1549        );
1550        assert_eq!(explanation.verdict, Verdict::Rejected);
1551        assert_eq!(explanation.root.label, "any");
1552        assert_eq!(explanation.root.children.len(), 3);
1553        let snapshot_branch = &explanation.root.children[0];
1554        assert_eq!(snapshot_branch.truth, Some(Truth::Unknown));
1555        assert_eq!(
1556            snapshot_branch.children[2].label,
1557            r#"changed contains "mode""#
1558        );
1559        assert_eq!(snapshot_branch.children[2].truth, Some(Truth::Unknown));
1560        let malformed = compiled.explain(
1561            &FilterRecord {
1562                payload: b"{",
1563                headers: &[],
1564            },
1565            &DecodeLimits::default(),
1566        );
1567        assert_eq!(malformed.verdict, Verdict::Fault);
1568        assert_eq!(malformed.fault, Some(FaultReason::Malformed));
1569    }
1570
1571    #[test]
1572    fn given_duplicate_keys_when_decoded_then_should_keep_the_last_value() {
1573        let filter = ConsumerFilter::json(FilterExpr::pred("kind", CmpOp::Eq, "B"));
1574        assert_eq!(
1575            verdict(&filter, r#"{"kind":"A","kind":"B"}"#),
1576            Verdict::Selected
1577        );
1578    }
1579
1580    fn decided(
1581        filter: &ConsumerFilter,
1582        payload: &[u8],
1583        headers: &[HeaderRef<'_>],
1584    ) -> (Verdict, Option<FaultReason>) {
1585        CompiledFilter::compile(filter)
1586            .expect("compiles")
1587            .evaluate_with_fault(&FilterRecord { payload, headers }, &DecodeLimits::default())
1588    }
1589
1590    fn content_type(code: u64) -> HeaderRef<'static> {
1591        HeaderRef {
1592            key: CONTENT_TYPE,
1593            value: HeaderValueRef::Uint(code),
1594        }
1595    }
1596
1597    #[test]
1598    fn given_a_record_declared_in_another_codec_when_a_json_filter_reads_it_then_should_reject_without_decoding()
1599     {
1600        let filter = ConsumerFilter::json(FilterExpr::pred("type", CmpOp::Eq, "metrics"));
1601        let protobuf = content_type(u64::from(ContentType::Protobuf.code()));
1602        assert_eq!(
1603            decided(&filter, b"\x08\x01", &[protobuf]),
1604            (Verdict::Rejected, Some(FaultReason::ForeignCodec))
1605        );
1606        let passing = filter.clone().with_foreign_policy(RecordPolicy::Pass);
1607        assert_eq!(
1608            decided(&passing, b"\x08\x01", &[protobuf]),
1609            (Verdict::Fault, Some(FaultReason::ForeignCodec))
1610        );
1611        let compiled = CompiledFilter::compile(&passing).expect("compiles");
1612        assert_eq!(
1613            compiled.policy_for(FaultReason::ForeignCodec),
1614            FaultPolicy::Pass
1615        );
1616        let json = content_type(u64::from(ContentType::Json.code()));
1617        assert_eq!(
1618            decided(&filter, br#"{"type":"metrics"}"#, &[json]),
1619            (Verdict::Selected, None)
1620        );
1621        assert_eq!(
1622            decided(&filter, br#"{"type":"metrics"}"#, &[content_type(255)]),
1623            (Verdict::Selected, None)
1624        );
1625        assert_eq!(
1626            decided(&filter, br#"{"type":"metrics"}"#, &[content_type(200)]),
1627            (Verdict::Selected, None)
1628        );
1629        assert_eq!(
1630            decided(&filter, b"not json", &[]),
1631            (Verdict::Fault, Some(FaultReason::Malformed))
1632        );
1633    }
1634
1635    #[test]
1636    fn given_a_value_of_another_type_when_compared_then_should_follow_the_mismatch_policy() {
1637        let filter = ConsumerFilter::json(FilterExpr::pred("xyz", CmpOp::Eq, "Abc"));
1638        for payload in [
1639            r#"{"xyz":1}"#,
1640            r#"{"xyz":{"a":1}}"#,
1641            r#"{"xyz":[1]}"#,
1642            r#"{"xyz":true}"#,
1643        ] {
1644            assert_eq!(
1645                decided(&filter, payload.as_bytes(), &[]),
1646                (Verdict::Rejected, Some(FaultReason::TypeMismatch)),
1647                "{payload}"
1648            );
1649        }
1650        let passing = filter.with_mismatch_policy(RecordPolicy::Pass);
1651        assert_eq!(
1652            decided(&passing, br#"{"xyz":1}"#, &[]),
1653            (Verdict::Fault, Some(FaultReason::TypeMismatch))
1654        );
1655        assert_eq!(
1656            decided(&passing, br#"{"xyz":"Abc"}"#, &[]),
1657            (Verdict::Selected, None)
1658        );
1659        assert_eq!(
1660            decided(&passing, br#"{"xyz":"Abd"}"#, &[]),
1661            (Verdict::Rejected, None)
1662        );
1663        assert_eq!(
1664            decided(&passing, br#"{"other":1}"#, &[]),
1665            (Verdict::Rejected, None)
1666        );
1667        assert_eq!(
1668            decided(&passing, br#"{"xyz":null}"#, &[]),
1669            (Verdict::Rejected, None)
1670        );
1671    }
1672
1673    #[test]
1674    fn given_text_predicates_on_a_mixed_log_when_evaluated_then_should_select_the_subdomain() {
1675        let filter = ConsumerFilter::json(FilterExpr::any([
1676            FilterExpr::text("type", TextMatch::Contains, ".v1."),
1677            FilterExpr::text("type", TextMatch::Glob, "ALERTS.*").case_insensitive(),
1678        ]));
1679        assert_eq!(
1680            verdict(&filter, r#"{"type":"metrics.cpu.v1.reported"}"#),
1681            Verdict::Selected
1682        );
1683        assert_eq!(
1684            verdict(&filter, r#"{"type":"alerts.disk.v2.raised"}"#),
1685            Verdict::Selected
1686        );
1687        assert_eq!(
1688            verdict(&filter, r#"{"type":"metrics.cpu.v2.reported"}"#),
1689            Verdict::Rejected
1690        );
1691        let headers = ConsumerFilter::headers_only(FilterExpr::header_text(
1692            "event.type",
1693            TextMatch::Regex,
1694            r"^metrics\.[a-z]+\.v1\.",
1695        ));
1696        let typed = [HeaderRef {
1697            key: "event.type",
1698            value: HeaderValueRef::String("metrics.cpu.v1.reported"),
1699        }];
1700        assert_eq!(
1701            decided(&headers, b"\xff\xfe anything", &typed),
1702            (Verdict::Selected, None)
1703        );
1704        let numeric = [HeaderRef {
1705            key: "event.type",
1706            value: HeaderValueRef::Uint(7),
1707        }];
1708        assert_eq!(
1709            decided(&headers, b"", &numeric),
1710            (Verdict::Rejected, Some(FaultReason::TypeMismatch))
1711        );
1712        let passing = headers.with_mismatch_policy(RecordPolicy::Pass);
1713        assert_eq!(
1714            decided(&passing, b"", &numeric),
1715            (Verdict::Fault, Some(FaultReason::TypeMismatch))
1716        );
1717    }
1718
1719    #[test]
1720    fn given_escaped_quotes_and_deep_nesting_when_depth_is_checked_then_should_count_only_structure()
1721     {
1722        assert!(json_depth_within(br#"{"a":"[[[[\"{{{{"}"#, 1));
1723        assert!(!json_depth_within(br#"{"a":[[1]]}"#, 2));
1724        assert!(json_depth_within(br#"{"a":"\\"}"#, 1));
1725        assert!(json_depth_within(br#"{"a":"unterminated"#, 1));
1726    }
1727}