Skip to main content

sift_query_core/policy/
mod.rs

1//! Immutable policy configuration, limits and entity validation.
2use crate::{
3    Codec, CodecRegistry, ConfigError, CursorCodec, ParamError, RawParamFilter, RawParams,
4};
5use sift_query_algorithm::BoundedLruMap;
6use std::any::TypeId;
7use std::borrow::Cow;
8use std::sync::{Arc, Mutex};
9
10pub(crate) mod budget;
11mod fingerprint;
12mod limits;
13pub(crate) mod names;
14mod validation;
15use fingerprint::{compatibility_fingerprint, policy_fingerprint};
16use limits::ensure_within_hard_limits;
17pub use limits::{QueryHardLimits, QueryLimits};
18pub use names::ParamNames;
19use names::is_name;
20
21/// Immutable parser and execution policy.
22#[derive(Debug, Clone)]
23pub struct QueryPolicy {
24    names: ParamNames,
25    limits: QueryLimits,
26    hard_limits: QueryHardLimits,
27    compatibility: CompatibilityPolicy,
28    cursor_codec: Option<CursorCodec>,
29    filters: Vec<Arc<dyn RawParamFilter>>,
30    codecs: CodecRegistry,
31    operators: Vec<crate::RegisteredOperator>,
32    fingerprint: u64,
33    validated_names: Arc<Mutex<BoundedLruMap<TypeId, Result<(), ParamError>>>>,
34    validated_codecs: Arc<Mutex<BoundedLruMap<TypeId, Result<(), ParamError>>>>,
35    codec_bindings: Arc<Mutex<BoundedLruMap<TypeId, Result<Arc<[CodecBinding]>, ParamError>>>>,
36    validated_operators: Arc<Mutex<BoundedLruMap<TypeId, Result<(), ParamError>>>>,
37}
38
39#[derive(Debug, Clone)]
40struct CodecBinding {
41    field: &'static crate::FieldMeta,
42    codec: Arc<dyn Codec>,
43}
44
45#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
46pub enum CompatibilityPolicy {
47    #[default]
48    Strict,
49    BeanSearcher4812,
50}
51
52impl Default for QueryPolicy {
53    fn default() -> Self {
54        let names = ParamNames::default();
55        let limits = QueryLimits::default();
56        Self {
57            fingerprint: policy_fingerprint(&names, &limits),
58            names,
59            hard_limits: QueryHardLimits::default(),
60            limits,
61            compatibility: CompatibilityPolicy::Strict,
62            cursor_codec: None,
63            filters: Vec::new(),
64            codecs: CodecRegistry::default(),
65            operators: Vec::new(),
66            validated_names: name_cache(),
67            validated_codecs: validation_cache(),
68            codec_bindings: codec_binding_cache(),
69            validated_operators: validation_cache(),
70        }
71    }
72}
73
74impl QueryPolicy {
75    pub fn new(names: ParamNames, limits: QueryLimits) -> Result<Self, ConfigError> {
76        Self::with_hard_limits(names, limits, QueryHardLimits::default())
77    }
78
79    /// Builds a policy with explicitly elevated hard limits.
80    pub fn with_hard_limits(
81        names: ParamNames,
82        limits: QueryLimits,
83        hard_limits: QueryHardLimits,
84    ) -> Result<Self, ConfigError> {
85        names.validate()?;
86        ensure_within_hard_limits(&limits, &hard_limits.0)?;
87        if limits.timeout_ms == 0 {
88            return Err(ConfigError::Invalid(
89                "query timeout must be greater than zero".to_owned(),
90            ));
91        }
92        Ok(Self {
93            fingerprint: policy_fingerprint(&names, &limits),
94            names,
95            limits,
96            hard_limits,
97            compatibility: CompatibilityPolicy::Strict,
98            cursor_codec: None,
99            filters: Vec::new(),
100            codecs: CodecRegistry::default(),
101            operators: Vec::new(),
102            validated_names: name_cache(),
103            validated_codecs: validation_cache(),
104            codec_bindings: codec_binding_cache(),
105            validated_operators: validation_cache(),
106        })
107    }
108
109    #[must_use]
110    pub fn bean_searcher_4812() -> Self {
111        let mut policy = Self::default();
112        policy.compatibility = CompatibilityPolicy::BeanSearcher4812;
113        policy.fingerprint = policy_fingerprint(&policy.names, &policy.limits)
114            ^ compatibility_fingerprint(policy.compatibility);
115        policy
116    }
117
118    #[must_use]
119    pub fn with_cursor_codec(mut self, codec: CursorCodec) -> Self {
120        self.cursor_codec = Some(codec);
121        self.recompute_fingerprint();
122        self
123    }
124
125    /// Appends a server-owned parameter normalization step.
126    #[must_use]
127    pub fn with_raw_param_filter<F: RawParamFilter + 'static>(mut self, filter: F) -> Self {
128        self.filters.push(Arc::new(filter));
129        self.recompute_fingerprint();
130        self
131    }
132
133    /// Appends a named input/result codec.
134    #[must_use]
135    pub fn with_codec<C: Codec + 'static>(mut self, codec: C) -> Self {
136        self.codecs.push(codec);
137        self.validated_codecs = validation_cache();
138        self.codec_bindings = codec_binding_cache();
139        self.recompute_fingerprint();
140        self
141    }
142
143    /// Registers one typed HTTP operator. Aliases cannot shadow builtins.
144    pub fn with_custom_operator(
145        mut self,
146        operator: crate::CustomOperator,
147        input_type: crate::ValueType,
148    ) -> Result<Self, ConfigError> {
149        if !is_name(operator.name())
150            || operator.name().len() > 64
151            || crate::Operator::parse(operator.name()).is_some()
152            || self
153                .operators
154                .iter()
155                .any(|item| item.operator.name() == operator.name())
156            || self.operators.len() >= 64
157        {
158            return Err(ConfigError::Invalid(
159                "invalid, duplicate or reserved custom operator registration".to_owned(),
160            ));
161        }
162        if operator.types().is_some_and(|types| types.1 != input_type) {
163            return Err(ConfigError::Invalid(
164                "operator input type differs from registration".to_owned(),
165            ));
166        }
167        self.operators.push(crate::RegisteredOperator {
168            operator,
169            field_type: operator.types().map_or(input_type, |types| types.0),
170            input_type,
171        });
172        self.validated_operators = validation_cache();
173        self.recompute_fingerprint();
174        Ok(self)
175    }
176
177    /// Registers a server-owned SQL extension and verifies its generated field
178    /// whitelist against an entity before it can be exposed to HTTP.
179    pub fn with_server_sql_extension<E: crate::SearchEntity>(
180        self,
181        extension: crate::ServerSqlExtension,
182    ) -> Result<Self, ConfigError> {
183        for field_name in extension.fields {
184            let field = E::META.resolve_field(field_name).ok_or_else(|| {
185                ConfigError::Invalid(format!(
186                    "server SQL extension references unknown field {field_name}"
187                ))
188            })?;
189            if !E::custom_operator_names(field.id).contains(&extension.operator.name()) {
190                return Err(ConfigError::Invalid(format!(
191                    "server SQL extension {} is not whitelisted on field {}",
192                    extension.operator.name(),
193                    field_name
194                )));
195            }
196        }
197        self.with_custom_operator(extension.operator, extension.input_type)
198    }
199
200    /// Registers every bounded, server-owned SQL extension in a registry for
201    /// one entity. The registry itself contains no request data and is
202    /// expected to be constructed during application startup.
203    pub fn with_server_sql_registry<E: crate::SearchEntity>(
204        mut self,
205        registry: &crate::ServerSqlRegistry,
206    ) -> Result<Self, ConfigError> {
207        for extension in registry.iter().copied() {
208            self = self.with_server_sql_extension::<E>(extension)?;
209        }
210        Ok(self)
211    }
212
213    #[must_use]
214    pub fn registered_operator(&self, name: &str) -> Option<&crate::RegisteredOperator> {
215        self.operators
216            .iter()
217            .find(|item| item.operator.name() == name)
218    }
219
220    #[must_use]
221    pub fn names(&self) -> &ParamNames {
222        &self.names
223    }
224
225    /// Copies the policy for a remote endpoint with different control names.
226    pub fn with_param_names(mut self, names: ParamNames) -> Result<Self, ConfigError> {
227        names.validate()?;
228        self.names = names;
229        self.validated_names = name_cache();
230        self.recompute_fingerprint();
231        Ok(self)
232    }
233
234    #[must_use]
235    pub fn limits(&self) -> &QueryLimits {
236        &self.limits
237    }
238
239    #[must_use]
240    pub fn hard_limits(&self) -> &QueryHardLimits {
241        &self.hard_limits
242    }
243
244    #[must_use]
245    pub const fn fingerprint(&self) -> u64 {
246        self.fingerprint
247    }
248
249    #[must_use]
250    pub const fn compatibility(&self) -> CompatibilityPolicy {
251        self.compatibility
252    }
253
254    #[must_use]
255    pub fn cursor_codec(&self) -> Option<&CursorCodec> {
256        self.cursor_codec.as_ref()
257    }
258
259    pub(crate) fn apply_raw_filters<'raw, E: crate::SearchEntity>(
260        &self,
261        raw: &'raw RawParams,
262    ) -> Result<Cow<'raw, RawParams>, ParamError> {
263        if self.filters.is_empty() {
264            return Ok(Cow::Borrowed(raw));
265        }
266        if self.filters.len() > self.limits.max_filter_steps {
267            return Err(ParamError::TooComplex("parameter filter chain"));
268        }
269        let mut current = raw.clone();
270        let started = std::time::Instant::now();
271        for filter in &self.filters {
272            current = filter.filter_with_limits(E::META, &current, &self.limits)?;
273            if current.len() > self.limits.max_filter_items
274                || current.decoded_bytes() > self.limits.max_filter_bytes
275            {
276                return Err(ParamError::TooComplex("filtered parameter budget"));
277            }
278            if started.elapsed().as_micros() > u128::from(self.limits.max_filter_micros) {
279                return Err(ParamError::TooComplex("parameter filter time budget"));
280            }
281        }
282        Ok(Cow::Owned(current))
283    }
284}
285
286fn name_cache() -> Arc<Mutex<BoundedLruMap<TypeId, Result<(), ParamError>>>> {
287    validation_cache()
288}
289
290fn validation_cache() -> Arc<Mutex<BoundedLruMap<TypeId, Result<(), ParamError>>>> {
291    Arc::new(Mutex::new(
292        BoundedLruMap::new(256).expect("fixed nonzero metadata cache capacity"),
293    ))
294}
295
296fn codec_binding_cache()
297-> Arc<Mutex<BoundedLruMap<TypeId, Result<Arc<[CodecBinding]>, ParamError>>>> {
298    Arc::new(Mutex::new(
299        BoundedLruMap::new(256).expect("fixed nonzero metadata cache capacity"),
300    ))
301}