Commands: an index of 2^32 or more no longer wraps around on 32-bit builds

Squashed from #3292 onto main.
This commit is contained in:
bflatastic
2026-10-21 23:38:03 +07:00
parent 4e5f5e608b
commit e5471be4d9
37 changed files with 270 additions and 41 deletions
+4 -3
View File
@@ -218,7 +218,7 @@ pub(crate) fn item_target_at(s: &Session, p: &Value, key: &str, cmd: &str) -> Re
Some(Value::Null) => Ok(ItemTarget::Top),
Some(v) => v
.as_u64()
.map(|i| ItemTarget::Item { index: i as usize, explicit: true })
.map(|i| ItemTarget::Item { index: index(i), explicit: true })
.ok_or_else(|| bad(cmd, format!("`{key}` must be an appearance item index (paint order) or null"))),
// The active item is a row of the first selected object's own stack.
None if p.get("ids").is_some() || p.get("id").is_some() || targets_contents(p)? => Ok(ItemTarget::Top),
@@ -383,8 +383,9 @@ pub(crate) fn edits_stroke(s: &Session, p: &Value, item: ItemTarget, default: bo
Ok(ids.first().and_then(|id| d.node(*id)?.appearance.items.get(index)).map_or(default, |it| !it.is_fill()))
}
/// The whole number of 0 or more that `key` holds, read with [`index`].
pub(crate) fn index_param(p: &Value, key: &str, cmd: &str) -> Result<usize> {
p.get(key).and_then(Value::as_u64).map(|i| i as usize).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
}
fn set_active_item(s: &mut Session, p: &Value) -> Result<Value> {
@@ -603,7 +604,7 @@ fn item_indices(p: &Value, cmd: &str) -> Result<Vec<usize>> {
let mut v = match p.get("indices") {
Some(a) => a
.as_array()
.and_then(|a| a.iter().map(|x| x.as_u64().map(|i| i as usize)).collect::<Option<Vec<_>>>())
.and_then(|a| a.iter().map(|x| x.as_u64().map(index)).collect::<Option<Vec<_>>>())
.filter(|v| !v.is_empty())
.ok_or_else(|| bad(cmd, "`indices` must be a non-empty array of item indices"))?,
None => vec![index_param(p, "index", cmd)?],
+2 -1
View File
@@ -211,8 +211,9 @@ fn within_input_bound(q: Point) -> bool {
q.x.is_finite() && q.y.is_finite() && q.x.abs() < MAX_INPUT && q.y.abs() < MAX_INPUT
}
/// The whole number of 0 or more that `key` holds, read with [`index`].
fn usize_req(p: &Value, key: &str, cmd: &str) -> Result<usize> {
p.get(key).and_then(Value::as_u64).map(|v| v as usize).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
p.get(key).and_then(Value::as_u64).map(index).ok_or_else(|| bad(cmd, format!("missing integer `{key}`")))
}
/// The whole number of 0 or more that `key` holds, or `None` when `key` is absent or null. Any other
+2 -2
View File
@@ -171,7 +171,7 @@ pub fn specs() -> Vec<CommandSpec> {
}
fn file_close(s: &mut Session, p: &Value) -> Result<Value> {
let i = p.get("index").and_then(Value::as_u64).map(|v| v as usize).or(s.active_index()).ok_or(EngineError::NoDocument)?;
let i = p.get("index").and_then(Value::as_u64).map(index).or(s.active_index()).ok_or(EngineError::NoDocument)?;
if !s.close_document(i) {
return Err(bad("file.close", "no such document"));
}
@@ -179,7 +179,7 @@ fn file_close(s: &mut Session, p: &Value) -> Result<Value> {
}
fn doc_activate(s: &mut Session, p: &Value) -> Result<Value> {
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("document.activate", "missing index"))? as usize;
let i = p.get("index").and_then(Value::as_u64).map(index).ok_or_else(|| bad("document.activate", "missing index"))?;
if s.set_active(i) { ok() } else { Err(bad("document.activate", "no such document")) }
}
+2 -2
View File
@@ -227,7 +227,7 @@ fn edit<R>(s: &mut Session, p: &Value, cmd: &str, mut f: impl FnMut(&mut Freefor
/// The `index` param, else the selected point.
fn point_index(s: &Session, p: &Value, cmd: &str) -> Result<usize> {
match p.get("index") {
Some(v) => v.as_u64().map(|i| i as usize).ok_or_else(|| bad(cmd, "`index` must be a point index")),
Some(v) => v.as_u64().map(index).ok_or_else(|| bad(cmd, "`index` must be a point index")),
None => s.selected_freeform_point().ok_or_else(|| bad(cmd, "no point selected (give `index`)")),
}
}
@@ -318,7 +318,7 @@ fn select_point(s: &mut Session, p: &Value) -> Result<Value> {
None => return Err(bad(C, "missing `index` (a point index, or null to clear)")),
Some(Value::Null) => None,
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(C, "`index` must be a whole number or null"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`index` must be a whole number or null"))?;
let (f, _) = s.proxy_freeform(!s.fill_active).ok_or_else(|| bad(C, NOT_FREEFORM))?;
check_index(&f, i).map_err(|e| bad(C, e))?;
Some(i)
+1 -1
View File
@@ -60,7 +60,7 @@ fn select_stop(s: &mut Session, p: &Value) -> Result<Value> {
None => return Err(bad(C, "missing `index` (a stop index, or null to clear)")),
Some(Value::Null) => None,
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(C, "`index` must be a whole number or null"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`index` must be a whole number or null"))?;
let n = active_gradient(s).ok_or_else(|| bad(C, "the active paint is not a gradient"))?.gradient.stops.len();
if i >= n {
return Err(bad(C, format!("no stop {i} (the gradient has {n})")));
+2 -2
View File
@@ -289,7 +289,7 @@ pub(crate) fn make_layer(d: &mut Document, name: Option<&str>) -> Node {
pub(super) fn layer_color_param(v: &Value) -> std::result::Result<LayerColor, String> {
match v {
Value::Number(n) => match n.as_u64() {
Some(i) if (i as usize) < LAYER_COLORS.len() => Ok(LayerColor::Preset(i as u8)),
Some(i) if index(i) < LAYER_COLORS.len() => Ok(LayerColor::Preset(i as u8)),
_ => Err(format!("a colour index is 0 to {}, not {n}", LAYER_COLORS.len() - 1)),
},
Value::String(s) => {
@@ -781,7 +781,7 @@ fn move_node(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "node.move";
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing id"))?;
let parent = id_param(p, "parent");
let index = p.get("index").and_then(Value::as_u64).unwrap_or(u64::MAX).min(usize::MAX as u64) as usize;
let index = p.get("index").and_then(Value::as_u64).map_or(usize::MAX, index);
let d = &s.doc()?.doc;
let n = d.node(id).ok_or(EngineError::NoNode(id))?;
if !accepts(d, parent, n) {
+1 -1
View File
@@ -1088,7 +1088,7 @@ fn move_group(s: &mut Session, p: &Value) -> Result<Value> {
let (name, at) = s.libraries.change(&id, |lib| {
let i = group_at(lib, group, C)?;
let g = lib.groups.remove(i);
let at = usize::try_from(to).unwrap_or(usize::MAX).min(lib.groups.len());
let at = index(to).min(lib.groups.len());
let name = g.name.clone();
lib.groups.insert(at, g);
Ok((name, at))
+4
View File
@@ -341,6 +341,10 @@ pub(crate) fn id_param(p: &Value, key: &str) -> Option<NodeId> {
pub(crate) fn ids_param(p: &Value, key: &str) -> Option<Vec<NodeId>> {
p.get(key).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(NodeId).collect())
}
/// `v` as an index, a count or a byte offset: past `usize::MAX` reads as `usize::MAX`, past the end of any list.
pub(crate) fn index(v: u64) -> usize {
usize::try_from(v).unwrap_or(usize::MAX)
}
/// Validate all explicit targets before a command changes the document or selection.
pub(crate) fn checked_ids_param(s: &Session, p: &Value, key: &str, command: &str) -> Result<Vec<NodeId>> {
let values =
+3 -3
View File
@@ -279,7 +279,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Bounds",
[],
None,
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))",
"{x?, y?, width?, height?, reference?: 0..8 (9-point grid; a number past 8 is read as 8), proportional?, strokes?, corners?, copy?: bool, patterns?: bool (Transform Patterns), typeAreas?: bool (area type: the frame is reshaped and the text reflows at its size), objects?: bool (Transform Objects, as object.move)} (Transform panel; width/height and the reference point follow the bounding box, rotated with rotated objects; x, y are page coordinates; with Use Preview Bounds the values measure the visual bounds; strokes/corners as object.scale; with anchor points selected (Direct Selection) and no copy, the values are the box around those points, square to the page, and only they move or scale, as with object.move (#1824))",
has_selection,
set_bounds
),
@@ -1791,7 +1791,7 @@ fn bounds_transform(s: &Session, p: &Value, ids: Vec<NodeId>, least: f64) -> Res
let margins = |b: Rect| [g.x0 - b.x0, g.y0 - b.y0, b.x1 - g.x1, b.y1 - g.y1];
let (m, all) = (margins(vs), margins(v));
let fixed = [all[0] - m[0], all[1] - m[1], all[2] - m[2], all[3] - m[3]];
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize;
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) as usize;
let rp = vectorcraft_geom::reference_point(v, refi);
let (w, h) = wanted_size(p, v);
// The geometric scale that gives the visual size: margins grow by the strokes' scale `f`. It is
@@ -1954,7 +1954,7 @@ fn wanted_size(p: &Value, b: Rect) -> (f64, f64) {
/// `b`, the box around the anchors (and the objects selected whole) square to the page, and only
/// the anchors, their handles with them, and those objects move or scale.
fn set_anchor_bounds(s: &mut Session, p: &Value, ids: Vec<NodeId>, b: Rect) -> Result<Value> {
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize;
let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4).min(8) as usize;
let rp = vectorcraft_geom::reference_point(b, refi);
let (w, h) = wanted_size(p, b);
// A single anchor, or anchors in a line, have no size to scale on that side.
+1 -1
View File
@@ -55,7 +55,7 @@ pub fn specs() -> Vec<CommandSpec> {
fn artboard_reorder(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "artboard.reorder";
let i = artboard_index(&s.doc()?.doc, p, C)?;
let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(C, "missing to"))? as usize;
let to = p.get("to").and_then(Value::as_u64).map(index).ok_or_else(|| bad(C, "missing to"))?;
s.edit("Reorder Artboards", |d, _| {
if i >= d.artboards.len() {
return Err(EngineError::Other("no such artboard".into()));
+3 -3
View File
@@ -226,7 +226,7 @@ pub(crate) fn transform_anchors(s: &mut Session, label: &str, xf: vectorcraft_ge
fn set_handle(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.setHandle", "missing id"))?;
let si = usize_opt(p, "subpath", "path.setHandle")?.unwrap_or(0);
let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad("path.setHandle", "missing anchor"))? as usize;
let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.setHandle", "missing anchor"))?;
let out = str_param(p, "which") != Some("in");
let pos = Point::new(f64_req(p, "x", "path.setHandle")?, f64_req(p, "y", "path.setHandle")?);
let independent = bool_or(p, "independent", false);
@@ -244,7 +244,7 @@ fn set_handle(s: &mut Session, p: &Value) -> Result<Value> {
fn move_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.moveAnchor", "missing id"))?;
let si = usize_opt(p, "subpath", "path.moveAnchor")?.unwrap_or(0);
let ai = p.get("anchor").and_then(Value::as_u64).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))? as usize;
let ai = p.get("anchor").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.moveAnchor", "missing anchor"))?;
let pos = Point::new(f64_req(p, "x", "path.moveAnchor")?, f64_req(p, "y", "path.moveAnchor")?);
s.edit("Move", |d, _| {
let path = path_mut(d, id)?;
@@ -772,7 +772,7 @@ fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
fn insert_anchor(s: &mut Session, p: &Value) -> Result<Value> {
let id = id_param(p, "id").ok_or_else(|| bad("path.insertAnchor", "missing id"))?;
let si = usize_opt(p, "subpath", "path.insertAnchor")?.unwrap_or(0);
let seg = p.get("segment").and_then(Value::as_u64).ok_or_else(|| bad("path.insertAnchor", "missing segment"))? as usize;
let seg = p.get("segment").and_then(Value::as_u64).map(index).ok_or_else(|| bad("path.insertAnchor", "missing segment"))?;
let t = f64_or(p, "t", 0.5).clamp(0.0, 1.0);
let idx = s.edit("Add Anchor Point", |d, _| {
let path = path_mut(d, id)?;
+1 -1
View File
@@ -96,7 +96,7 @@ fn set_origin(s: &mut Session, p: &Value) -> Result<Value> {
let regions = vectorcraft_pdf::print_regions(doc, &set).map_err(|e| pdf_error(SET, e))?;
let picked = match p.get("artboard").filter(|v| !v.is_null()) {
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(SET, "artboard must be a 0-based index"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(SET, "artboard must be a 0-based index"))?;
regions.iter().find(|(a, _)| a.is_none() || *a == Some(i)).ok_or_else(|| bad(SET, format!("artboard {i} doesn't print")))?
}
None => region_holding(&regions, at).or(regions.first()).ok_or_else(|| bad(SET, "nothing prints"))?,
+3 -3
View File
@@ -156,11 +156,11 @@ fn reduce(s: &mut Session, p: &Value) -> Result<Value> {
}
}
let unit_colors = |g: &[&Used]| g.iter().map(|u| u.color).collect::<Vec<_>>();
let rows: Vec<Vec<&Used>> = match p.get("colors").and_then(Value::as_u64) {
Some(n) if (n as usize) < units.len() => {
let rows: Vec<Vec<&Used>> = match p.get("colors").and_then(Value::as_u64).map(index) {
Some(n) if n < units.len() => {
let keys: Vec<Color> = units.iter().map(|g| method.key_color(&unit_colors(g)).unwrap_or_default()).collect();
let weights: Vec<f32> = units.iter().map(|g| g.iter().map(|u| u.count as f32).sum()).collect();
cluster(&keys, &weights, n as usize).into_iter().map(|ix| ix.into_iter().flat_map(|i| units[i].iter().copied()).collect()).collect()
cluster(&keys, &weights, n).into_iter().map(|ix| ix.into_iter().flat_map(|i| units[i].iter().copied()).collect()).collect()
}
_ => units,
};
+2 -2
View File
@@ -266,7 +266,7 @@ fn all(s: &mut Session, _: &Value) -> Result<Value> {
fn all_on_artboard(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let i = p.get("artboard").and_then(Value::as_u64).unwrap_or(0) as usize;
let i = p.get("artboard").and_then(Value::as_u64).map_or(0, index);
let r = st.doc.artboards.get(i).map(|a| a.rect).ok_or_else(|| bad("select.allOnArtboard", "no such artboard"))?;
// Symbol instances are measured by their art.
let symbols = st.doc.symbol_boxes();
@@ -644,7 +644,7 @@ fn same_attribute(s: &mut Session, p: &Value) -> Result<Value> {
let st = s.doc()?;
let refn = st.selection.objects.first().and_then(|id| st.doc.node(*id)).ok_or_else(|| bad(cmd, "nothing selected"))?;
let ap = &refn.appearance;
let item = p.get("item").and_then(Value::as_u64).map(|i| i as usize).or(s.appearance_item());
let item = p.get("item").and_then(Value::as_u64).map(index).or(s.appearance_item());
let attr = match (item.and_then(|i| ap.items.get(i)), ap.effects.first()) {
(Some(it), _) => Attribute::Item(it.clone()),
(None, Some(e)) => Attribute::Effect(e.clone()),
+1 -1
View File
@@ -409,7 +409,7 @@ fn splits(lo: f64, hi: f64, count: Option<u64>, size: Option<f64>) -> std::resul
(None, Some(_)) => return Err("a slice size must be a positive number of points".into()),
(None, None) => 1,
};
if n == 0 || n as usize > MAX_PIECES {
if n == 0 || index(n) > MAX_PIECES {
return Err(format!("divide into 1 to {MAX_PIECES} slices"));
}
let step = match (count, size) {
+1 -1
View File
@@ -547,7 +547,7 @@ fn style_move(s: &mut Session, p: &Value) -> Result<Value> {
let to = p.get("to").and_then(Value::as_u64).ok_or_else(|| bad(cmd, "missing `to` (an index)"))?;
let d = &s.doc()?.doc;
let from = style_index(d, name)?;
let to = (to as usize).min(d.graphic_styles.len() - 1);
let to = index(to).min(d.graphic_styles.len() - 1);
if from == to {
return ok();
}
+1 -1
View File
@@ -364,7 +364,7 @@ fn insert_before<T>(list: &mut Vec<T>, items: Vec<T>, anchor: Option<&str>, name
fn swatch_move(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "swatch.move";
let names = names_param(p, C)?;
let to = p.get("to").and_then(Value::as_u64).map(|v| v as usize);
let to = p.get("to").and_then(Value::as_u64).map(index);
let group = str_param(p, "group").map(str::to_string);
let moved = names.len();
s.edit("Move Swatch", |d, _| {
+2 -1
View File
@@ -174,8 +174,9 @@ pub(crate) fn set_plain_text(d: &mut vectorcraft_doc::Document, id: NodeId, text
Ok(())
}
/// The byte offset that `k` holds, read with [`index`].
fn byte_param(p: &Value, k: &str) -> Option<usize> {
p.get(k).and_then(Value::as_u64).map(|v| v as usize)
p.get(k).and_then(Value::as_u64).map(index)
}
fn range_of(p: &Value, len: usize) -> (usize, usize) {
+5 -8
View File
@@ -343,10 +343,10 @@ fn selection_attrs(s: &Session, p: &Value, kind: Kind) -> Result<Option<Map<Stri
None => text_targets(s, &json!({}), "").ok().and_then(|v| v.first().copied()),
};
let Some(NodeKind::Text(t)) = id.and_then(|i| d.node(i)).map(|n| &n.kind) else { return Ok(None) };
let at = p.get("start").and_then(Value::as_u64).map_or(0, |v| usize::try_from(v).unwrap_or(usize::MAX));
let at = p.get("start").and_then(Value::as_u64).map_or(0, index);
Ok(Some(match kind {
Kind::Char => {
let style = match p.get("end").and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)) {
let style = match p.get("end").and_then(Value::as_u64).map(index) {
Some(end) => vectorcraft_text::edit::insertion_style(&t.runs, at.min(end), at.max(end)),
None => vectorcraft_text::edit::style_at(&t.runs, at),
};
@@ -487,12 +487,9 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
match kind {
Kind::Char => {
let len = vectorcraft_text::edit::runs_len(&t.runs);
let (a, b) = match range {
Some(_) => {
let g = |k: &str, def: usize| p.get(k).and_then(Value::as_u64).map_or(def, |v| (v as usize).min(len));
(g("start", 0), g("end", len))
}
None => (0, len),
let (a, b) = match (range, text_range) {
(Some(_), Some(r)) => r.bytes(t),
_ => (0, len),
};
let mut err = None;
vectorcraft_text::edit::style_range(&mut t.runs, a.min(b), a.max(b), |st| match char_fn(st) {
+1 -3
View File
@@ -532,9 +532,7 @@ impl TextRange {
let get = |k: &str| -> Result<Option<usize>> {
match p.get(k) {
None | Some(Value::Null) => Ok(None),
Some(v) => {
v.as_u64().map(|n| Some(usize::try_from(n).unwrap_or(usize::MAX))).ok_or_else(|| bad(cmd, format!("`{k}` must be a byte offset")))
}
Some(v) => v.as_u64().map(|n| Some(index(n))).ok_or_else(|| bad(cmd, format!("`{k}` must be a byte offset"))),
}
};
let (start, end) = (get("start")?, get("end")?);
+1 -1
View File
@@ -345,7 +345,7 @@ fn sample_color(s: &mut Session, p: &Value) -> Result<Value> {
let (id, mut q) = match p.get("stop") {
None | Some(Value::Null) => (if stroke { "paint.setStroke" } else { "paint.setFill" }, json!({ "color": c })),
Some(v) => {
let i = v.as_u64().ok_or_else(|| bad(C, "`stop` must be a stop index"))? as usize;
let i = v.as_u64().map(index).ok_or_else(|| bad(C, "`stop` must be a stop index"))?;
let color = super::paint::new_color(s, p, color_value(c).ok_or_else(|| bad(C, format!("bad color {c}")))?);
let (fill, stroke_paint) = s.proxy_paints();
let Paint::Gradient(g) = (if stroke { stroke_paint } else { fill }) else {
+39
View File
@@ -1084,6 +1084,45 @@ fn set_bounds_acts_on_direct_selected_anchors() {
assert_eq!(points(&s), [(10.0, 0.0), (140.0, 20.0), (60.0, 100.0), (10.0, 100.0)]);
}
/// `object.setBounds` reads a `reference` past 8 as 8, the bottom-right point, as
/// `object.transformEach` does, for a path selected whole and for direct-selected anchors. Before,
/// 9 read as the bottom-left point, 2^32 as the bottom center, and a 32-bit build read 2^32 as the
/// top-left point.
#[test]
fn set_bounds_reads_a_reference_past_8_as_8() {
for reference in [9, 1u64 << 32] {
for anchors in [false, true] {
let mut s = session();
let square = json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}], "closed": true});
let id = NodeId(s.execute("path.create", &square).unwrap()["id"].as_u64().unwrap());
if anchors {
s.execute("select.anchors", &json!({"id": id.0, "anchors": [[0, 0], [0, 1], [0, 2], [0, 3]]})).unwrap();
} else {
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
}
s.execute("object.setBounds", &json!({"width": 50, "height": 50, "reference": reference})).unwrap();
let NodeKind::Path { path, .. } = &s.doc().unwrap().doc.node(id).unwrap().kind else { panic!("a path") };
let points: Vec<(f64, f64)> = path.subpaths[0].anchors.iter().map(|a| (a.p.x.round(), a.p.y.round())).collect();
assert_eq!(points, [(50.0, 50.0), (100.0, 50.0), (100.0, 100.0), (50.0, 100.0)], "reference {reference}, anchors {anchors}");
}
}
}
/// `cmd::index` keeps a number that fits in a `usize` and reads a larger one (2^32 or more on a
/// 32-bit build) as `usize::MAX`.
#[test]
fn index_reads_a_number_past_usize_max_as_usize_max() {
let past_u32 = u64::from(u32::MAX) + 1;
#[cfg(target_pointer_width = "64")]
let expected = 1usize << 32;
#[cfg(not(target_pointer_width = "64"))]
let expected = usize::MAX;
assert_eq!(cmd::index(0), 0);
assert_eq!(cmd::index(7), 7);
assert_eq!(cmd::index(past_u32), expected);
assert_eq!(cmd::index(u64::MAX), usize::MAX);
}
/// Arrange commands that leave the stacking order as it is record no undo step: Bring to Front and
/// Bring Forward on the front objects, Send to Back and Send Backward on the back ones, and Send to
/// Current Layer on objects already on top of the current layer.
+44
View File
@@ -974,6 +974,50 @@ fn split_refuses_a_subpath_anchor_or_segment_that_is_not_a_whole_number() {
}
}
/// A subpath, anchor or segment of 2^32 + k fails, where k names one that exists, and the document
/// and the undo history stay as they were. A 32-bit build read the number as k.
#[test]
fn path_commands_refuse_a_subpath_anchor_or_segment_past_u32() {
let mut s = session();
let id = two_subpaths(&mut s);
let at_anchor = json!({"id": id.0, "anchor": 1});
let at_segment = json!({"id": id.0, "segment": 0, "t": 0.5});
let curvature_move = json!({"id": id.0, "op": "move", "anchor": 1, "x": 200, "y": 150});
let curvature_insert = json!({"id": id.0, "op": "insert", "segment": 0, "t": 0.5});
let convert = json!({"id": id.0, "anchor": 1, "to": "corner"});
let reshape = json!({"id": id.0, "segment": 0, "t": 0.5, "dx": 0, "dy": 20});
let handle = json!({"id": id.0, "anchor": 0, "which": "out", "x": 140, "y": 250});
let moved = json!({"id": id.0, "anchor": 0, "x": 140, "y": 250});
for (cmd, p, key) in [
("path.curvatureEdit", &curvature_move, "subpath"),
("path.curvatureEdit", &curvature_move, "anchor"),
("path.curvatureEdit", &curvature_insert, "segment"),
("path.removeAnchor", &at_anchor, "subpath"),
("path.removeAnchor", &at_anchor, "anchor"),
("path.convertAnchor", &convert, "subpath"),
("path.convertAnchor", &convert, "anchor"),
("path.reshapeSegment", &reshape, "subpath"),
("path.reshapeSegment", &reshape, "segment"),
("path.split", &at_anchor, "anchor"),
("path.split", &at_segment, "segment"),
("path.setHandle", &handle, "subpath"),
("path.setHandle", &handle, "anchor"),
("path.moveAnchor", &moved, "subpath"),
("path.moveAnchor", &moved, "anchor"),
("path.insertAnchor", &at_segment, "subpath"),
("path.insertAnchor", &at_segment, "segment"),
] {
let mut p = p.clone();
let k = if key == "subpath" { 1 } else { p[key].as_u64().unwrap() };
p[key] = json!((1u64 << 32) + k);
let before = s.doc().unwrap().doc.clone();
let undo = undo_steps(&s);
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p} succeeded");
assert!(s.doc().unwrap().doc == before, "{cmd} {p} changed the document");
assert_eq!(undo_steps(&s), undo, "{cmd} {p} left an undo step");
}
}
#[test]
fn set_handle_refuses_a_subpath_that_is_not_a_whole_number() {
let mut s = session();
+21
View File
@@ -262,6 +262,27 @@ fn point_commands_need_a_freeform_gradient() {
assert!(s.execute("paint.freeform.addPoint", &json!({"at": [1, 1]})).is_err());
}
/// The point and line commands refuse a point, line or segment of 2^32 + k, where k names one that
/// exists, and the points and the selected point stay as they were. A 32-bit build read the number
/// as k.
#[test]
fn point_commands_refuse_an_index_past_u32() {
let (mut s, id) = freeform_rect();
run(&mut s, "paint.freeform.addLine", json!({"points": [0, 1]}));
let (before, selected) = (points(&s, id), s.selected_freeform_point());
let past = |k: u64| (1u64 << 32) + k;
for (cmd, p) in [
("paint.freeform.setPoint", json!({"index": past(1), "color": "#000000"})),
("paint.freeform.deletePoint", json!({"index": past(1)})),
("paint.freeform.selectPoint", json!({"index": past(1)})),
("paint.freeform.splitLine", json!({"line": past(0), "segment": 0})),
("paint.freeform.splitLine", json!({"line": 0, "segment": past(0)})),
] {
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p}");
assert_eq!((points(&s, id), s.selected_freeform_point()), (before.clone(), selected), "{cmd} {p}");
}
}
#[test]
fn selection_follows_the_object_and_the_proxy() {
let (mut s, id) = freeform_rect();
+2
View File
@@ -288,6 +288,8 @@ fn select_stop_is_validated_and_shared() {
assert_eq!(s.execute("gradient.selectStop", &json!({"index": 1})).unwrap(), json!({"index": 1}));
assert_eq!(s.selected_stop(), Some(1));
assert!(s.execute("gradient.selectStop", &json!({"index": 2})).is_err(), "two stops");
// A 32-bit build read 2^32 + 1 as stop 1.
assert!(s.execute("gradient.selectStop", &json!({"index": (1u64 << 32) + 1})).is_err(), "past u32");
assert!(s.execute("gradient.selectStop", &json!({"index": "a"})).is_err());
assert!(s.execute("gradient.selectStop", &json!({})).is_err());
s.execute("gradient.selectStop", &json!({"index": null})).unwrap();
+2
View File
@@ -38,6 +38,8 @@ fn sample_color_with_a_stop_recolours_that_stop_in_one_undo_step() {
s.execute("paint.sampleColor", &json!({"color": "#ff0000", "stop": 1})).unwrap();
assert_eq!(stop_hexes(&s, id, false), ["#ffffff", "#ff0000"]);
assert!(s.execute("paint.sampleColor", &json!({"color": "#ff0000", "stop": 2})).unwrap_err().to_string().contains("no stop 2"));
// A 32-bit build read 2^32 + 1 as stop 1.
assert!(s.execute("paint.sampleColor", &json!({"color": "#00ff00", "stop": (1u64 << 32) + 1})).is_err());
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(stop_hexes(&s, id, false), ["#ffffff", "#000000"]);
// The stroke's gradient through `stroke`, the fill left alone.
+2
View File
@@ -349,6 +349,8 @@ fn layer_options_validate_and_template_locks_and_dims() {
p["id"] = json!(layer.0);
assert!(s.execute("layer.setProps", &p).is_err(), "{bad}");
}
// A 32-bit build read color 2^32 + 4 as preset 4.
assert!(s.execute("layer.setProps", &json!({"id": layer.0, "color": (1u64 << 32) + 4})).is_err());
let color = |s: &Session| match node(s, layer).kind {
NodeKind::Layer { color, .. } => color,
_ => panic!("not a layer"),
+23
View File
@@ -1515,6 +1515,17 @@ fn select_all_on_artboard_takes_lines_on_its_right_and_bottom_edges() {
assert_eq!(on(1), vec![right, shared]);
}
/// All on Active Artboard refuses artboard 2^32 and selects nothing. A 32-bit build read it as
/// artboard 0.
#[test]
fn select_all_on_artboard_refuses_an_artboard_past_u32() {
let mut s = session();
rect(&mut s, 10.0, 10.0, 20.0, 20.0);
s.execute("select.none", &json!({})).unwrap();
assert!(s.execute("select.allOnArtboard", &json!({"artboard": 1u64 << 32})).is_err());
assert!(selected(&s).is_empty());
}
#[test]
fn save_and_recall_selection() {
let mut s = session();
@@ -2699,6 +2710,18 @@ fn close_all_documents() {
assert!(s.execute("file.closeAll", &json!({})).is_err());
}
/// Close and Activate refuse a document index of 2^32, and both documents stay open with the second
/// one active. A 32-bit build read the index as 0.
#[test]
fn close_and_activate_refuse_a_document_index_past_u32() {
let mut s = session();
s.execute("file.new", &json!({})).unwrap();
for cmd in ["document.activate", "file.close"] {
assert!(s.execute(cmd, &json!({"index": 1u64 << 32})).is_err(), "{cmd}");
assert_eq!((s.documents().len(), s.active_index()), (2, Some(1)), "{cmd}");
}
}
#[test]
fn document_color_mode_converts_colours() {
let mut s = session();
+36
View File
@@ -81,6 +81,31 @@ fn appearance_duplicate_and_move() {
assert!(s.execute("appearance.duplicateItem", &json!({"index": 99})).is_err());
}
/// The appearance and effect commands given an item index of 2^32 + k, where k names one of the
/// object's items, leave the object's appearance and the undo history as they were, as for any index
/// past the last item: `appearance.removeItem` removes nothing, and the others fail. A 32-bit build
/// read the index as k.
#[test]
fn appearance_commands_leave_an_item_past_u32_alone() {
let mut s = session();
let id = rect(&mut s);
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
let before = node(&s, id).appearance;
let undo = s.doc().unwrap().history.undo.len();
let past = |k: u64| (1u64 << 32) + k;
for (cmd, p, fails) in [
("appearance.removeItem", json!({"index": past(0)}), false),
("appearance.removeItem", json!({"indices": [past(0)]}), false),
("appearance.duplicateItem", json!({"index": past(0)}), true),
("appearance.moveItem", json!({"from": past(1), "to": 0}), true),
("effect.apply", json!({"effect": "distort.roughen", "item": past(1)}), true),
] {
assert_eq!(s.execute(cmd, &p).is_err(), fails, "{cmd} {p}");
assert_eq!(node(&s, id).appearance, before, "{cmd} {p}");
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "{cmd} {p}");
}
}
#[test]
fn artboards_reorder_and_duplicate() {
let mut s = session();
@@ -155,6 +180,17 @@ fn artboard_color_round_trips_through_the_native_format() {
assert_eq!(back.artboards, s.doc().unwrap().doc.artboards);
}
/// An artboard moved to place 2^32 goes last. A 32-bit build read the place as 0 and moved it to the
/// front.
#[test]
fn artboard_reorder_past_u32_goes_last() {
let mut s = session();
s.execute("artboard.new", &json!({"name": "Second"})).unwrap();
s.execute("artboard.new", &json!({"name": "Third"})).unwrap();
s.execute("artboard.reorder", &json!({"index": 1, "to": 1u64 << 32})).unwrap();
assert_eq!(s.doc().unwrap().doc.artboards.last().map(|a| a.name.as_str()), Some("Second"));
}
/// The artboards' left edges and names.
fn boards(s: &Session) -> Vec<(f64, String)> {
s.doc().unwrap().doc.artboards.iter().map(|a| (a.rect.x0, a.name.clone())).collect()
+2
View File
@@ -144,6 +144,8 @@ fn tiles_start_at_the_tile_origin_and_cover_the_art() {
// Bad input is refused.
assert!(s.execute("print.tiling.set", &json!({})).is_err());
assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 7})).is_err());
// A 32-bit build read 2^32 as artboard 0.
assert!(s.execute("print.tiling.set", &json!({"origin": [1, 2], "artboard": 1u64 << 32})).is_err());
assert!(s.execute("print.tiling.set", &json!({"origin": [1e9, 2]})).is_err());
}
+2
View File
@@ -72,6 +72,8 @@ fn five_colours_reduce_to_two() {
assert_eq!(run(&mut s, "recolor.colors", json!({}))["colors"].as_array().unwrap().len(), 5);
// Without a count every colour is its own row.
assert_eq!(run(&mut s, "recolor.reduce", json!({}))["map"].as_array().unwrap().len(), 5);
// A count larger than the number of colors also keeps every color in its own row. A 32-bit build read 2^32 + 2 as 2.
assert_eq!(run(&mut s, "recolor.reduce", json!({"colors": (1u64 << 32) + 2}))["map"].as_array().unwrap().len(), 5);
}
#[test]
+3
View File
@@ -140,6 +140,9 @@ fn divide_two_by_three_and_by_size() {
assert_eq!(hs, [25.0, 25.0, 10.0]);
assert!(s.execute("object.slice.divide", &json!({"rows": 0})).is_err());
assert!(s.execute("object.slice.divide", &json!({"columns": 5000})).is_err());
// A 32-bit build read 2^32 + 2 as 2, passed the check and went on to list 2^32 + 3 cuts.
let e = s.execute("object.slice.divide", &json!({"rows": (1u64 << 32) + 2})).unwrap_err().to_string();
assert!(e.contains("divide into 1 to"), "{e}");
assert!(s.execute("object.slice.divide", &json!({})).is_err(), "nothing to divide by");
}
+10
View File
@@ -102,6 +102,16 @@ fn move_reorders_and_undoes_in_one_step() {
assert!(s.execute("graphicStyle.move", &json!({"name": first})).is_err());
}
/// A graphic style moved to place 2^32 goes last. A 32-bit build read the place as 0 and moved it
/// to the front.
#[test]
fn move_past_u32_goes_last() {
let mut s = session();
let second = names(&s)[1].clone();
run(&mut s, "graphicStyle.move", json!({"name": second, "to": 1u64 << 32}));
assert_eq!(names(&s).last().unwrap(), &second);
}
#[test]
fn override_character_color_clears_the_run_fill() {
let mut s = session();
+4
View File
@@ -250,6 +250,10 @@ fn select_same_appearance_attribute() {
run(&mut s, "select.set", json!({"ids": [a.0]}));
run(&mut s, "select.same.appearanceAttribute", json!({"item": 1}));
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b, c]);
// An item past the last one leaves the shared effect. A 32-bit build read 2^32 + 1 as item 1.
run(&mut s, "select.set", json!({"ids": [a.0]}));
run(&mut s, "select.same.appearanceAttribute", json!({"item": (1u64 << 32) + 1}));
assert_eq!(s.doc().unwrap().selection.objects, vec![a, b]);
}
#[test]
+11
View File
@@ -56,6 +56,17 @@ fn move_between_groups_is_one_undo_step() {
assert_eq!(names(&s, None).last().map(String::as_str), Some("White"));
}
/// A swatch or color group moved to place 2^32 goes last. A 32-bit build read the place as 0 and
/// moved it to the front.
#[test]
fn move_past_u32_goes_last() {
let mut s = session();
run(&mut s, "swatch.move", json!({"name": "White", "to": 1u64 << 32}));
assert_eq!(names(&s, None).last().map(String::as_str), Some("White"));
run(&mut s, "swatch.move", json!({"name": "Grays", "to": 1u64 << 32}));
assert_eq!(doc(&s).swatch_groups.last().map(|g| g.name.as_str()), Some("Grays"));
}
#[test]
fn move_reorders_groups_and_refuses_bad_moves() {
let mut s = session();
+16
View File
@@ -55,6 +55,9 @@ fn edit_range_replaces_and_undoes() {
assert_eq!(obj(&s, id).plain_text(), "Hello!");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(obj(&s, id).plain_text(), "Hello there");
// An offset of 2^32 is past the end too. A 32-bit build read it as 0 and inserted at the start.
s.execute("text.editRange", &json!({"id": id.0, "start": 1u64 << 32, "end": 1u64 << 32, "insert": "!"})).unwrap();
assert_eq!(obj(&s, id).plain_text(), "Hello there!");
assert!(s.execute("text.editRange", &json!({"start": 0})).is_err(), "id required");
let rect = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap();
assert!(s.execute("text.editRange", &json!({"id": rect["id"], "insert": "x"})).is_err(), "not text");
@@ -84,6 +87,17 @@ fn set_range_style_splits_runs() {
assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "size": -1})).is_err());
}
/// A character style applied from byte 2^32 to 2^32 + 5 styles no character, as a range past the
/// end does. A 32-bit build read the range as bytes 0 to 5 and styled "Hello".
#[test]
fn char_style_range_past_u32_styles_nothing() {
let mut s = session();
let id = text(&mut s, "Hello world");
s.execute("charStyle.new", &json!({"name": "Big", "attrs": {"size": 40.0}})).unwrap();
s.execute("charStyle.apply", &json!({"name": "Big", "id": id.0, "start": 1u64 << 32, "end": (1u64 << 32) + 5})).unwrap();
assert!(obj(&s, id).runs.iter().all(|r| r.style.size == 20.0 && r.style.style_name.is_none()), "{:?}", obj(&s, id).runs);
}
#[test]
fn set_range_style_defaults_to_all_text_and_get_range() {
let mut s = session();
@@ -147,6 +161,8 @@ fn touch_type_moves_one_character_and_leaves_the_others_where_they_are() {
assert!(near(moved_by(&before, &glyph_pens(&s, id), 0), (0.0, 2.0)));
assert!(s.execute("text.touchType", &json!({"id": id.0, "start": 1})).is_err(), "nothing to change");
assert!(s.execute("text.touchType", &json!({"id": id.0, "start": 9, "dy": 1})).is_err(), "no character there");
// A 32-bit build read 2^32 + 1 as character 1.
assert!(s.execute("text.touchType", &json!({"id": id.0, "start": (1u64 << 32) + 1, "dy": 1})).is_err(), "no character there");
}
/// Raising, scaling or rotating a character leaves it and the characters after it where they
+10
View File
@@ -942,6 +942,16 @@ missing one does.
`` `columns` must be a number, not "4" ``. Each message names the key that was given, and the command then changes
nothing.
A whole number that is larger than the build's `usize` can hold (4294967295 on a 32-bit build, such as the
`wasm32-unknown-unknown` web build) counts as larger than any index, place, byte offset or count. An index, such as
a `subpath`, `anchor`, `segment`, document `index`, `artboard`, appearance `item` or `index`, gradient `stop` or
freeform point `index`, names nothing, and the command does what it does for any other index past the end: most
commands fail and change nothing, and `appearance.removeItem` removes nothing. A place (`to` in `swatch.move`,
`artboard.reorder`, `graphicStyle.move`, `appearance.moveItem`, `appearance.duplicateItem` and `effect.move`) puts
the item last. A text `start` or `end` is past the end of the text. `object.slice.divide` refuses such a `rows` or
`columns`, `recolor.reduce` keeps each color in its own row for such a `colors`, and `layer.setProps` refuses such a
`color`. `object.setBounds` reads a `reference` past 8 as 8, as `object.transformEach` does.
The blend, envelope and gradient mesh commands refuse a string or another value that is not a number with the same
message, as in `` `steps` must be a number, not "8" ``, and change nothing: `steps`, `distance` and `value` in
`object.blend.make` and `object.blend.options`; `bend`, `h`, `v`, `horizontalDistortion` and `verticalDistortion` in