mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
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:
1 parent
62b0d0bd60
commit
11f6f00ccc
2 files changed
+60
-1
No files matched your search
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user