#![allow(clippy::too_many_arguments)]
use std::collections::{BTreeMap, HashMap};
use base64::Engine;
use http::StatusCode;
use serde_json::{json, Value};
use fakecloud_core::service::AwsServiceError;
use crate::state::*;
pub(super) fn parse_on_demand_throughput(val: &Value) -> Option<crate::state::OnDemandThroughput> {
if !val.is_object() {
return None;
}
Some(crate::state::OnDemandThroughput {
max_read_request_units: val["MaxReadRequestUnits"].as_i64().unwrap_or(-1),
max_write_request_units: val["MaxWriteRequestUnits"].as_i64().unwrap_or(-1),
})
}
pub(super) fn parse_projection(val: &Value) -> Projection {
Projection {
projection_type: val["ProjectionType"].as_str().unwrap_or("ALL").to_string(),
non_key_attributes: val["NonKeyAttributes"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default(),
}
}
#[derive(Debug)]
pub(crate) enum PathSegment {
Key(String),
Index(usize),
}
pub(crate) fn strip_outer_parens(expr: &str) -> &str {
let trimmed = expr.trim();
if !trimmed.starts_with('(') || !trimmed.ends_with(')') {
return trimmed;
}
let inner = &trimmed[1..trimmed.len() - 1];
let mut depth = 0;
for ch in inner.bytes() {
match ch {
b'(' => depth += 1,
b')' => {
if depth == 0 {
return trimmed; }
depth -= 1;
}
_ => {}
}
}
if depth == 0 {
inner
} else {
trimmed
}
}
pub(crate) fn evaluate_single_filter_condition(
part: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> bool {
if let Some(inner) = extract_function_arg(part, "attribute_exists") {
return resolve_path(inner, item, expr_attr_names).is_some();
}
if let Some(inner) = extract_function_arg(part, "attribute_not_exists") {
return resolve_path(inner, item, expr_attr_names).is_none();
}
if let Some(rest) = part
.strip_prefix("begins_with(")
.or_else(|| part.strip_prefix("begins_with ("))
{
return eval_begins_with(rest, item, expr_attr_names, expr_attr_values);
}
if let Some(rest) = part
.strip_prefix("contains(")
.or_else(|| part.strip_prefix("contains ("))
{
return eval_contains(rest, item, expr_attr_names, expr_attr_values);
}
if part.starts_with("size(") || part.starts_with("size (") {
if let Some(result) =
evaluate_size_comparison(part, item, expr_attr_names, expr_attr_values)
{
return result;
}
}
if let Some(rest) = part
.strip_prefix("attribute_type(")
.or_else(|| part.strip_prefix("attribute_type ("))
{
return eval_attribute_type(rest, item, expr_attr_names, expr_attr_values);
}
if let Some((attr_ref, value_refs)) = parse_in_expression(part) {
let attr_name = resolve_attr_name(attr_ref, expr_attr_names);
let actual = item.get(&attr_name);
return evaluate_in_match(actual, &value_refs, expr_attr_values);
}
evaluate_single_key_condition(part, item, expr_attr_names, expr_attr_values)
}
pub(crate) fn eval_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 actual = resolve_path(attr_ref.trim(), item, expr_attr_names);
let expected = expr_attr_values.get(val_ref.trim());
match (actual.as_ref(), 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 eval_contains(
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 actual = resolve_path(attr_ref.trim(), item, expr_attr_names);
let expected = expr_attr_values.get(val_ref.trim());
let (Some(a), Some(e)) = (actual.as_ref(), expected) else {
return false;
};
if let (Some(a_s), Some(e_s)) = (
a.get("S").and_then(|v| v.as_str()),
e.get("S").and_then(|v| v.as_str()),
) {
return a_s.contains(e_s);
}
if let Some(set) = a.get("SS").and_then(|v| v.as_array()) {
if let Some(val) = e.get("S") {
return set.contains(val);
}
}
if let Some(set) = a.get("NS").and_then(|v| v.as_array()) {
if let Some(val) = e.get("N") {
return set.contains(val);
}
}
if let Some(set) = a.get("BS").and_then(|v| v.as_array()) {
if let Some(val) = e.get("B") {
return set.contains(val);
}
}
if let Some(list) = a.get("L").and_then(|v| v.as_array()) {
return list.contains(e);
}
false
}
pub(crate) fn eval_attribute_type(
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 actual = resolve_path(attr_ref.trim(), item, expr_attr_names);
let expected_type = expr_attr_values
.get(val_ref.trim())
.and_then(|v| v.get("S"))
.and_then(|v| v.as_str());
let (Some(val), Some(t)) = (actual.as_ref(), expected_type) else {
return false;
};
match t {
"S" => val.get("S").is_some(),
"N" => val.get("N").is_some(),
"B" => val.get("B").is_some(),
"BOOL" => val.get("BOOL").is_some(),
"NULL" => val.get("NULL").is_some(),
"SS" => val.get("SS").is_some(),
"NS" => val.get("NS").is_some(),
"BS" => val.get("BS").is_some(),
"L" => val.get("L").is_some(),
"M" => val.get("M").is_some(),
_ => false,
}
}
pub(crate) fn parse_in_expression(expr: &str) -> Option<(&str, Vec<&str>)> {
let upper = expr.to_ascii_uppercase();
let in_pos = upper.find(" IN ")?;
let attr_ref = expr[..in_pos].trim();
if attr_ref.is_empty() {
return None;
}
let rest = expr[in_pos + 4..].trim_start();
let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
let values: Vec<&str> = inner
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if values.is_empty() {
return None;
}
Some((attr_ref, values))
}
pub(crate) fn evaluate_in_match(
actual: Option<&AttributeValue>,
value_refs: &[&str],
expr_attr_values: &HashMap<String, Value>,
) -> bool {
value_refs.iter().any(|v_ref| {
let expected = expr_attr_values.get(*v_ref);
matches!((actual, expected), (Some(a), Some(e)) if a == e)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum UpdateAction {
Set,
Remove,
Add,
Delete,
}
impl UpdateAction {
const KEYWORDS: &'static [(&'static str, UpdateAction)] = &[
("SET", UpdateAction::Set),
("REMOVE", UpdateAction::Remove),
("ADD", UpdateAction::Add),
("DELETE", UpdateAction::Delete),
];
fn keyword(self) -> &'static str {
match self {
UpdateAction::Set => "SET",
UpdateAction::Remove => "REMOVE",
UpdateAction::Add => "ADD",
UpdateAction::Delete => "DELETE",
}
}
}
pub(crate) fn apply_update_expression(
item: &mut HashMap<String, AttributeValue>,
expr: &str,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Result<(), AwsServiceError> {
let clauses = parse_update_clauses(expr);
if clauses.is_empty() && !expr.trim().is_empty() {
return Err(AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationException",
"Invalid UpdateExpression: Syntax error; token: \"<expression>\"",
));
}
for (action, assignments) in &clauses {
match action {
UpdateAction::Set => {
for assignment in assignments {
apply_set_assignment(item, assignment, expr_attr_names, expr_attr_values)?;
}
}
UpdateAction::Remove => {
for attr_ref in assignments {
remove_path(item, attr_ref.trim(), expr_attr_names);
}
}
UpdateAction::Add => {
for assignment in assignments {
apply_add_assignment(item, assignment, expr_attr_names, expr_attr_values)?;
}
}
UpdateAction::Delete => {
for assignment in assignments {
apply_delete_assignment(item, assignment, expr_attr_names, expr_attr_values)?;
}
}
}
}
Ok(())
}
pub(crate) fn parse_update_clauses(expr: &str) -> Vec<(UpdateAction, Vec<String>)> {
let mut clauses: Vec<(UpdateAction, Vec<String>)> = Vec::new();
let upper = expr.to_ascii_uppercase();
let mut positions: Vec<(usize, UpdateAction)> = Vec::new();
let is_boundary = |b: u8| b == b' ' || b == b'\t' || b == b'\n' || b == b'\r';
for &(kw, action) in UpdateAction::KEYWORDS {
let mut search_from = 0;
while let Some(pos) = upper[search_from..].find(kw) {
let abs_pos = search_from + pos;
let before_ok = abs_pos == 0 || is_boundary(expr.as_bytes()[abs_pos - 1]);
let after_pos = abs_pos + kw.len();
let after_ok = after_pos >= expr.len() || is_boundary(expr.as_bytes()[after_pos]);
if before_ok && after_ok {
positions.push((abs_pos, action));
}
search_from = abs_pos + kw.len();
}
}
positions.sort_by_key(|(pos, _)| *pos);
for (i, &(pos, action)) in positions.iter().enumerate() {
let start = pos + action.keyword().len();
let end = if i + 1 < positions.len() {
positions[i + 1].0
} else {
expr.len()
};
let content = expr[start..end].trim();
let assignments: Vec<String> = split_on_top_level_keyword(content, ",")
.into_iter()
.map(|s| s.trim().to_string())
.collect();
clauses.push((action, assignments));
}
clauses
}
pub(crate) fn apply_set_assignment(
item: &mut HashMap<String, AttributeValue>,
assignment: &str,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Result<(), AwsServiceError> {
let Some((left, right)) = assignment.split_once('=') else {
return Ok(());
};
let left_trimmed = left.trim();
let right = right.trim();
let new_value = evaluate_set_rhs(right, item, expr_attr_names, expr_attr_values)?;
if is_dotted_path(left_trimmed) {
let Some(v) = new_value else {
return Ok(());
};
return assign_nested_path(item, left_trimmed, expr_attr_names, v);
}
let (attr_ref, list_index) = match parse_list_index_suffix(left_trimmed) {
Some((name, idx)) => (name, Some(idx)),
None => (left_trimmed, None),
};
let attr = resolve_attr_name(attr_ref, expr_attr_names);
let Some(v) = new_value else {
return Ok(());
};
match list_index {
Some(idx) => assign_list_index(item, &attr, idx, v),
None => {
item.insert(attr, v);
Ok(())
}
}
}
pub(crate) fn evaluate_set_rhs(
right: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Result<Option<Value>, AwsServiceError> {
if let Some((arith_left, arith_right, is_add)) = parse_arithmetic(right) {
return evaluate_arithmetic_rhs(
arith_left,
arith_right,
is_add,
item,
expr_attr_names,
expr_attr_values,
);
}
if let Some(rest) = right
.strip_prefix("if_not_exists(")
.or_else(|| right.strip_prefix("if_not_exists ("))
{
return Ok(evaluate_if_not_exists_rhs(
rest,
item,
expr_attr_names,
expr_attr_values,
));
}
if let Some(rest) = right
.strip_prefix("list_append(")
.or_else(|| right.strip_prefix("list_append ("))
{
return Ok(evaluate_list_append_rhs(
rest,
item,
expr_attr_names,
expr_attr_values,
));
}
Ok(resolve_ref_or_path(
right,
item,
expr_attr_names,
expr_attr_values,
))
}
pub(crate) fn evaluate_arithmetic_operand(
operand: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Option<Value> {
let operand = operand.trim();
if let Some(rest) = operand
.strip_prefix("if_not_exists(")
.or_else(|| operand.strip_prefix("if_not_exists ("))
{
let inner = rest.strip_suffix(')')?;
let mut split = inner.splitn(2, ',');
let (check, default) = (split.next()?, split.next()?);
return resolve_ref_or_path(check.trim(), item, expr_attr_names, expr_attr_values).or_else(
|| resolve_ref_or_path(default.trim(), item, expr_attr_names, expr_attr_values),
);
}
resolve_ref_or_path(operand, item, expr_attr_names, expr_attr_values)
}
pub(crate) fn evaluate_if_not_exists_rhs(
rest: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Option<Value> {
let inner = rest.strip_suffix(')')?;
let mut split = inner.splitn(2, ',');
let (check, default) = (split.next()?, split.next()?);
if resolve_ref_or_path(check.trim(), item, expr_attr_names, expr_attr_values).is_some() {
return None;
}
resolve_ref_or_path(default.trim(), item, expr_attr_names, expr_attr_values)
}
pub(crate) fn evaluate_list_append_rhs(
rest: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Option<Value> {
let inner = rest.strip_suffix(')')?;
let mut split = inner.splitn(2, ',');
let (a_ref, b_ref) = (split.next()?, split.next()?);
let a_val = resolve_ref_or_path(a_ref.trim(), item, expr_attr_names, expr_attr_values);
let b_val = resolve_ref_or_path(b_ref.trim(), item, expr_attr_names, expr_attr_values);
let mut merged = Vec::new();
for v in [&a_val, &b_val].iter().copied().flatten() {
if let Value::Object(obj) = v {
if let Some(Value::Array(arr)) = obj.get("L") {
merged.extend(arr.clone());
}
}
}
Some(json!({ "L": merged }))
}
pub(crate) fn evaluate_arithmetic_rhs(
arith_left: &str,
arith_right: &str,
is_add: bool,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Result<Option<Value>, AwsServiceError> {
let left_val = evaluate_arithmetic_operand(arith_left, item, expr_attr_names, expr_attr_values);
let right_val =
evaluate_arithmetic_operand(arith_right, item, expr_attr_names, expr_attr_values);
let bad_operand = || {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationException",
"An operand in the update expression has an incorrect data type",
)
};
let left_num = left_val
.as_ref()
.and_then(|v| v.get("N"))
.and_then(|n| n.as_str())
.ok_or_else(bad_operand)?;
let right_num = right_val
.as_ref()
.and_then(|v| v.get("N"))
.and_then(|n| n.as_str())
.ok_or_else(bad_operand)?;
let num_str = decimal_add_sub(left_num, right_num, is_add).ok_or_else(bad_operand)?;
Ok(Some(json!({ "N": num_str })))
}
pub(crate) fn parse_list_index_suffix(path: &str) -> Option<(&str, usize)> {
let path = path.trim();
if !path.ends_with(']') {
return None;
}
let open = path.rfind('[')?;
let idx_str = &path[open + 1..path.len() - 1];
let idx: usize = idx_str.parse().ok()?;
let name = &path[..open];
if name.is_empty() || name.contains('[') || name.contains(']') || name.contains('.') {
return None;
}
Some((name, idx))
}
pub(crate) fn assign_list_index(
item: &mut HashMap<String, AttributeValue>,
attr: &str,
idx: usize,
value: Value,
) -> Result<(), AwsServiceError> {
let Some(existing) = item.get_mut(attr) else {
return Err(invalid_document_path());
};
let Some(list) = existing.get_mut("L").and_then(|l| l.as_array_mut()) else {
return Err(invalid_document_path());
};
if idx < list.len() {
list[idx] = value;
} else if idx == list.len() {
list.push(value);
} else {
return Err(invalid_document_path());
}
Ok(())
}
pub(crate) fn invalid_document_path() -> AwsServiceError {
AwsServiceError::aws_error(
StatusCode::BAD_REQUEST,
"ValidationException",
"The document path provided in the update expression is invalid for update",
)
}
pub(crate) fn resolve_ref_or_path(
reference: &str,
item: &HashMap<String, AttributeValue>,
expr_attr_names: &HashMap<String, String>,
expr_attr_values: &HashMap<String, Value>,
) -> Option<Value> {
let reference = reference.trim();
if reference.starts_with(':') {
return expr_attr_values.get(reference).cloned();
}
resolve_path(reference, item, expr_attr_names)
}
pub(crate) fn is_dotted_path(path: &str) -> bool {
path.contains('.') && !path.contains('[')
}
pub(crate) struct TableDescriptionInput<'a> {
pub arn: &'a str,
pub table_id: &'a str,
pub key_schema: &'a [KeySchemaElement],
pub attribute_definitions: &'a [AttributeDefinition],
pub provisioned_throughput: &'a ProvisionedThroughput,
pub gsi: &'a [GlobalSecondaryIndex],
pub lsi: &'a [LocalSecondaryIndex],
pub billing_mode: &'a str,
pub created_at: chrono::DateTime<chrono::Utc>,
pub item_count: i64,
pub size_bytes: i64,
pub status: &'a str,
pub deletion_protection_enabled: bool,
pub on_demand_throughput: Option<&'a crate::state::OnDemandThroughput>,
}
pub(crate) struct PartiqlOutcome {
pub response: Value,
pub table_name: Option<String>,
pub event_name: Option<String>, pub keys: Option<HashMap<String, AttributeValue>>,
pub old_image: Option<HashMap<String, AttributeValue>>,
pub new_image: Option<HashMap<String, AttributeValue>>,
}
#[derive(Debug, Clone)]
pub(crate) enum PartiqlExpr {
Cond(PartiqlCond),
And(Box<PartiqlExpr>, Box<PartiqlExpr>),
Or(Box<PartiqlExpr>, Box<PartiqlExpr>),
Not(Box<PartiqlExpr>),
}
#[derive(Debug, Clone)]
enum WhereTok<'a> {
LParen,
RParen,
And,
Or,
Not,
Atom(&'a str),
}
#[derive(Debug, Clone)]
pub(crate) enum PartiqlCond {
Eq(String, Value),
Ne(String, Value),
Lt(String, Value),
Le(String, Value),
Gt(String, Value),
Ge(String, Value),
Between(String, Value, Value),
In(String, Vec<Value>),
Like(String, String),
BeginsWith(String, Value),
Contains(String, Value),
AttributeExists(String),
AttributeNotExists(String),
}
pub(crate) fn count_params_in_str(s: &str) -> usize {
s.chars().filter(|c| *c == '?').count()
}
mod conditions;
mod keys;
mod metrics;
mod partiql;
mod paths;
mod request;
pub(crate) mod schemas;
mod table_descriptions;
mod table_lookup;
mod updates;
pub(crate) use conditions::*;
pub(crate) use keys::*;
pub(crate) use metrics::*;
pub(crate) use partiql::*;
pub(crate) use paths::*;
pub(crate) use request::*;
pub(crate) use schemas::*;
pub(crate) use table_descriptions::*;
pub(crate) use table_lookup::*;
pub(crate) use updates::*;
#[cfg(test)]
mod set_rhs_tests {
use super::*;
use serde_json::json;
fn names() -> HashMap<String, String> {
HashMap::from([("#c".to_string(), "count".to_string())])
}
fn values() -> HashMap<String, Value> {
HashMap::from([
(":zero".to_string(), json!({"N": "0"})),
(":inc".to_string(), json!({"N": "1"})),
])
}
#[test]
fn atomic_counter_from_absent_both_orderings() {
let item: HashMap<String, AttributeValue> = HashMap::new();
let r1 = evaluate_set_rhs(
"if_not_exists(#c, :zero) + :inc",
&item,
&names(),
&values(),
)
.unwrap();
assert_eq!(r1, Some(json!({"N": "1"})));
let r2 = evaluate_set_rhs(
":inc + if_not_exists(#c, :zero)",
&item,
&names(),
&values(),
)
.unwrap();
assert_eq!(r2, Some(json!({"N": "1"})));
}
#[test]
fn atomic_counter_from_existing_both_orderings() {
let item: HashMap<String, AttributeValue> =
HashMap::from([("count".to_string(), json!({"N": "41"}))]);
let r1 = evaluate_set_rhs(
"if_not_exists(#c, :zero) + :inc",
&item,
&names(),
&values(),
)
.unwrap();
assert_eq!(r1, Some(json!({"N": "42"})));
let r2 = evaluate_set_rhs(
":inc + if_not_exists(#c, :zero)",
&item,
&names(),
&values(),
)
.unwrap();
assert_eq!(r2, Some(json!({"N": "42"})));
}
#[test]
fn bare_if_not_exists_still_works() {
let item: HashMap<String, AttributeValue> = HashMap::new();
let r = evaluate_set_rhs("if_not_exists(#c, :zero)", &item, &names(), &values()).unwrap();
assert_eq!(r, Some(json!({"N": "0"})));
}
}