use std::collections::HashMap;
use crate::errors::{DynoxideError, Result};
use crate::types::AttributeValue;
use crate::validation::{NESTING_LIMIT_MESSAGE, envelope_message};
const LIMIT: usize = 32;
fn nests_deeper_than(value: &AttributeValue, limit: usize) -> bool {
match value {
AttributeValue::M(map) => {
limit == 0 || map.values().any(|v| nests_deeper_than(v, limit - 1))
}
AttributeValue::L(list) => {
limit == 0 || list.iter().any(|v| nests_deeper_than(v, limit - 1))
}
_ => false,
}
}
fn json_nests_deeper_than(value: &serde_json::Value, limit: usize) -> bool {
let Some(object) = value.as_object() else {
return false;
};
if let Some(map) = object.get("M").and_then(|m| m.as_object()) {
return limit == 0 || map.values().any(|v| json_nests_deeper_than(v, limit - 1));
}
if let Some(list) = object.get("L").and_then(|l| l.as_array()) {
return limit == 0 || list.iter().any(|v| json_nests_deeper_than(v, limit - 1));
}
false
}
fn too_deep(value: &AttributeValue) -> bool {
nests_deeper_than(value, LIMIT - 1)
}
fn bare() -> DynoxideError {
DynoxideError::ValidationException(NESTING_LIMIT_MESSAGE.to_string())
}
fn enveloped() -> DynoxideError {
DynoxideError::ValidationException(envelope_message(NESTING_LIMIT_MESSAGE))
}
pub(crate) fn check_key(key: &HashMap<String, AttributeValue>) -> Result<()> {
if key.values().any(too_deep) {
return Err(bare());
}
Ok(())
}
pub(crate) fn check_key_enveloped(key: &HashMap<String, AttributeValue>) -> Result<()> {
if key.values().any(too_deep) {
return Err(enveloped());
}
Ok(())
}
pub(crate) fn check_scan_start_key(key: Option<&HashMap<String, AttributeValue>>) -> Result<()> {
if key.is_some_and(|k| k.values().any(too_deep)) {
return Err(DynoxideError::ValidationException(format!(
"The provided starting key is invalid: {NESTING_LIMIT_MESSAGE}"
)));
}
Ok(())
}
pub(crate) fn check_query_start_key(key: Option<&HashMap<String, AttributeValue>>) -> Result<()> {
if key.is_some_and(|k| k.values().any(too_deep)) {
return Err(bare());
}
Ok(())
}
pub(crate) fn check_attribute_updates<'a>(
values: impl IntoIterator<Item = Option<&'a AttributeValue>>,
) -> Result<()> {
if values.into_iter().flatten().any(too_deep) {
return Err(enveloped());
}
Ok(())
}
fn conditions_nest_too_deep(conditions: Option<&serde_json::Value>, limit: usize) -> bool {
let Some(conditions) = conditions.and_then(|c| c.as_object()) else {
return false;
};
conditions.values().filter_map(|c| c.as_object()).any(|c| {
c.get("Value")
.is_some_and(|v| json_nests_deeper_than(v, limit))
|| c.get("AttributeValueList")
.and_then(|l| l.as_array())
.is_some_and(|l| l.iter().any(|v| json_nests_deeper_than(v, limit)))
})
}
pub(crate) fn check_expected(expected: Option<&serde_json::Value>) -> Result<()> {
if conditions_nest_too_deep(expected, LIMIT - 1) {
return Err(enveloped());
}
Ok(())
}
pub(crate) fn check_scan_filter(filter: Option<&serde_json::Value>) -> Result<()> {
if conditions_nest_too_deep(filter, LIMIT) {
return Err(bare());
}
Ok(())
}
pub(crate) fn check_query_conditions(conditions: Option<&serde_json::Value>) -> Result<()> {
if conditions_nest_too_deep(conditions, LIMIT - 1) {
return Err(bare());
}
Ok(())
}
pub(crate) fn parameters_nest_too_deep(parameters: &[AttributeValue]) -> bool {
parameters.iter().any(|p| nests_deeper_than(p, LIMIT))
}
pub(crate) fn deep_parameter_error() -> DynoxideError {
bare()
}
#[cfg(test)]
mod tests {
use super::*;
fn deep_map(depth: usize) -> AttributeValue {
let mut v = AttributeValue::S("leaf".into());
for _ in 0..depth {
v = AttributeValue::M(HashMap::from([("n".to_string(), v)]));
}
v
}
fn deep_json(depth: usize) -> serde_json::Value {
serde_json::to_value(deep_map(depth)).unwrap()
}
#[test]
fn the_limit_is_32_levels_and_one_more_for_a_parameter() {
assert!(!too_deep(&deep_map(31)));
assert!(too_deep(&deep_map(32)));
assert!(!parameters_nest_too_deep(&[deep_map(32)]));
assert!(parameters_nest_too_deep(&[deep_map(33)]));
assert!(!json_nests_deeper_than(&deep_json(31), LIMIT - 1));
assert!(json_nests_deeper_than(&deep_json(32), LIMIT - 1));
}
#[test]
fn a_far_deeper_value_built_in_process_is_refused_without_walking_all_of_it() {
let mut v = AttributeValue::S("leaf".into());
for _ in 0..100_000 {
v = AttributeValue::L(vec![v]);
}
assert!(too_deep(&v));
let mut stack = vec![v];
while let Some(value) = stack.pop() {
if let AttributeValue::L(list) = value {
stack.extend(list);
}
}
}
#[test]
fn legacy_conditions_are_read_through_value_and_the_value_list() {
let expected = serde_json::json!({
"a": {"Value": deep_json(32)},
});
assert!(conditions_nest_too_deep(Some(&expected), LIMIT - 1));
let filter = serde_json::json!({
"a": {"ComparisonOperator": "EQ", "AttributeValueList": [{"S": "x"}, deep_json(32)]},
});
assert!(conditions_nest_too_deep(Some(&filter), LIMIT - 1));
let shallow = serde_json::json!({
"a": {"Value": deep_json(31), "AttributeValueList": [deep_json(31)]},
"b": "not an object",
});
assert!(!conditions_nest_too_deep(Some(&shallow), LIMIT - 1));
assert!(!conditions_nest_too_deep(None, LIMIT - 1));
}
}