Engine: a number in sketch is an exact sketch id, never a name (#5425, review of #6336)

`sketch_store::target` turned a JSON number into text and looked it up with
`Document::find_feature`, which falls back to a name when no feature has that id. So
`{"sketch": 999999}` could pick a sketch named "999999" whose id is something else, and export
or inspect it. sketch.export_dxf, sketch.glyphs and the two curvature inspects read an exact id
before they moved onto the store; the other sketch commands (the old `target_sketch`) and
sketch.inspect read a number as text.

One rule now, for every command that takes `sketch`: a number is an exact id (`as_u64`, then
the feature with that id); text is an id written out or a name, as before. A number that isn't
a whole number of 0 or more (negative, fractional) is refused with "`sketch` must be an id or a
name"; an id no feature has gives "no sketch `N`", as before. Nothing in the tests, docs,
scenarios or examples names a sketch with digits, so nothing relied on the old fallback.

Test (sketch_store): a sketch named "999999" with another id. sketch.export_dxf, sketch.glyphs
(with another sketch being edited, for its enabled check), inspect.curvature_comb and
inspect.minimum_radius (with a real curve id) refuse `{"sketch": 999999}`, find the sketch as
`"999999"` and by its own id, and refuse -1 and 1.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cameron Wight
2026-10-25 17:01:37 +00:00
parent c806f6062a
commit 2307757ae1
+48 -7
View File
@@ -11,16 +11,20 @@ use solidcraft_sketch::Sketch;
use crate::params::bad;
use crate::{EngineError, Result, Session};
/// The sketch a command acts on: its `sketch` param (id or name), or the sketch being edited
/// (also when `sketch` is null, as callers send an optional value they don't have).
/// The sketch a command acts on: its `sketch` param, or the sketch being edited (also when
/// `sketch` is null, as callers send an optional value they don't have). A number is an exact id;
/// text is an id written out or a name, so a sketch named "7" is reached only as text.
pub(super) fn target(s: &Session, p: &Value, cmd: &str) -> Result<u64> {
if let Some(v) = p.get("sketch").filter(|v| !v.is_null()) {
let key = match v {
Value::Number(n) => n.to_string(),
Value::String(x) => x.clone(),
let (key, found) = match v {
Value::Number(n) => {
let id = n.as_u64().ok_or_else(|| bad(cmd, "`sketch` must be an id or a name"))?;
(id.to_string(), s.doc.feature(id))
}
Value::String(x) => (x.clone(), s.doc.find_feature(x)),
_ => return Err(bad(cmd, "`sketch` must be an id or a name")),
};
let f = s.doc.find_feature(&key).ok_or_else(|| bad(cmd, format!("no sketch `{key}`")))?;
let f = found.ok_or_else(|| bad(cmd, format!("no sketch `{key}`")))?;
if !matches!(f.kind, FeatureKind::Sketch { .. }) {
return Err(bad(cmd, format!("`{key}` is not a sketch")));
}
@@ -64,7 +68,7 @@ pub(super) fn checked(sk: &Sketch) -> Result<()> {
#[cfg(test)]
mod tests {
use serde_json::json;
use serde_json::{Value, json};
use crate::Session;
@@ -81,6 +85,43 @@ mod tests {
assert!(super::target(&s, &json!({"sketch": true}), "test").is_err());
}
/// A number is an exact id, never a name (0xlillith on #6336): a sketch named "999999" isn't
/// sketch 999999, so a command given that number refuses instead of picking it, and the name
/// as text still finds it. Through the commands that read only ids before W38.0.
#[test]
fn target_reads_a_number_as_an_id_and_never_a_name() {
let mut s = Session::default();
s.execute("sketch.create", &json!({"plane": "XY", "name": "999999"})).unwrap();
let named = s.active_sketch.unwrap();
assert_ne!(named, 999999);
let c = s.execute("sketch.circle.center", &json!({"center": [0, 0], "radius": 5})).unwrap()["curves"][0].clone();
s.execute("sketch.finish", &json!({})).unwrap();
// Glyphs and the curvature inspects need a sketch being edited; it's another one.
s.execute("sketch.create", &json!({"plane": "XZ"})).unwrap();
for (cmd, p) in [
("sketch.export_dxf", json!({})),
("sketch.glyphs", json!({})),
("inspect.curvature_comb", json!({"curves": [c]})),
("inspect.minimum_radius", json!({"curves": [c]})),
] {
let with = |sketch: Value| {
let mut p = p.clone();
p["sketch"] = sketch;
p
};
let e = s.execute(cmd, &with(json!(999999))).unwrap_err().to_string();
assert!(e.contains("no sketch `999999`"), "{cmd}: {e}");
s.execute(cmd, &with(json!("999999"))).unwrap_or_else(|e| panic!("{cmd} by name: {e}"));
s.execute(cmd, &with(json!(named))).unwrap_or_else(|e| panic!("{cmd} by id: {e}"));
for bad in [json!(-1), json!(1.5)] {
let e = s.execute(cmd, &with(bad.clone())).unwrap_err().to_string();
assert!(e.contains("must be an id or a name"), "{cmd} {bad}: {e}");
}
}
assert_eq!(super::target(&s, &json!({"sketch": "999999"}), "test").unwrap(), named);
assert_eq!(super::target(&s, &json!({"sketch": named.to_string()}), "test").unwrap(), named);
}
/// Each sketch command finds and keeps its sketch through this store (W38.0). Outside it, the
/// command sources (non-test code) reach a sketch directly only on a line marked
/// `// design-only:` in the files that work on the design's own sketches.