Model: Repeat and If nest four deep, as the limit says

The depth check refused any step list at the fifth level, so a fourth Repeat
or If could not even hold an empty body: three levels nested, not the four
MAX_MACRO_DEPTH and the docs state. The check now counts the Repeat or If
steps themselves, in the model's limits and in the parser's pass over the
JSON. The test asks for four and refuses a fifth.
This commit is contained in:
prxnvt committed 2026-10-23 21:45:54 -04:00
1 parent 378ce3a5a0
commit 259b6e67e8
3 files changed
+23 -11

No files matched your search

+5 -3
View File
@@ -53,9 +53,6 @@ fn check_size(m: &Macro) -> Result<(), String> {
/// before the steps are deserialized.
fn normalize_steps(steps: &mut Json, level: usize) -> Result<(), String> {
let Some(list) = steps.as_array_mut() else { return Err("`steps` must be a list.".into()) };
if level > gridcraft_model::MAX_MACRO_DEPTH {
return Err(format!("Repeat and If steps can nest at most {} deep.", gridcraft_model::MAX_MACRO_DEPTH));
}
for st in list {
let Some(o) = st.as_object_mut() else { return Err("Every step must be an object.".into()) };
if !o.contains_key("type")
@@ -64,6 +61,11 @@ fn normalize_steps(steps: &mut Json, level: usize) -> Result<(), String> {
o.insert("type".into(), json!("command"));
o.insert("id".into(), id);
}
// A step with a body is a Repeat or an If: one at the top is nested 1 deep.
let nested = ["body", "then", "else"].iter().any(|key| o.contains_key(*key));
if nested && level > gridcraft_model::MAX_MACRO_DEPTH {
return Err(format!("Repeat and If steps can nest at most {} deep.", gridcraft_model::MAX_MACRO_DEPTH));
}
for key in ["body", "then", "else"] {
if let Some(body) = o.get_mut(key) {
normalize_steps(body, level + 1)?;
+14 -5
View File
@@ -55,16 +55,25 @@ fn macros_over_the_limits_are_refused() {
let flat = |n: usize| json!({"name": "Big", "steps": vec![json!({"command": "cell.set"}); n]});
assert!(parse_macro(&flat(MAX_MACRO_STEPS)).is_ok());
assert!(parse_macro(&flat(MAX_MACRO_STEPS + 1)).unwrap_err().contains("10000 steps"));
// Nested Repeat steps: 4 levels are fine, 5 are not.
let nest = |levels: usize| {
// Nested Repeat steps: 4 Repeats one inside the other are fine (the innermost holds a
// command, or nothing), a 5th is not.
let nest = |repeats: usize| {
let mut step = json!({"command": "cell.set"});
for _ in 1..levels {
for _ in 0..repeats {
step = json!({"type": "repeat", "over": "Input", "body": [step]});
}
json!({"name": "Deep", "steps": [step]})
};
assert!(parse_macro(&nest(4)).is_ok());
assert!(parse_macro(&nest(5)).unwrap_err().contains("4 deep"));
assert!(parse_macro(&nest(MAX_MACRO_DEPTH)).is_ok());
assert_eq!(parse_macro(&nest(MAX_MACRO_DEPTH)).unwrap().step_count(), MAX_MACRO_DEPTH + 1);
assert!(parse_macro(&nest(MAX_MACRO_DEPTH + 1)).unwrap_err().contains("4 deep"));
let mut empty = json!({"type": "repeat", "over": "Input", "body": []});
for _ in 1..MAX_MACRO_DEPTH {
empty = json!({"type": "if", "cell": "A1", "op": "eq", "value": 1, "then": [empty], "else": []});
}
let deep = parse_macro(&json!({"name": "Deep", "steps": [empty]})).unwrap();
assert_eq!(deep.step_count(), MAX_MACRO_DEPTH);
assert!(deep.check_limits().is_ok(), "the model's check agrees with the parser");
// Far deeper than serde would allow is refused before it is read.
assert!(parse_macro(&nest(200)).is_err());
for bad in [
+4 -3
View File
@@ -81,10 +81,8 @@ impl Macro {
return Err(format!("A macro description can be at most {MAX_MACRO_TEXT} characters long."));
}
// Count without recursing past the depth limit (a hostile file can nest deeper).
// A Repeat or If at the top is nested 1 deep; one inside it, 2 deep.
fn walk(steps: &[Step], level: usize, count: &mut usize) -> Result<(), String> {
if level > MAX_MACRO_DEPTH {
return Err(format!("Repeat and If steps can nest at most {MAX_MACRO_DEPTH} deep."));
}
for s in steps {
*count = count.saturating_add(1);
if *count > MAX_MACRO_STEPS {
@@ -93,6 +91,9 @@ impl Macro {
if s.note.chars().count() > MAX_MACRO_TEXT {
return Err(format!("A step note can be at most {MAX_MACRO_TEXT} characters long."));
}
if s.kind.children().next().is_some() && level > MAX_MACRO_DEPTH {
return Err(format!("Repeat and If steps can nest at most {MAX_MACRO_DEPTH} deep."));
}
for body in s.kind.children() {
walk(body, level + 1, count)?;
}