Variables dialog: malformed entries can't crash an edit (#875)

`ui.dialog.set` can store any JSON in the dialog's `__variables` field, and the Define and Data
Sets pages write fields into its entries (`d["name"] = …`, `set["name"] = …`), which panics when an
entry is a number or a string. The dialog now normalizes the state each frame, keeping only object
entries in `defs`, `dataSets` and each set's `values`.
This commit is contained in:
Tanish Manku authored and GitHub committed 2026-10-07 23:58:31 -07:00
1 parent 62b0d0bd60
commit 11f6f00ccc
2 files changed
+60 -1

No files matched your search

+26 -1
View File
@@ -110,7 +110,7 @@ const METHODS: &[(&str, &str)] = &[("fit", "Fit"), ("fill", "Fill"), ("conform",
pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, fields: &mut Map<String, Value>) {
let t = Tokens::get(ui.ctx());
let layers = layer_options(app);
let mut state = fields.get("__variables").cloned().unwrap_or_else(|| json!({"defs": [], "dataSets": []}));
let mut state = normalize(fields.get("__variables").unwrap_or(&Value::Null));
let mut page = fields.get("__page").and_then(Value::as_str).unwrap_or("define").to_string();
ui.set_min_width(420.0);
@@ -137,6 +137,22 @@ pub fn body(app: &mut PhotocraftApp, ui: &mut egui::Ui, fields: &mut Map<String,
fields.insert("__page".into(), json!(page));
}
/// The dialog state with only object entries in `defs`, `dataSets` and each set's `values`.
/// `ui.dialog.set` can store any JSON in `__variables`, and the pages write fields into entries
/// (`d["name"] = …`), which panics on a number or a string.
fn normalize(state: &Value) -> Value {
let objects =
|v: Option<&Value>| -> Vec<Value> { v.and_then(Value::as_array).map(|a| a.iter().filter(|e| e.is_object()).cloned().collect()).unwrap_or_default() };
let sets: Vec<Value> = objects(state.get("dataSets"))
.into_iter()
.map(|mut set| {
set["values"] = Value::Array(objects(set.get("values")));
set
})
.collect();
json!({"defs": objects(state.get("defs")), "dataSets": sets})
}
fn define_page(ui: &mut egui::Ui, _t: &Tokens, layers: &[(u64, String, bool)], state: &mut Value) {
let Some(defs) = state.get_mut("defs").and_then(Value::as_array_mut) else { return };
ui.label(tl!("Variables bind a layer's visibility, text or pixels to a named data slot."));
@@ -339,6 +355,15 @@ mod tests {
assert_eq!(data_set_index(&fields, 0), 0);
}
#[test]
fn normalize_keeps_only_object_entries() {
let s = normalize(&json!({"defs": [1, {"name": "a"}], "dataSets": ["x", {"name": "A", "values": [true, {"variable": "a"}]}, {"values": 3}]}));
assert_eq!(s, json!({"defs": [{"name": "a"}], "dataSets": [{"name": "A", "values": [{"variable": "a"}]}, {"values": []}]}));
for bad in [Value::Null, json!(5), json!("s"), json!([1]), json!({"defs": "x", "dataSets": {}})] {
assert_eq!(normalize(&bad), json!({"defs": [], "dataSets": []}), "{bad}");
}
}
#[test]
fn owns_detects_the_field() {
let mut f = Map::new();
@@ -55,3 +55,37 @@ fn out_of_range_data_set_index_is_clamped_before_delete() {
h.get_by_label("Delete").click();
h.run_steps(2);
}
/// `ui.dialog.set` can store any JSON in `__variables`; the editors write into its entries.
fn app_with_malformed_variables(page: &str) -> PhotocraftApp {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), photocraft_ui_egui::Services::default());
app.run("file.new", json!({"width": 64, "height": 64})).unwrap();
app.run("layer.new.layer", json!({})).unwrap();
let mut fields = Map::new();
fields.insert(
"__variables".into(),
json!({"defs": [42, {"name": "v1", "layer": 0, "type": "textReplacement"}], "dataSets": ["bad", {"name": "A", "values": [7, "x"]}, {"name": "B", "values": "nope"}]}),
);
fields.insert("__page".into(), json!(page));
app.ui.open_dialog(DialogKind::Command, fields);
app
}
fn type_into_first_text_field(h: &mut Harness<'static, PhotocraftApp>) {
h.query_all_by_role(egui::accesskit::Role::TextInput).next().expect("a text field").click();
h.run_steps(2);
h.query_all_by_role(egui::accesskit::Role::TextInput).next().expect("a text field").type_text("z");
h.run_steps(2);
}
#[test]
fn malformed_variable_entries_are_dropped_before_an_edit() {
for page in ["define", "dataSets"] {
let mut h = harness(app_with_malformed_variables(page));
type_into_first_text_field(&mut h);
let state = &h.state().ui.dialogs.last().unwrap().fields["__variables"];
assert_eq!(state["defs"].as_array().unwrap().len(), 1, "{page}: {state}");
assert_eq!(state["dataSets"].as_array().unwrap().len(), 2, "{page}: {state}");
assert!(state["dataSets"].as_array().unwrap().iter().all(|s| s["values"].as_array().unwrap().iter().all(|v| v.is_object())), "{page}: {state}");
}
}