1use crate::codes::FILTER_OP_VERSION;
2use crate::error::InvalidError;
3use crate::filter::catalog::CatalogPosition;
4use crate::filter::expr::ConsumerFilter;
5use crate::limits::{
6 MAX_FILTER_PREVIEW_EXAMINED, MAX_FILTER_PREVIEW_RECORDS, MAX_FILTER_SAMPLE_BYTES,
7 MAX_FILTER_SAMPLE_HEADERS, MAX_FILTER_SOURCE_NAME_BYTES, MAX_FILTERED_PAGE_BYTES,
8 MAX_FILTERED_PAGE_EXAMINED, MAX_FILTERED_PAGE_RECORDS,
9};
10use crate::result::ResultCode;
11use crate::schema::Digest32;
12use crate::validate::Validate;
13use serde::ser::SerializeStruct;
14use serde::{Deserialize, Deserializer, Serialize, Serializer};
15use std::fmt;
16
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub struct FilterSource {
20 pub stream: String,
21 pub topic: String,
22}
23
24#[derive(Clone, Debug, PartialEq, Eq)]
27pub enum FilterConsumer {
28 Consumer(String),
29 Group(String),
30 GroupId(u64),
31}
32
33impl fmt::Display for FilterConsumer {
34 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::Consumer(name) | Self::Group(name) => formatter.write_str(name),
37 Self::GroupId(id) => write!(formatter, "{id}"),
38 }
39 }
40}
41
42impl FilterConsumer {
43 pub const fn is_group(&self) -> bool {
44 matches!(self, Self::Group(_) | Self::GroupId(_))
45 }
46}
47
48impl Serialize for FilterConsumer {
49 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50 let mut state = serializer.serialize_struct("FilterConsumer", 2)?;
51 match self {
52 Self::Consumer(name) => {
53 state.serialize_field("kind", "consumer")?;
54 state.serialize_field("name", name)?;
55 }
56 Self::Group(name) => {
57 state.serialize_field("kind", "group")?;
58 state.serialize_field("name", name)?;
59 }
60 Self::GroupId(id) => {
61 state.serialize_field("kind", "group_id")?;
62 state.serialize_field("id", id)?;
63 }
64 }
65 state.end()
66 }
67}
68
69impl<'de> Deserialize<'de> for FilterConsumer {
70 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
71 #[derive(Deserialize)]
72 #[serde(tag = "kind", rename_all = "snake_case")]
73 enum Selector {
74 Consumer { name: String },
75 Group { name: String },
76 GroupId { id: u64 },
77 }
78 Ok(match Selector::deserialize(deserializer)? {
79 Selector::Consumer { name } => Self::Consumer(name),
80 Selector::Group { name } => Self::Group(name),
81 Selector::GroupId { id } => Self::GroupId(id),
82 })
83 }
84}
85
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum FilterRef {
90 Inline(ConsumerFilter),
93 Revision { filter_id: u32, revision: u32 },
95 Bound,
98 Group,
102}
103
104#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(rename_all = "snake_case")]
107pub enum FilteredStart {
108 Next,
110 First,
111 Last,
112 Offset(u64),
113 Timestamp(u64),
115 Continue(Continuation),
117}
118
119#[derive(
123 Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize, strum::Display,
124)]
125#[serde(rename_all = "snake_case")]
126#[strum(serialize_all = "snake_case")]
127pub enum ExecutionMode {
128 #[default]
129 Filtered,
130 Unfiltered,
131}
132
133impl ExecutionMode {
134 pub const fn is_filtered(&self) -> bool {
135 matches!(self, Self::Filtered)
136 }
137}
138
139fn validate_policy_identity(
141 mode: ExecutionMode,
142 digest: Option<&Digest32>,
143) -> Result<(), InvalidError> {
144 match (mode, digest) {
145 (ExecutionMode::Filtered, Some(digest)) => digest.validate(),
146 (ExecutionMode::Filtered, None) => {
147 Err(InvalidError::new("a filtered policy names its digest"))
148 }
149 (ExecutionMode::Unfiltered, None) => Ok(()),
150 (ExecutionMode::Unfiltered, Some(_)) => {
151 Err(InvalidError::new("an unfiltered policy carries no digest"))
152 }
153 }
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
161pub struct SourceGeneration {
162 pub stream_id: u32,
163 pub stream_created_at_micros: u64,
164 pub topic_id: u32,
165 pub topic_created_at_micros: u64,
166 pub partition_id: u32,
167 pub partition_incarnation: u64,
168}
169
170#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
174pub struct AckContext {
175 pub owner_generation: u64,
177 pub metadata_op: u64,
179}
180
181#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
183pub struct Continuation {
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub group_id: Option<u64>,
186 pub next_scan_offset: u64,
187 pub generation: SourceGeneration,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub digest: Option<Digest32>,
191 #[serde(default)]
192 pub mode: ExecutionMode,
193 #[serde(default)]
196 pub policy_generation: u64,
197}
198
199#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
201pub struct FilteredPollRequest {
202 pub v: u32,
203 pub source: FilterSource,
204 pub partition_id: u32,
205 pub consumer: FilterConsumer,
206 pub filter: FilterRef,
207 pub start: FilteredStart,
208 pub count: u32,
210 pub max_reply_bytes: u32,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub max_examined: Option<u32>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub min_catalog_position: Option<CatalogPosition>,
221}
222
223#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
225#[serde(rename_all = "snake_case")]
226#[strum(serialize_all = "snake_case")]
227pub enum StopReason {
228 Filled,
230 Budget,
232 EndOfVisible,
234 Fault,
236 OversizedRecord,
238}
239
240#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
242#[serde(rename_all = "snake_case")]
243#[strum(serialize_all = "snake_case")]
244pub enum FaultReason {
245 MissingSchema,
247 SchemaNotAllowed,
249 SchemaMismatch,
251 Malformed,
253 TooLarge,
255 TooDeep,
257 ForeignCodec,
259 TypeMismatch,
261}
262
263impl FaultReason {
264 pub const fn is_foreign(self) -> bool {
267 matches!(
268 self,
269 FaultReason::ForeignCodec
270 | FaultReason::MissingSchema
271 | FaultReason::SchemaNotAllowed
272 | FaultReason::SchemaMismatch
273 )
274 }
275}
276
277#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
279pub struct RecordFault {
280 pub offset: u64,
281 pub reason: FaultReason,
282}
283
284#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
286pub struct FilterDecodeLimits {
287 pub max_payload_bytes: u32,
288 pub max_depth: u32,
289}
290
291#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
293pub struct AppliedPolicy {
294 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub group_id: Option<u64>,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub digest: Option<Digest32>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub filter_id: Option<u32>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub revision: Option<u32>,
303 #[serde(default)]
304 pub mode: ExecutionMode,
305 #[serde(default)]
307 pub policy_generation: u64,
308}
309
310impl AppliedPolicy {
311 #[must_use]
313 pub fn filtered(group_id: Option<u64>, digest: Digest32) -> Self {
314 Self {
315 group_id,
316 digest: Some(digest),
317 filter_id: None,
318 revision: None,
319 mode: ExecutionMode::Filtered,
320 policy_generation: 0,
321 }
322 }
323
324 #[must_use]
326 pub const fn unfiltered(group_id: u64, policy_generation: u64) -> Self {
327 Self {
328 group_id: Some(group_id),
329 digest: None,
330 filter_id: None,
331 revision: None,
332 mode: ExecutionMode::Unfiltered,
333 policy_generation,
334 }
335 }
336}
337
338impl Validate for AppliedPolicy {
339 fn validate(&self) -> Result<(), InvalidError> {
340 validate_policy_identity(self.mode, self.digest.as_ref())?;
341 if self.mode == ExecutionMode::Unfiltered {
342 if self.group_id.is_none() {
343 return Err(InvalidError::new("only a consumer group runs unfiltered"));
344 }
345 if self.filter_id.is_some() || self.revision.is_some() {
346 return Err(InvalidError::new(
347 "an unfiltered policy has no filter reference",
348 ));
349 }
350 }
351 Ok(())
352 }
353}
354
355#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
367pub struct FilteredPage {
368 pub v: u32,
369 pub partition_id: u32,
370 pub policy: AppliedPolicy,
371 pub generation: SourceGeneration,
372 pub ack_context: AckContext,
373 #[serde(default, skip_serializing_if = "Option::is_none")]
374 pub next_scan_offset: Option<u64>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub safe_ack_offset: Option<u64>,
377 pub frontier: u64,
380 pub examined: u32,
381 pub matched: u32,
382 pub stop: StopReason,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub fault: Option<RecordFault>,
385 #[serde(default, skip_serializing_if = "Vec::is_empty")]
388 pub unevaluated: Vec<u64>,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub evaluation_limits: Option<Box<FilterDecodeLimits>>,
391 #[serde(with = "crate::encoding::bin_bytes")]
392 pub records: Vec<u8>,
393}
394
395impl FilteredPage {
396 pub fn next_start(&self, original: &FilteredStart) -> FilteredStart {
399 match self.next_scan_offset {
400 Some(next_scan_offset) => FilteredStart::Continue(Continuation {
401 group_id: self.policy.group_id,
402 next_scan_offset,
403 generation: self.generation,
404 digest: self.policy.digest.clone(),
405 mode: self.policy.mode,
406 policy_generation: self.policy.policy_generation,
407 }),
408 None => original.clone(),
409 }
410 }
411}
412
413#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
417pub struct FilteredAck {
418 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub group_id: Option<u64>,
420 pub v: u32,
421 pub source: FilterSource,
422 pub partition_id: u32,
423 pub consumer: FilterConsumer,
424 pub generation: SourceGeneration,
425 pub ack_context: AckContext,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub digest: Option<Digest32>,
429 pub offset: u64,
430 #[serde(default)]
431 pub mode: ExecutionMode,
432 #[serde(default)]
435 pub policy_generation: u64,
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
440pub struct AckReceipt {
441 pub partition_id: u32,
442 pub offset: u64,
443 pub generation: SourceGeneration,
444}
445
446#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
449pub struct FilterPreviewRequest {
450 pub v: u32,
451 pub source: FilterSource,
452 pub partition_id: u32,
453 pub filter: FilterRef,
455 pub from_offset: u64,
456 pub max_examined: u32,
457 pub max_records: u32,
458 #[serde(default)]
460 pub explain: bool,
461}
462
463#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
465#[serde(rename_all = "snake_case")]
466#[strum(serialize_all = "snake_case")]
467pub enum Verdict {
468 Selected,
469 Rejected,
470 Fault,
471}
472
473#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
475pub struct PreviewRecord {
476 pub offset: u64,
477 pub timestamp_micros: u64,
478 pub verdict: Verdict,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
480 pub fault: Option<FaultReason>,
481 pub payload_text: String,
483 #[serde(default)]
484 pub payload_truncated: bool,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub explanation: Option<FilterExplanation>,
487}
488
489#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
491pub struct FilterPreview {
492 pub v: u32,
493 pub partition_id: u32,
494 pub policy: AppliedPolicy,
495 pub examined: u32,
496 pub matched: u32,
497 pub faults: u32,
498 pub stop: StopReason,
499 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub next_offset: Option<u64>,
501 pub frontier: u64,
502 pub records: Vec<PreviewRecord>,
505}
506
507#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
510#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
511pub enum HeaderScalar {
512 Bool(bool),
513 Int(i64),
514 Uint(u64),
515 Float(f64),
516 String(String),
517 Raw(#[serde(with = "crate::encoding::bin_bytes")] Vec<u8>),
518}
519
520#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
522pub struct FilterHeader {
523 pub key: String,
524 pub value: HeaderScalar,
525}
526
527#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
530pub struct FilterTestRequest {
531 pub v: u32,
532 pub filter: FilterRef,
534 #[serde(with = "crate::encoding::bin_bytes")]
535 pub payload: Vec<u8>,
536 #[serde(default, skip_serializing_if = "Vec::is_empty")]
537 pub headers: Vec<FilterHeader>,
538}
539
540#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
542#[serde(rename_all = "snake_case")]
543#[strum(serialize_all = "snake_case")]
544pub enum Truth {
545 Match,
546 NoMatch,
547 Unknown,
548}
549
550#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
553pub struct ExplainNode {
554 pub label: String,
555 #[serde(default, skip_serializing_if = "Option::is_none")]
556 pub truth: Option<Truth>,
557 #[serde(default, skip_serializing_if = "Vec::is_empty")]
558 pub children: Vec<ExplainNode>,
559}
560
561#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
563pub struct FilterExplanation {
564 pub verdict: Verdict,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub fault: Option<FaultReason>,
567 pub root: ExplainNode,
568}
569
570#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
572pub struct FilterTestResult {
573 pub v: u32,
574 pub policy: AppliedPolicy,
575 pub explanation: FilterExplanation,
576}
577
578#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
580pub struct FilterValidation {
581 pub v: u32,
582 pub digest: Digest32,
583 pub reads_payload: bool,
585 pub reads_headers: bool,
587}
588
589#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, strum::Display)]
592#[serde(rename_all = "snake_case")]
593#[strum(serialize_all = "snake_case")]
594#[non_exhaustive]
595pub enum FilterErrorReason {
596 InvalidRequest,
598 Unsupported,
600 VersionSkew,
602 NotFound,
604 Conflict,
607 SourceChanged,
610 MembershipStale,
612 CatalogUnavailable,
614 RevisionDisabled,
616 TooLarge,
618 Unauthenticated,
619 Forbidden,
620 Unavailable,
622 CapacityExhausted,
624 Backend,
626 #[serde(other)]
629 Unknown,
630}
631
632impl FilterErrorReason {
633 pub const fn code(self) -> ResultCode {
634 match self {
635 Self::InvalidRequest => ResultCode::InvalidArgument,
636 Self::Unsupported => ResultCode::Unsupported,
637 Self::VersionSkew => ResultCode::VersionSkew,
638 Self::NotFound => ResultCode::NotFound,
639 Self::Conflict => ResultCode::Conflict,
640 Self::SourceChanged => ResultCode::StaleGeneration,
641 Self::MembershipStale => ResultCode::Stale,
642 Self::CatalogUnavailable | Self::Unavailable | Self::RevisionDisabled => {
643 ResultCode::Unavailable
644 }
645 Self::TooLarge => ResultCode::TooLarge,
646 Self::Unauthenticated => ResultCode::Unauthenticated,
647 Self::Forbidden => ResultCode::Forbidden,
648 Self::CapacityExhausted => ResultCode::ResourceLimit,
649 Self::Backend | Self::Unknown => ResultCode::Backend,
650 }
651 }
652}
653
654#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
656#[error("{reason}: {message}")]
657pub struct FilterError {
658 pub code: ResultCode,
659 pub reason: FilterErrorReason,
660 pub message: String,
661}
662
663impl FilterError {
664 pub fn new(reason: FilterErrorReason, message: impl Into<String>) -> Self {
665 Self {
666 code: reason.code(),
667 reason,
668 message: message.into(),
669 }
670 }
671
672 pub fn invalid(error: InvalidError) -> Self {
673 Self::new(FilterErrorReason::InvalidRequest, error.0)
674 }
675
676 pub fn check_version(v: u32) -> Result<(), Self> {
684 validate_version(v).map_err(|error| Self::new(FilterErrorReason::VersionSkew, error.0))
685 }
686}
687
688#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
690#[serde(rename_all = "snake_case")]
691pub enum FilterOutcome {
692 Page(FilteredPage),
693 Acknowledged(AckReceipt),
694 Preview(FilterPreview),
695 Tested(FilterTestResult),
696 Validated(FilterValidation),
697}
698
699#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
701#[serde(rename_all = "snake_case")]
702#[allow(clippy::large_enum_variant)]
704pub enum FilterReply {
705 Ok(FilterOutcome),
706 Err(FilterError),
707}
708
709fn validate_version(v: u32) -> Result<(), InvalidError> {
710 if v == FILTER_OP_VERSION {
711 Ok(())
712 } else {
713 Err(InvalidError::new(format!(
714 "filter request version {v} is not supported, expected {FILTER_OP_VERSION}"
715 )))
716 }
717}
718
719pub(crate) fn validate_name(label: &str, name: &str) -> Result<(), InvalidError> {
720 if name.is_empty() || name.len() > MAX_FILTER_SOURCE_NAME_BYTES {
721 return Err(InvalidError::new(format!(
722 "{label} must be 1..={MAX_FILTER_SOURCE_NAME_BYTES} bytes"
723 )));
724 }
725 Ok(())
726}
727
728impl Validate for FilterSource {
729 fn validate(&self) -> Result<(), InvalidError> {
730 validate_name("stream", &self.stream)?;
731 validate_name("topic", &self.topic)
732 }
733}
734
735impl Validate for FilterConsumer {
736 fn validate(&self) -> Result<(), InvalidError> {
737 match self {
738 Self::Consumer(name) => validate_name("consumer", name),
739 Self::Group(name) => validate_name("consumer group", name),
740 Self::GroupId(id) => u32::try_from(*id).map(|_| ()).map_err(|_| {
741 InvalidError::new("consumer group id exceeds the native offset-key range")
742 }),
743 }
744 }
745}
746
747impl Validate for FilterRef {
748 fn validate(&self) -> Result<(), InvalidError> {
749 match self {
750 Self::Inline(filter) => filter.validate(),
751 Self::Revision { .. } | Self::Bound | Self::Group => Ok(()),
752 }
753 }
754}
755
756impl Validate for FilteredPollRequest {
757 fn validate(&self) -> Result<(), InvalidError> {
758 validate_version(self.v)?;
759 self.source.validate()?;
760 self.consumer.validate()?;
761 self.filter.validate()?;
762 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) && !self.consumer.is_group() {
763 return Err(InvalidError::new(
764 "a `bound` or `group` filter needs a consumer group",
765 ));
766 }
767 if self.count == 0 || self.count > MAX_FILTERED_PAGE_RECORDS {
768 return Err(InvalidError::new(format!(
769 "count must be 1..={MAX_FILTERED_PAGE_RECORDS}"
770 )));
771 }
772 if let Some(max_examined) = self.max_examined
773 && (max_examined == 0 || max_examined > MAX_FILTERED_PAGE_EXAMINED)
774 {
775 return Err(InvalidError::new(format!(
776 "max_examined must be 1..={MAX_FILTERED_PAGE_EXAMINED}"
777 )));
778 }
779 if self.max_reply_bytes == 0 || self.max_reply_bytes > MAX_FILTERED_PAGE_BYTES {
780 return Err(InvalidError::new(format!(
781 "max_reply_bytes must be 1..={MAX_FILTERED_PAGE_BYTES}"
782 )));
783 }
784 if let FilteredStart::Continue(continuation) = &self.start {
785 validate_policy_identity(continuation.mode, continuation.digest.as_ref())?;
786 if continuation.mode == ExecutionMode::Unfiltered && !self.consumer.is_group() {
787 return Err(InvalidError::new("only a consumer group runs unfiltered"));
788 }
789 if self.consumer.is_group() != continuation.group_id.is_some() {
790 return Err(InvalidError::new(
791 "a group continuation must retain its group identity",
792 ));
793 }
794 if continuation.generation.partition_id != self.partition_id {
795 return Err(InvalidError::new(
796 "a continuation belongs to another partition",
797 ));
798 }
799 }
800 Ok(())
801 }
802}
803
804impl Validate for FilteredAck {
805 fn validate(&self) -> Result<(), InvalidError> {
806 validate_version(self.v)?;
807 self.source.validate()?;
808 self.consumer.validate()?;
809 validate_policy_identity(self.mode, self.digest.as_ref())?;
810 if self.mode == ExecutionMode::Unfiltered && !self.consumer.is_group() {
811 return Err(InvalidError::new("only a consumer group runs unfiltered"));
812 }
813 if self.consumer.is_group() != self.group_id.is_some() {
814 return Err(InvalidError::new(
815 "a group acknowledgment must retain its group identity",
816 ));
817 }
818 if self.generation.partition_id != self.partition_id {
819 return Err(InvalidError::new(
820 "an acknowledgment names another partition's generation",
821 ));
822 }
823 Ok(())
824 }
825}
826
827impl Validate for FilterPreviewRequest {
828 fn validate(&self) -> Result<(), InvalidError> {
829 validate_version(self.v)?;
830 self.source.validate()?;
831 self.filter.validate()?;
832 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) {
833 return Err(InvalidError::new(
834 "a preview executes an inline filter or a saved revision",
835 ));
836 }
837 if self.max_examined == 0 || self.max_examined > MAX_FILTER_PREVIEW_EXAMINED {
838 return Err(InvalidError::new(format!(
839 "max_examined must be 1..={MAX_FILTER_PREVIEW_EXAMINED}"
840 )));
841 }
842 if self.max_records == 0 || self.max_records > MAX_FILTER_PREVIEW_RECORDS {
843 return Err(InvalidError::new(format!(
844 "max_records must be 1..={MAX_FILTER_PREVIEW_RECORDS}"
845 )));
846 }
847 Ok(())
848 }
849}
850
851impl Validate for FilterTestRequest {
852 fn validate(&self) -> Result<(), InvalidError> {
853 validate_version(self.v)?;
854 self.filter.validate()?;
855 if matches!(self.filter, FilterRef::Bound | FilterRef::Group) {
856 return Err(InvalidError::new(
857 "a sample test executes an inline filter or a saved revision",
858 ));
859 }
860 if self.payload.len() > MAX_FILTER_SAMPLE_BYTES {
861 return Err(InvalidError::new(format!(
862 "a sample payload exceeds {MAX_FILTER_SAMPLE_BYTES}B"
863 )));
864 }
865 if self.headers.len() > MAX_FILTER_SAMPLE_HEADERS {
866 return Err(InvalidError::new(format!(
867 "a sample carries more than {MAX_FILTER_SAMPLE_HEADERS} headers"
868 )));
869 }
870 for header in &self.headers {
871 validate_name("sample header key", &header.key)?;
872 let bytes = match &header.value {
873 HeaderScalar::String(value) => value.len(),
874 HeaderScalar::Raw(value) => value.len(),
875 _ => 0,
876 };
877 if bytes > crate::limits::MAX_FILTER_STRING_BYTES {
878 return Err(InvalidError::new(
879 "a sample header value exceeds its byte limit",
880 ));
881 }
882 }
883 Ok(())
884 }
885}
886
887#[cfg(test)]
888mod tests {
889 use super::*;
890 use crate::filter::expr::FilterExpr;
891 use crate::query::CmpOp;
892
893 fn generation() -> SourceGeneration {
894 SourceGeneration {
895 stream_id: 1,
896 stream_created_at_micros: 10,
897 topic_id: 2,
898 topic_created_at_micros: 20,
899 partition_id: 0,
900 partition_incarnation: 30,
901 }
902 }
903
904 fn request() -> FilteredPollRequest {
905 FilteredPollRequest {
906 v: FILTER_OP_VERSION,
907 source: FilterSource {
908 stream: "orbit".to_owned(),
909 topic: "fleet_changes".to_owned(),
910 },
911 partition_id: 0,
912 consumer: FilterConsumer::Group("anomaly-desk".to_owned()),
913 filter: FilterRef::Bound,
914 start: FilteredStart::Next,
915 count: 100,
916 max_reply_bytes: 1024 * 1024,
917 max_examined: None,
918 min_catalog_position: None,
919 }
920 }
921
922 #[test]
923 fn given_a_group_read_when_validated_then_should_pass() {
924 request().validate().expect("valid");
925 }
926
927 #[test]
928 fn given_out_of_range_reads_when_validated_then_should_be_rejected() {
929 let mut bound_consumer = request();
930 bound_consumer.consumer = FilterConsumer::Consumer("one".to_owned());
931 let mut zero = request();
932 zero.count = 0;
933 let mut huge = request();
934 huge.max_reply_bytes = MAX_FILTERED_PAGE_BYTES + 1;
935 let mut other_partition = request();
936 other_partition.start = FilteredStart::Continue(Continuation {
937 group_id: None,
938 next_scan_offset: 7,
939 generation: SourceGeneration {
940 partition_id: 5,
941 ..generation()
942 },
943 digest: Some(Digest32::new([1; 32])),
944 mode: ExecutionMode::Filtered,
945 policy_generation: 0,
946 });
947 let mut group_consumer = request();
948 group_consumer.filter = FilterRef::Group;
949 group_consumer.consumer = FilterConsumer::Consumer("one".to_owned());
950 let mut over_examined = request();
951 over_examined.max_examined = Some(MAX_FILTERED_PAGE_EXAMINED + 1);
952 let mut zero_examined = request();
953 zero_examined.max_examined = Some(0);
954 let mut unfiltered_with_digest = request();
955 unfiltered_with_digest.start = FilteredStart::Continue(Continuation {
956 group_id: Some(3),
957 next_scan_offset: 7,
958 generation: generation(),
959 digest: Some(Digest32::new([1; 32])),
960 mode: ExecutionMode::Unfiltered,
961 policy_generation: 1,
962 });
963 for request in [
964 bound_consumer,
965 zero,
966 huge,
967 other_partition,
968 group_consumer,
969 over_examined,
970 zero_examined,
971 unfiltered_with_digest,
972 ] {
973 assert!(request.validate().is_err(), "{request:?} must be rejected");
974 }
975 }
976
977 #[test]
978 fn given_an_automatic_group_read_with_an_examined_bound_when_validated_then_should_pass() {
979 let mut automatic = request();
980 automatic.filter = FilterRef::Group;
981 automatic.max_examined = Some(100);
982 automatic.min_catalog_position = Some(CatalogPosition {
983 partition_id: 0,
984 offset: 41,
985 operation_id: Some(11),
986 });
987 automatic.validate().expect("valid");
988 }
989
990 #[test]
991 fn given_an_unfiltered_policy_when_validated_then_should_need_a_group_and_no_digest() {
992 AppliedPolicy::unfiltered(3, 1).validate().expect("valid");
993 assert!(
994 AppliedPolicy::filtered(Some(3), Digest32::new([1; 32]))
995 .validate()
996 .is_ok()
997 );
998 let mut no_group = AppliedPolicy::unfiltered(3, 1);
999 no_group.group_id = None;
1000 assert!(no_group.validate().is_err());
1001 let mut with_digest = AppliedPolicy::unfiltered(3, 1);
1002 with_digest.digest = Some(Digest32::new([1; 32]));
1003 assert!(with_digest.validate().is_err());
1004 let mut with_reference = AppliedPolicy::unfiltered(3, 1);
1005 with_reference.filter_id = Some(1);
1006 assert!(with_reference.validate().is_err());
1007 with_reference.filter_id = None;
1008 with_reference.revision = Some(1);
1009 assert!(with_reference.validate().is_err());
1010 let mut without_digest = AppliedPolicy::filtered(None, Digest32::new([1; 32]));
1011 without_digest.digest = None;
1012 assert!(without_digest.validate().is_err());
1013 }
1014
1015 #[test]
1016 fn given_a_page_when_asked_for_the_next_start_then_should_continue_or_repeat() {
1017 let page = FilteredPage {
1018 v: FILTER_OP_VERSION,
1019 partition_id: 0,
1020 policy: AppliedPolicy {
1021 group_id: None,
1022 digest: Some(Digest32::new([9; 32])),
1023 filter_id: Some(3),
1024 revision: Some(1),
1025 mode: ExecutionMode::Filtered,
1026 policy_generation: 0,
1027 },
1028 generation: generation(),
1029 ack_context: AckContext {
1030 owner_generation: 4,
1031 metadata_op: 40,
1032 },
1033 next_scan_offset: Some(1397),
1034 safe_ack_offset: Some(1396),
1035 frontier: 2000,
1036 examined: 397,
1037 matched: 3,
1038 stop: StopReason::Filled,
1039 fault: None,
1040 unevaluated: Vec::new(),
1041 evaluation_limits: None,
1042 records: Vec::new(),
1043 };
1044 assert_eq!(
1045 page.next_start(&FilteredStart::Next),
1046 FilteredStart::Continue(Continuation {
1047 group_id: None,
1048 next_scan_offset: 1397,
1049 generation: generation(),
1050 digest: Some(Digest32::new([9; 32])),
1051 mode: ExecutionMode::Filtered,
1052 policy_generation: 0,
1053 })
1054 );
1055 let empty = FilteredPage {
1056 next_scan_offset: None,
1057 safe_ack_offset: None,
1058 ..page
1059 };
1060 assert_eq!(empty.next_start(&FilteredStart::Last), FilteredStart::Last);
1061 }
1062
1063 #[test]
1064 fn given_error_reasons_when_classified_then_should_map_to_shared_codes() {
1065 assert_eq!(
1066 FilterError::new(FilterErrorReason::SourceChanged, "recreated").code,
1067 ResultCode::StaleGeneration
1068 );
1069 assert_eq!(FilterErrorReason::MembershipStale.code(), ResultCode::Stale);
1070 assert_eq!(FilterErrorReason::Conflict.code(), ResultCode::Conflict);
1071 assert_eq!(
1072 FilterErrorReason::CatalogUnavailable.code(),
1073 ResultCode::Unavailable
1074 );
1075 }
1076
1077 #[test]
1078 fn given_samples_and_previews_when_validated_then_should_reject_group_selectors() {
1079 let inline = FilterRef::Inline(ConsumerFilter::json(FilterExpr::pred(
1080 "kind",
1081 CmpOp::Eq,
1082 "satellites",
1083 )));
1084 let test = FilterTestRequest {
1085 v: FILTER_OP_VERSION,
1086 filter: inline.clone(),
1087 payload: br#"{"kind":"satellites"}"#.to_vec(),
1088 headers: Vec::new(),
1089 };
1090 test.validate().expect("valid sample");
1091 let mut preview = FilterPreviewRequest {
1092 v: FILTER_OP_VERSION,
1093 source: request().source,
1094 partition_id: 0,
1095 filter: inline,
1096 from_offset: 0,
1097 max_examined: 100,
1098 max_records: 10,
1099 explain: false,
1100 };
1101 preview.validate().expect("valid preview");
1102 for selector in [FilterRef::Bound, FilterRef::Group] {
1103 let sample = FilterTestRequest {
1104 filter: selector.clone(),
1105 ..test.clone()
1106 };
1107 assert!(sample.validate().is_err());
1108 preview.filter = selector;
1109 assert!(preview.validate().is_err());
1110 }
1111 }
1112
1113 #[test]
1114 fn given_oversized_sample_headers_when_validated_then_should_reject_before_evaluation() {
1115 let mut request = FilterTestRequest {
1116 v: FILTER_OP_VERSION,
1117 filter: FilterRef::Inline(ConsumerFilter::headers_only(FilterExpr::header(
1118 "priority",
1119 CmpOp::Eq,
1120 "critical",
1121 ))),
1122 payload: Vec::new(),
1123 headers: vec![FilterHeader {
1124 key: "priority".to_owned(),
1125 value: HeaderScalar::Raw(vec![0; crate::limits::MAX_FILTER_STRING_BYTES + 1]),
1126 }],
1127 };
1128 assert!(request.validate().is_err());
1129 request.headers[0].value = HeaderScalar::String("critical".to_owned());
1130 request.validate().expect("bounded header");
1131 request.headers[0].key.clear();
1132 assert!(request.validate().is_err());
1133 }
1134}