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#[derive(Clone, Copy, Debug)]
33pub struct FilterRecord<'a> {
34 pub payload: &'a [u8],
35 pub headers: &'a [HeaderRef<'a>],
36}
37
38#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct HeaderRef<'a> {
41 pub key: &'a str,
42 pub value: HeaderValueRef<'a>,
43}
44
45#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
74pub enum HeaderNeed {
75 None,
76 ContentType,
78 All,
79}
80
81#[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#[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
168enum Scalar<'a> {
170 Null,
171 Bool(bool),
172 Int(i128),
173 Float(f64),
174 Text(&'a str),
175 Composite,
176}
177
178impl CompiledFilter {
179 pub fn compile(filter: &ConsumerFilter) -> Result<Self, InvalidError> {
181 Self::compile_with_schemas(filter, &[])
182 }
183
184 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 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 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 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 pub fn evaluate(&self, record: &FilterRecord<'_>, limits: &DecodeLimits) -> Verdict {
264 self.evaluate_with_fault(record, limits).0
265 }
266
267 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 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 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 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 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 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 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 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
667fn 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 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
732fn 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
749fn 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
936pub fn decode_json(payload: &[u8], limits: &DecodeLimits) -> Result<Value, FaultReason> {
940 decode_json_paths(payload, limits, &PathTrie::whole())
941}
942
943fn 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
1057struct 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 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
1165fn 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(¬_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}