Merge fix/625

This commit is contained in:
bflatastic
2026-10-17 16:37:25 +07:00
2 changed files with 60 additions and 1 deletions
@@ -0,0 +1,40 @@
//! Commands never leave a layer with a non-finite position or size: such a file saves but can't be
//! reopened (JSON writes NaN and infinity as null).
use serde_json::json;
use crate::Session;
fn rect(s: &mut Session) -> crate::NodeId {
let r = s.execute("node.create", &json!({"type": "rectangle", "x": 0, "y": 10, "width": 100, "height": 50})).unwrap();
crate::params::id_of(&r["id"]).unwrap()
}
fn assert_roundtrips(s: &Session) {
let bytes = figcraft_format::save_bytes(s.doc().unwrap(), None).unwrap();
assert!(figcraft_format::load_bytes(&bytes).is_ok(), "the saved file reopens");
}
#[test]
fn huge_moves_are_refused_and_the_file_still_reopens() {
let mut s = Session::new();
let r = rect(&mut s);
s.execute("transform.move", &json!({"ids": [r], "dx": 1e308, "dy": 0})).ok();
let second = s.execute("transform.move", &json!({"ids": [r], "dx": 1e308, "dy": 1e308}));
assert!(second.is_err(), "a move past the f64 range is refused");
let n = s.doc().unwrap().node(r).unwrap();
assert!(n.transform.as_coeffs().iter().all(|c| c.is_finite()), "{:?}", n.transform);
assert_roundtrips(&s);
}
#[test]
fn huge_scale_is_refused_and_the_file_still_reopens() {
let mut s = Session::new();
let r = rect(&mut s);
s.execute("transform.scale", &json!({"ids": [r], "factor": 1e300})).ok();
let second = s.execute("transform.scale", &json!({"ids": [r], "factor": 1e300}));
assert!(second.is_err(), "a size past the f64 range is refused");
let n = s.doc().unwrap().node(r).unwrap();
assert!(n.width.is_finite() && n.height.is_finite(), "{} x {}", n.width, n.height);
assert_roundtrips(&s);
}
+20 -1
View File
@@ -880,7 +880,8 @@ impl Session {
let result = guard::run(id, || (spec.run)(self, params))
.and_then(|v| self.check_instance_structure(id, &before).map(|()| v))
.and_then(|v| self.check_preview(id, &before).map(|()| v))
.and_then(|v| self.check_live_access(id, &before).map(|()| v));
.and_then(|v| self.check_live_access(id, &before).map(|()| v))
.and_then(|v| self.check_finite_geometry(id, &before).map(|()| v));
match &result {
Ok(_) => {
self.after_command(spec, before);
@@ -918,6 +919,22 @@ impl Session {
Err(EngineError::Disabled(id.to_string(), PREVIEW_READ_ONLY.into()))
}
/// A layer's position and size stay finite: infinity and NaN are written as `null`, so a file
/// holding them saves but can't be reopened. Refuse such an edit (it is rolled back).
fn check_finite_geometry(&self, id: &str, before: &Option<(Arc<Document>, Vec<NodeId>, NodeId, u64)>) -> Result<()> {
let Some((bdoc, _, _, uid)) = before else { return Ok(()) };
let Some(ds) = self.docs.iter().find(|d| d.uid == *uid) else { return Ok(()) };
if Arc::ptr_eq(&ds.doc, bdoc) {
return Ok(());
}
let broken = |n: &Node| !(n.transform.as_coeffs().iter().all(|c| c.is_finite()) && n.width.is_finite() && n.height.is_finite());
let untouched = |n: &Node| bdoc.node_arc(n.id).zip(ds.doc.node_arc(n.id)).is_some_and(|(a, b)| Arc::ptr_eq(a, b));
match ds.doc.nodes().find(|n| broken(n) && !untouched(n)) {
Some(n) => Err(bad(id, format!("\"{}\" would get a position or size beyond the number range", n.name))),
None => Ok(()),
}
}
/// A live session's document you may only view (or that is on its way to a server) is
/// read-only: refuse an edit (it is rolled back).
fn check_live_access(&self, id: &str, before: &Option<(Arc<Document>, Vec<NodeId>, NodeId, u64)>) -> Result<()> {
@@ -1530,6 +1547,8 @@ mod fig_export_tests;
#[cfg(test)]
mod fig_import_tests;
#[cfg(test)]
mod finite_geometry_tests;
#[cfg(test)]
mod frame_guides_tests;
#[cfg(test)]
mod frame_label_tests;