use bson::{Bson, Document, Regex};
use regex::{Regex as CompiledRegex, RegexBuilder};
use crate::errors::RegexError;
use crate::{Error, Result};
pub(super) fn field_path_value(document: &Document, path: &str) -> Option<Bson> {
let mut segments = path.split('.');
let first = segments.next()?;
let mut value = document.get(first)?;
for segment in segments {
value = value.as_document()?.get(segment)?;
}
Some(value.clone())
}
pub(super) fn matches_in(field_value: &Bson, candidates: &[Bson]) -> Result<bool> {
let compiled_regexes = compile_candidate_regexes(candidates)?;
Ok(candidates
.iter()
.zip(compiled_regexes.iter())
.any(|(candidate, regex)| candidate_matches(field_value, candidate, regex.as_ref())))
}
pub(super) fn matches_all(field_value: &Bson, candidates: &[Bson]) -> Result<bool> {
let compiled_regexes = compile_candidate_regexes(candidates)?;
Ok(!candidates.is_empty()
&& candidates
.iter()
.zip(compiled_regexes.iter())
.all(|(candidate, regex)| candidate_matches(field_value, candidate, regex.as_ref())))
}
pub(super) fn validate_membership_candidates(candidates: &[Bson]) -> Result<()> {
compile_candidate_regexes(candidates).map(|_| ())
}
pub(super) fn matches_regex(field_value: &Bson, expression: &Regex) -> Result<bool> {
let regex = compile_regex(expression)?;
Ok(regex.is_match(&field_value.to_string()))
}
fn matches_compiled_regex(field_value: &Bson, regex: &CompiledRegex) -> bool {
match field_value {
Bson::String(value) | Bson::Symbol(value) => regex.is_match(value),
Bson::Array(values) => values.iter().any(|value| match value {
Bson::String(value) | Bson::Symbol(value) => regex.is_match(value),
_ => false,
}),
_ => false,
}
}
fn candidate_matches(
field_value: &Bson,
candidate: &Bson,
compiled_regex: Option<&CompiledRegex>,
) -> bool {
query_values_equal(field_value, candidate)
|| matches!(
field_value,
Bson::Array(values)
if values
.iter()
.any(|value| query_values_equal(value, candidate))
)
|| compiled_regex.is_some_and(|regex| matches_compiled_regex(field_value, regex))
}
fn compile_candidate_regexes(candidates: &[Bson]) -> Result<Vec<Option<CompiledRegex>>> {
candidates
.iter()
.map(|candidate| match candidate {
Bson::RegularExpression(expression) => compile_regex(expression).map(Some),
_ => Ok(None),
})
.collect()
}
fn compile_regex(expression: &Regex) -> Result<CompiledRegex> {
let mut builder = RegexBuilder::new(expression.pattern.as_str());
for option in expression.options.chars() {
match option {
'i' => builder.case_insensitive(true),
'm' => builder.multi_line(true),
's' => builder.dot_matches_new_line(true),
'u' => builder.unicode(true),
'U' => builder.swap_greed(true),
'x' => builder.ignore_whitespace(true),
_ => {
return Err(Error::from(RegexError {
error: format!("unknown regex option: {option}"),
expression: expression.pattern.clone(),
options: expression.options.clone(),
}));
}
};
}
builder.build().map_err(|error| {
Error::from(RegexError {
error: format!("regex build error: {error}"),
expression: expression.pattern.clone(),
options: expression.options.clone(),
})
})
}
fn query_values_equal(left: &Bson, right: &Bson) -> bool {
match (left, right) {
(
Bson::Int32(_) | Bson::Int64(_) | Bson::Double(_),
Bson::Int32(_) | Bson::Int64(_) | Bson::Double(_),
) => numeric_values_equal(left, right),
(Bson::Array(left), Bson::Array(right)) => {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|(left, right)| query_values_equal(left, right))
}
(Bson::Document(left), Bson::Document(right)) => documents_equal(left, right),
(Bson::JavaScriptCodeWithScope(left), Bson::JavaScriptCodeWithScope(right)) => {
left.code == right.code && documents_equal(&left.scope, &right.scope)
}
(Bson::Binary(left), Bson::Binary(right)) => {
left.subtype == right.subtype && left.bytes == right.bytes
}
(Bson::String(left), Bson::Symbol(right)) | (Bson::Symbol(left), Bson::String(right)) => {
left == right
}
_ => left == right,
}
}
fn numeric_values_equal(left: &Bson, right: &Bson) -> bool {
match (left, right) {
(Bson::Double(left), Bson::Double(right)) => {
left == right || (left.is_nan() && right.is_nan())
}
(Bson::Double(double), Bson::Int32(integer))
| (Bson::Int32(integer), Bson::Double(double)) => {
integer_equals_double(i64::from(*integer), *double)
}
(Bson::Double(double), Bson::Int64(integer))
| (Bson::Int64(integer), Bson::Double(double)) => integer_equals_double(*integer, *double),
(Bson::Int32(left), Bson::Int32(right)) => left == right,
(Bson::Int32(left), Bson::Int64(right)) => i64::from(*left) == *right,
(Bson::Int64(left), Bson::Int32(right)) => *left == i64::from(*right),
(Bson::Int64(left), Bson::Int64(right)) => left == right,
_ => false,
}
}
fn integer_equals_double(integer: i64, double: f64) -> bool {
const I64_LOWER_BOUND: f64 = -9_223_372_036_854_775_808.0;
const I64_UPPER_BOUND: f64 = 9_223_372_036_854_775_808.0;
double.is_finite()
&& double.trunc() == double
&& (I64_LOWER_BOUND..I64_UPPER_BOUND).contains(&double)
&& double as i64 == integer
}
fn documents_equal(left: &Document, right: &Document) -> bool {
left.len() == right.len()
&& left.iter().zip(right.iter()).all(
|((left_key, left_value), (right_key, right_value))| {
left_key == right_key && query_values_equal(left_value, right_value)
},
)
}
#[cfg(test)]
mod tests {
use bson::spec::BinarySubtype;
use bson::{doc, Binary, Bson, JavaScriptCodeWithScope, Regex};
use super::{field_path_value, matches_all, matches_in, query_values_equal};
#[test]
fn field_path_lookup_preserves_terminal_documents() {
let document = doc! {
"profile": {
"name": "Ada",
},
};
assert_eq!(
field_path_value(&document, "profile"),
document.get("profile").cloned(),
);
assert_eq!(
field_path_value(&document, "profile.name"),
Some(Bson::String("Ada".into())),
);
}
#[test]
fn query_equality_is_recursive_and_ordered() {
assert!(query_values_equal(
&Bson::Array(vec![Bson::Int32(1)]),
&Bson::Array(vec![Bson::Int64(1)]),
));
assert!(!query_values_equal(
&Bson::Document(doc! { "a": 1, "b": 2 }),
&Bson::Document(doc! { "b": 2, "a": 1 }),
));
}
#[test]
fn numeric_equality_is_exact_across_bson_types() {
assert!(query_values_equal(&Bson::Double(-0.0), &Bson::Int32(0)));
assert!(query_values_equal(&Bson::Double(0.0), &Bson::Double(-0.0)));
assert!(query_values_equal(
&Bson::Double(f64::NAN),
&Bson::Double(f64::NAN),
));
assert!(!query_values_equal(
&Bson::Int64(9_007_199_254_740_993),
&Bson::Double(9_007_199_254_740_992.0),
));
assert!(!query_values_equal(
&Bson::Int64(i64::MAX),
&Bson::Double(9_223_372_036_854_775_808.0),
));
assert!(query_values_equal(
&Bson::Int64(i64::MIN),
&Bson::Double(-9_223_372_036_854_775_808.0),
));
}
#[test]
fn binary_equality_includes_subtype() {
let bytes = vec![1, 2, 3];
assert!(!query_values_equal(
&Bson::Binary(Binary {
subtype: BinarySubtype::Generic,
bytes: bytes.clone(),
}),
&Bson::Binary(Binary {
subtype: BinarySubtype::Uuid,
bytes,
}),
));
}
#[test]
fn code_with_scope_uses_recursive_ordered_document_equality() {
let stored = Bson::JavaScriptCodeWithScope(JavaScriptCodeWithScope {
code: "return a".into(),
scope: doc! { "a": Bson::Int32(1), "b": "x" },
});
let equivalent = Bson::JavaScriptCodeWithScope(JavaScriptCodeWithScope {
code: "return a".into(),
scope: doc! { "a": Bson::Int64(1), "b": Bson::Symbol("x".into()) },
});
let reversed = Bson::JavaScriptCodeWithScope(JavaScriptCodeWithScope {
code: "return a".into(),
scope: doc! { "b": Bson::Symbol("x".into()), "a": Bson::Int64(1) },
});
assert!(query_values_equal(&stored, &equivalent));
assert!(!query_values_equal(&stored, &reversed));
}
#[test]
fn regex_candidates_only_match_strings() {
let candidate = Bson::RegularExpression(Regex {
pattern: "^foo".into(),
options: "i".into(),
});
assert!(matches_in(&Bson::String("Food".into()), &[candidate.clone()]).unwrap());
assert!(matches_in(
&Bson::Array(vec![Bson::String("Food".into())]),
&[candidate.clone()],
)
.unwrap());
assert!(!matches_in(&Bson::Int32(42), &[candidate]).unwrap());
}
#[test]
fn in_validates_all_regex_candidates_before_matching() {
let candidates = [
Bson::Int32(1),
Bson::RegularExpression(Regex {
pattern: "[".into(),
options: "".into(),
}),
];
assert!(matches_in(&Bson::Int32(1), &candidates).is_err());
}
#[test]
fn array_candidates_match_whole_or_nested_arrays() {
let candidate = Bson::Array(vec![Bson::Int32(1), Bson::Int32(2)]);
assert!(matches_in(
&Bson::Array(vec![Bson::Int64(1), Bson::Double(2.0)]),
&[candidate.clone()],
)
.unwrap());
assert!(matches_in(
&Bson::Array(vec![candidate.clone(), Bson::String("x".into())]),
&[candidate],
)
.unwrap());
}
#[test]
fn all_candidates_must_match_and_empty_candidates_never_match() {
let field = Bson::Array(vec![Bson::Int32(1), Bson::Int64(2)]);
assert!(
matches_all(&field, &[Bson::Double(1.0), Bson::Int32(2), Bson::Int64(2)],).unwrap()
);
assert!(!matches_all(&field, &[Bson::Int32(1), Bson::Int32(3)]).unwrap());
assert!(!matches_all(&field, &[]).unwrap());
}
#[test]
fn all_array_candidates_match_whole_or_nested_arrays() {
let candidate = Bson::Array(vec![Bson::Int32(1), Bson::Int32(2)]);
assert!(matches_all(
&Bson::Array(vec![Bson::Int64(1), Bson::Double(2.0)]),
std::slice::from_ref(&candidate),
)
.unwrap());
assert!(matches_all(
&Bson::Array(vec![candidate.clone(), Bson::String("x".into())]),
&[candidate],
)
.unwrap());
}
#[test]
fn all_validates_all_regex_candidates_before_matching() {
let candidates = [
Bson::Int32(3),
Bson::RegularExpression(Regex {
pattern: "[".into(),
options: "".into(),
}),
];
assert!(matches_all(&Bson::Int32(1), &candidates).is_err());
}
}