Merge pull request #1079 from Ni-zav/fix/widthpoint-remove-indices-rebased-20261011

fix(width): validate every explicit width-point removal index
This commit is contained in:
BFlat 2026-10-11 07:44:20 +07:00 committed by GitHub
commit 2a52b2901f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 37 additions and 10 deletions

File diff suppressed because one or more lines are too long

View File

@ -323,11 +323,13 @@ fn width_point_copy(s: &mut Session, p: &Value) -> Result<Value> {
fn width_point_remove(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "stroke.widthPoint.remove";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let mut indices: Vec<usize> = match (p.get("indices").and_then(Value::as_array), p.get("index").and_then(Value::as_u64)) {
(Some(a), _) => a.iter().filter_map(Value::as_u64).map(|i| i as usize).collect(),
(None, Some(i)) => vec![i as usize],
let indices = match (p.get("indices"), p.get("index")) {
(Some(a), _) => point_indices(a),
(None, Some(i)) => point_indices(&json!([i])),
(None, None) => return Err(bad(C, "missing index")),
};
// A malformed index fails the whole delete instead of being dropped while the others go.
let mut indices = indices.ok_or_else(|| bad(C, "no such width point"))?;
indices.sort_unstable();
indices.dedup();
s.edit("Delete Width Point", |d, _| {

View File

@ -161,12 +161,6 @@ pub(crate) fn parse_freeform(v: &Value) -> std::result::Result<Freeform, String>
Ok(f)
}
/// `v` as a list of point indices: `None` unless it is an array of non-negative integers, so no
/// malformed index is dropped or wrapped round.
fn point_indices(v: &Value) -> Option<Vec<usize>> {
v.as_array()?.iter().map(|i| usize::try_from(i.as_u64()?).ok()).collect()
}
pub(crate) fn parse_mode(m: &str) -> std::result::Result<FreeformMode, String> {
FreeformMode::parse(m).ok_or_else(|| format!("unknown freeform mode `{m}` (points, lines)"))
}

View File

@ -340,6 +340,12 @@ pub(crate) fn finite_numbers<const N: usize>(v: &Value) -> Option<[f64; N]> {
Some(out)
}
/// `v` as a list of point indices: `None` unless it is an array of non-negative integers, so no
/// malformed index is dropped or wrapped round.
pub(crate) fn point_indices(v: &Value) -> Option<Vec<usize>> {
v.as_array()?.iter().map(|i| usize::try_from(i.as_u64()?).ok()).collect()
}
pub(crate) fn point_param(p: &Value, key: &str) -> Option<Point> {
let a = p.get(key)?.as_array()?;
Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))

View File

@ -30,6 +30,31 @@ fn dark(s: &Session, x: u32, y: u32) -> bool {
vectorcraft_render::Renderer::new().render(&doc, 200, 200, vectorcraft_geom::Affine::IDENTITY, &opts).pixel(x, y)[0] < 128
}
/// Invalid entries in an explicit multi-point delete must not delete the valid entries.
#[test]
fn invalid_width_point_indices_leave_the_entire_profile_unchanged() {
let (mut s, id) = line_session();
set(&mut s, json!({"id": id.0, "t": 0.5, "left": 10, "right": 10}));
let before = points(&s, id);
let history = s.doc().unwrap().history.undo.len();
for bad in [
json!({"indices": [1, "bad"]}),
json!({"indices": [-1, 1]}),
json!({"indices": [1.5, 1]}),
json!({"indices": {"point": 1}}),
json!({"index": "bad"}),
] {
let mut params = bad.as_object().unwrap().clone();
params.insert("id".into(), json!(id.0));
let err = s.execute("stroke.widthPoint.remove", &serde_json::Value::Object(params));
assert!(err.is_err(), "accepted malformed width point indices");
assert_eq!(points(&s, id), before);
assert_eq!(s.doc().unwrap().history.undo.len(), history);
}
s.execute("stroke.widthPoint.remove", &json!({"id": id.0, "indices": [1]})).unwrap();
assert_eq!(points(&s, id).len(), before.len() - 1);
}
#[test]
fn a_point_moved_onto_another_makes_a_discontinuous_point_that_renders_a_step() {
let (mut s, id) = line_session();