use super::*;
pub(crate) fn evaluate_condition(
condition: &str,
existing: Option<&HashMap<String, AttributeValue>>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Result<(), AwsServiceError> {
let empty = HashMap::new();
let item = existing.unwrap_or(&empty);
if evaluate_filter_expression(condition, item, expr_attr_names, expr_attr_values) {
Ok(())
} else {
Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ConditionalCheckFailedException",
"The conditional request failed",
))
}
}
pub(crate) fn extract_function_arg<'a>(expr: &'a str, func_name: &str) -> Option<&'a str> {
let with_paren = format!("{func_name}(");
let with_space = format!("{func_name} (");
let rest = expr
.strip_prefix(&with_paren)
.or_else(|| expr.strip_prefix(&with_space))?;
let inner = rest.strip_suffix(')')?;
Some(inner.trim())
}
pub(crate) fn evaluate_key_condition(
expr: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
let trimmed = expr.trim();
let parts = split_on_and(trimmed);
if parts.len() > 1 {
return parts.iter().all(|part| {
evaluate_key_condition(part.trim(), item, expr_attr_names, expr_attr_values)
});
}
let stripped = strip_outer_parens(trimmed);
if stripped != trimmed {
return evaluate_key_condition(stripped, item, expr_attr_names, expr_attr_values);
}
evaluate_single_key_condition(trimmed, item, expr_attr_names, expr_attr_values)
}
pub(crate) fn split_on_top_level_keyword<'a>(expr: &'a str, keyword: &str) -> Vec<&'a str> {
let bytes = expr.as_bytes();
let len = bytes.len();
let kw = keyword.as_bytes();
let is_and = keyword.eq_ignore_ascii_case("AND");
let mut parts: Vec<&str> = Vec::new();
let mut start = 0usize;
let mut depth: i32 = 0;
let mut between_skip: u32 = 0;
let mut i = 0usize;
while i < len {
let ch = bytes[i];
if ch == b'(' {
depth += 1;
i += 1;
continue;
}
if ch == b')' {
if depth > 0 {
depth -= 1;
}
i += 1;
continue;
}
if depth == 0 {
if is_and {
if let Some(end) = match_keyword(bytes, i, b"BETWEEN") {
between_skip = between_skip.saturating_add(1);
i = end;
continue;
}
}
if let Some(end) = match_keyword(bytes, i, kw) {
if is_and && between_skip > 0 {
between_skip -= 1;
i = end;
continue;
}
parts.push(&expr[start..i]);
start = end;
i = end;
continue;
}
}
i += 1;
}
parts.push(&expr[start..]);
parts
}
pub(crate) fn match_keyword(bytes: &[u8], i: usize, keyword: &[u8]) -> Option<usize> {
let end = i + keyword.len();
if end > bytes.len() {
return None;
}
for k in 0..keyword.len() {
if !bytes[i + k].eq_ignore_ascii_case(&keyword[k]) {
return None;
}
}
let needs_word_boundary = keyword.iter().all(|b| b.is_ascii_alphanumeric());
if needs_word_boundary {
let left_ok = i == 0 || bytes[i - 1].is_ascii_whitespace();
if !left_ok {
return None;
}
let right_ok = end == bytes.len() || bytes[end].is_ascii_whitespace();
if !right_ok {
return None;
}
}
Some(end)
}
pub(crate) fn split_on_and(expr: &str) -> Vec<&str> {
split_on_top_level_keyword(expr, "AND")
}
pub(crate) fn split_on_or(expr: &str) -> Vec<&str> {
split_on_top_level_keyword(expr, "OR")
}
pub(crate) fn evaluate_single_key_condition(
part: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
let part = part.trim();
if let Some(rest) = part
.strip_prefix("begins_with(")
.or_else(|| part.strip_prefix("begins_with ("))
{
return key_cond_begins_with(rest, item, expr_attr_names, expr_attr_values);
}
let upper = part.to_ascii_uppercase();
let bytes = upper.as_bytes();
let between_pos = (0..bytes.len().saturating_sub(7)).find(|&i| {
bytes[i..].starts_with(b"BETWEEN")
&& i > 0
&& (bytes[i - 1] as char).is_ascii_whitespace()
&& bytes
.get(i + 7)
.map(|c| (*c as char).is_ascii_whitespace())
.unwrap_or(false)
});
if let Some(between_pos) = between_pos {
return key_cond_between(part, between_pos, item, expr_attr_names, expr_attr_values);
}
key_cond_simple_comparison(part, item, expr_attr_names, expr_attr_values)
}
pub(crate) fn key_cond_begins_with(
rest: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
let Some(inner) = rest.strip_suffix(')') else {
return false;
};
let mut split = inner.splitn(2, ',');
let (Some(attr_ref), Some(val_ref)) = (split.next(), split.next()) else {
return false;
};
let attr_name = resolve_attr_name(attr_ref.trim(), expr_attr_names);
let expected = expr_attr_values.get(val_ref.trim());
let actual = item.get(&attr_name);
match (actual, expected) {
(Some(a), Some(e)) => {
let a_str = a.get("S").and_then(|v| v.as_str());
let e_str = e.get("S").and_then(|v| v.as_str());
matches!((a_str, e_str), (Some(a), Some(e)) if a.starts_with(e))
}
_ => false,
}
}
pub(crate) fn key_cond_between(
part: &str,
between_pos: usize,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
let attr_part = part[..between_pos].trim();
let attr_name = resolve_attr_name(attr_part, expr_attr_names);
let range_part = &part[between_pos + 7..];
let Some(and_pos) = range_part.to_ascii_uppercase().find(" AND ") else {
return false;
};
let lo_ref = range_part[..and_pos].trim();
let hi_ref = range_part[and_pos + 5..].trim();
let lo = expr_attr_values.get(lo_ref);
let hi = expr_attr_values.get(hi_ref);
let actual = item.get(&attr_name);
match (actual, lo, hi) {
(Some(a), Some(l), Some(h)) => {
compare_attribute_values(Some(a), Some(l)) != std::cmp::Ordering::Less
&& compare_attribute_values(Some(a), Some(h)) != std::cmp::Ordering::Greater
}
_ => false,
}
}
pub(crate) fn key_cond_simple_comparison(
part: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
for op in &["<=", ">=", "<>", "=", "<", ">"] {
let Some(pos) = part.find(op) else {
continue;
};
let left = part[..pos].trim();
let right = part[pos + op.len()..].trim();
let actual_owned = resolve_path(left, item, expr_attr_names);
let actual = actual_owned.as_ref();
let expected = expr_attr_values.get(right);
return match *op {
"=" => actual == expected,
"<>" => actual != expected,
"<" => compare_attribute_values(actual, expected) == std::cmp::Ordering::Less,
">" => compare_attribute_values(actual, expected) == std::cmp::Ordering::Greater,
"<=" => {
let cmp = compare_attribute_values(actual, expected);
cmp == std::cmp::Ordering::Less || cmp == std::cmp::Ordering::Equal
}
">=" => {
let cmp = compare_attribute_values(actual, expected);
cmp == std::cmp::Ordering::Greater || cmp == std::cmp::Ordering::Equal
}
_ => false,
};
}
false
}
pub(crate) fn attribute_size(val: &Value) -> Option<usize> {
if let Some(s) = val.get("S").and_then(|v| v.as_str()) {
return Some(s.len());
}
if let Some(b) = val.get("B").and_then(|v| v.as_str()) {
let decoded_len = base64::engine::general_purpose::STANDARD
.decode(b)
.map(|v| v.len())
.unwrap_or(b.len());
return Some(decoded_len);
}
if let Some(arr) = val.get("SS").and_then(|v| v.as_array()) {
return Some(arr.len());
}
if let Some(arr) = val.get("NS").and_then(|v| v.as_array()) {
return Some(arr.len());
}
if let Some(arr) = val.get("BS").and_then(|v| v.as_array()) {
return Some(arr.len());
}
if let Some(arr) = val.get("L").and_then(|v| v.as_array()) {
return Some(arr.len());
}
if let Some(obj) = val.get("M").and_then(|v| v.as_object()) {
return Some(obj.len());
}
None
}
pub(crate) fn evaluate_size_comparison(
part: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Option<bool> {
let open = part.find('(')?;
let close = part[open..].find(')')? + open;
let path = part[open + 1..close].trim();
let remainder = part[close + 1..].trim();
let (op, val_ref) = if let Some(rest) = remainder.strip_prefix("<=") {
("<=", rest.trim())
} else if let Some(rest) = remainder.strip_prefix(">=") {
(">=", rest.trim())
} else if let Some(rest) = remainder.strip_prefix("<>") {
("<>", rest.trim())
} else if let Some(rest) = remainder.strip_prefix('<') {
("<", rest.trim())
} else if let Some(rest) = remainder.strip_prefix('>') {
(">", rest.trim())
} else if let Some(rest) = remainder.strip_prefix('=') {
("=", rest.trim())
} else {
return None;
};
let actual_owned = resolve_path(path, item, expr_attr_names)?;
let size = attribute_size(&actual_owned)? as f64;
let expected = extract_number(&expr_attr_values.get(val_ref).cloned())?;
Some(match op {
"=" => (size - expected).abs() < f64::EPSILON,
"<>" => (size - expected).abs() >= f64::EPSILON,
"<" => size < expected,
">" => size > expected,
"<=" => size <= expected,
">=" => size >= expected,
_ => false,
})
}
pub(crate) fn evaluate_filter_expression(
expr: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
let trimmed = expr.trim();
let or_parts = split_on_or(trimmed);
if or_parts.len() > 1 {
return or_parts.iter().any(|part| {
evaluate_filter_expression(part.trim(), item, expr_attr_names, expr_attr_values)
});
}
let and_parts = split_on_and(trimmed);
if and_parts.len() > 1 {
return and_parts.iter().all(|part| {
evaluate_filter_expression(part.trim(), item, expr_attr_names, expr_attr_values)
});
}
let stripped = strip_outer_parens(trimmed);
if stripped != trimmed {
return evaluate_filter_expression(stripped, item, expr_attr_names, expr_attr_values);
}
if trimmed.len() > 4 && trimmed[..4].eq_ignore_ascii_case("NOT ") {
return !evaluate_filter_expression(&trimmed[4..], item, expr_attr_names, expr_attr_values);
}
evaluate_single_filter_condition(trimmed, item, expr_attr_names, expr_attr_values)
}