Fix #785: commands refuse an id that isn't an object id instead of using the selection

Commands whose `id` or `ids` the shared check in Session::execute does not
read still parsed them with id_param/ids_param, which drop anything that is
not a non-negative integer, and then acted on the selection or the current
layer. `object.blend.make {"ids": 3}` blended the selection,
`object.envelope.makeWithWarp {"ids": [2, "x"]}` warped the selection,
`graph.setData {"id": "2"}` changed the selected graph and
`layer.newSublayer {"parent": "2"}` added a sublayer to the current layer.

- cmd::named_ids and cmd::named_id read `ids`/`id` as targets does: a value
  that is not an object id, or an unresolved step reference, is an error.
- The symbol and brush, Live Paint, Shape Builder and Image Trace, blend,
  envelope and mesh, perspective, Liquify, Puppet Warp and graph helpers use
  them in place of the lenient readers.
- layer.clippingMask.toggle checks `id`, and layer.newSublayer checks
  `parent`, with checked_id before falling back to the highlighted row or the
  current layer.
This commit is contained in:
dotCooCoo
2026-11-02 14:32:49 +07:00
committed by bflatastic
parent cfb30d8139
commit 6be88a24b4
11 changed files with 116 additions and 29 deletions
+35 -4
View File
@@ -512,11 +512,44 @@ pub fn color_value(v: &Value) -> Option<Color> {
}
}
/// `v` as an object id: an error unless it is a non-negative integer.
fn object_id(v: &Value) -> Result<NodeId> {
v.as_u64().map(NodeId).ok_or_else(|| EngineError::Other(format!("object ids are non-negative integers, not {v}")))
}
/// The error for `id` given as step reference `r`, which only a batch resolves.
fn unresolved_reference(r: &str) -> EngineError {
EngineError::Other(format!("`id` is the step reference {r:?}, which only `run` and `command_batch` resolve"))
}
/// The objects `p` names for a command whose `id` can only be an object id: its `ids`, else its
/// `id` as `ids` of one. A value that isn't an object id is an error, never the selection (#785).
/// `None` when `p` gives neither (or gives them as `null`). The ids are not looked up in the
/// document.
pub(crate) fn named_ids(p: &Value) -> Result<Option<Vec<NodeId>>> {
match p.get("ids").filter(|v| !v.is_null()) {
Some(Value::Array(a)) => return a.iter().map(object_id).collect::<Result<Vec<_>>>().map(Some),
Some(v) => return Err(EngineError::Other(format!("`ids` must be an array of object ids, not {v}"))),
None => {}
}
Ok(named_id(p)?.map(|id| vec![id]))
}
/// The object `p`'s `id` names, for a command whose `id` can only be an object id: a value that
/// isn't an object id is an error, never the selection (#785). `None` when `p` leaves `id` out (or
/// gives it as `null`). The id is not looked up in the document.
pub(crate) fn named_id(p: &Value) -> Result<Option<NodeId>> {
match p.get("id").filter(|v| !v.is_null()) {
None => Ok(None),
Some(Value::String(r)) if r.starts_with('$') => Err(unresolved_reference(r)),
Some(v) => object_id(v).map(Some),
}
}
/// Objects a command targets: explicit `ids` param or the selection.
pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
// Objects given are used as given: a value that isn't an object id fails rather than the
// command acting on the selection instead (#785).
let object_id = |v: &Value| v.as_u64().map(NodeId).ok_or_else(|| EngineError::Other(format!("object ids are non-negative integers, not {v}")));
match p.get("ids").filter(|v| !v.is_null()) {
Some(Value::Array(a)) => return a.iter().map(object_id).collect(),
Some(v) => return Err(EngineError::Other(format!("`ids` must be an array of object ids, not {v}"))),
@@ -526,9 +559,7 @@ pub(crate) fn targets(s: &Session, p: &Value) -> Result<Vec<NodeId>> {
Some(v @ Value::Number(_)) => Ok(vec![object_id(v)?]),
// A step reference a batch couldn't resolve (other strings are other ids, such as an
// effect's).
Some(Value::String(r)) if r.starts_with('$') => {
Err(EngineError::Other(format!("`id` is the step reference {r:?}, which only `run` and `command_batch` resolve")))
}
Some(Value::String(r)) if r.starts_with('$') => Err(unresolved_reference(r)),
_ => Ok(s.doc()?.selection.objects.clone()),
}
}