Merge pull request #998 from Ni-zav/fix/noissue-path-anchor-validation-s20261010T1654Z-8d74ec

Fix/noissue path anchor validation s20261010 t1654 z 8d74ec
This commit is contained in:
BFlat 2026-10-11 03:44:37 +07:00 committed by GitHub
commit 0cb1beaf21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 64 additions and 16 deletions

View File

@ -385,15 +385,24 @@ fn polar_grid(s: &mut Session, p: &Value) -> Result<Value> {
grid_group(s, "Polar Grid", shapes::polar_grid(r, c, rad))
}
pub(crate) fn anchor_from_json(v: &Value) -> Option<Anchor> {
let p = Point::new(v.get("x")?.as_f64()?, v.get("y")?.as_f64()?);
let h_in = point_param(v, "in").unwrap_or(p);
let h_out = point_param(v, "out").unwrap_or(p);
let mut a = Anchor::with_handles(p, h_in, h_out);
if v.get("smooth").and_then(Value::as_bool) == Some(true) {
a.kind = AnchorKind::Smooth;
/// An anchor `{x, y, in?, out?, smooth?}` given to `cmd` (handles `[x, y]`, at the anchor when
/// absent or null): anything malformed fails rather than being left out or replaced by a default,
/// which would change the path's shape.
pub(crate) fn anchor_from_json(v: &Value, cmd: &str) -> Result<Anchor> {
let coord =
|key: &str| v.get(key).and_then(Value::as_f64).filter(|n| n.is_finite()).ok_or_else(|| bad(cmd, format!("anchor `{key}` must be a number")));
let p = Point::new(coord("x")?, coord("y")?);
let handle = |key: &str| match v.get(key).filter(|h| !h.is_null()) {
None => Ok(p),
Some(h) => finite_numbers(h).map(|[x, y]| Point::new(x, y)).ok_or_else(|| bad(cmd, format!("anchor `{key}` must be [x, y]"))),
};
let mut anchor = Anchor::with_handles(p, handle("in")?, handle("out")?);
match v.get("smooth").filter(|b| !b.is_null()) {
None | Some(Value::Bool(false)) => {}
Some(Value::Bool(true)) => anchor.kind = AnchorKind::Smooth,
Some(_) => return Err(bad(cmd, "anchor `smooth` must be true or false")),
}
Some(a)
Ok(anchor)
}
fn path_create(s: &mut Session, p: &Value) -> Result<Value> {
@ -406,8 +415,8 @@ fn path_create(s: &mut Session, p: &Value) -> Result<Value> {
.and_then(Value::as_array)
.ok_or_else(|| bad("path.create", "missing anchors"))?
.iter()
.filter_map(anchor_from_json)
.collect();
.map(|a| anchor_from_json(a, "path.create"))
.collect::<Result<_>>()?;
if anchors.is_empty() {
return Err(bad("path.create", "need at least one anchor"));
}

View File

@ -129,7 +129,7 @@ fn path_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Result<&mut PathDa
fn append_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.appendAnchor", "missing id"))?;
let a = anchor_from_json(p).ok_or_else(|| bad("path.appendAnchor", "missing x/y"))?;
let a = anchor_from_json(p, "path.appendAnchor")?;
s.edit("Pen", |d, sel| {
let path = path_mut(d, id)?;
match path.subpaths.last_mut() {
@ -217,12 +217,21 @@ fn set_anchors(s: &mut Session, p: &Value) -> Result<Value> {
let data = PathData::new(
subs.iter()
.map(|sp| {
SubPath::new(
sp.get("anchors").and_then(Value::as_array).map(|a| a.iter().filter_map(anchor_from_json).collect()).unwrap_or_default(),
bool_or(sp, "closed", false),
)
let anchors = sp
.get("anchors")
.and_then(Value::as_array)
.ok_or_else(|| bad("path.setAnchors", "each subpath needs an anchors array"))?
.iter()
.map(|a| anchor_from_json(a, "path.setAnchors"))
.collect::<Result<Vec<_>>>()?;
let closed = match sp.get("closed") {
None | Some(Value::Null) => false,
Some(Value::Bool(v)) => *v,
Some(_) => return Err(bad("path.setAnchors", "closed must be a boolean")),
};
Ok(SubPath::new(anchors, closed))
})
.collect(),
.collect::<Result<Vec<_>>>()?,
);
s.edit("Reshape", |d, _| {
*path_mut(d, id)? = data;

View File

@ -993,3 +993,33 @@ fn malformed_anchor_selection_never_replaces_or_partially_changes_selection() {
assert!(s.doc().unwrap().selection.contains(a));
assert!(!s.doc().unwrap().selection.contains(b));
}
#[test]
fn path_commands_reject_bad_anchors_before_modifying_geometry() {
let mut s = session();
let a = rect(&mut s, 10.0, 20.0, 50.0, 30.0);
let original_points: Vec<_> = s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchors().map(|(_, _, an)| an.p).collect();
let history = s.doc().unwrap().history.undo.len();
let valid = json!({"x": 10, "y": 20});
for invalid in [
json!({"x": "bad", "y": 40}),
json!({"x": 50}),
json!({"x": 50, "y": 40, "in": [0]}),
json!({"x": 50, "y": 40, "out": [1, "bad"]}),
json!({"x": 50, "y": 40, "smooth": "yes"}),
] {
let anchors = json!([valid.clone(), invalid]);
assert!(s.execute("path.create", &json!({"anchors": anchors})).is_err(), "{anchors}");
assert!(s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": anchors}]})).is_err(), "{anchors}");
let points: Vec<_> = s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchors().map(|(_, _, an)| an.p).collect();
assert_eq!(points, original_points);
assert_eq!(s.doc().unwrap().history.undo.len(), history);
}
assert!(s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": []}, {"closed": true}]})).is_err());
assert!(s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": [valid.clone()], "closed": "yes"}]})).is_err());
// The Pen's append reads its anchor the same way.
assert!(s.execute("path.appendAnchor", &json!({"id": a.0, "x": 5, "y": 5, "out": [1]})).is_err());
assert_eq!(s.doc().unwrap().history.undo.len(), history);
s.execute("path.setAnchors", &json!({"id": a.0, "subpaths": [{"anchors": [{"x": 0, "y": 0}, {"x": 30, "y": 40}], "closed": false}]})).unwrap();
assert_eq!(s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().anchor_count(), 2);
}