From 65e52aa7b99ad20ebe1bbeae84aeb654fc28bcd6 Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:22:41 +0300 Subject: [PATCH] engine: long digit strings no longer overflow object.opacityKeys opacity_from_digits multiplied and added on every character of its input string before the one-or-two-digit check, so "9999999999" overflowed u32 on the way (panic in debug builds, wrap in release). The scan now steps n with checked_mul/checked_add and stops (None) at the first character the number cannot take, so a long digit string is refused like any other bad input. Test: opacity_digits (engine keymap) covers the ten-digit string and the command's BadParams refusal. --- crates/engine/src/cmd/edit.rs | 3 ++- crates/engine/src/keymap_tests.rs | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 57ba9fb0..3068ffb9 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -803,7 +803,8 @@ pub fn opacity_from_digits(digits: &str) -> Option { let mut n = 0u32; let mut count = 0; for c in digits.chars() { - n = n * 10 + c.to_digit(10)?; + // Input-derived: a number that outgrows `u32` stops the scan (None); it never wraps. + n = n.checked_mul(10)?.checked_add(c.to_digit(10)?)?; count += 1; } match count { diff --git a/crates/engine/src/keymap_tests.rs b/crates/engine/src/keymap_tests.rs index 8f6f28c1..38775d3b 100644 --- a/crates/engine/src/keymap_tests.rs +++ b/crates/engine/src/keymap_tests.rs @@ -4,11 +4,11 @@ use serde_json::{Value, json}; -use crate::Session; use crate::cmd::edit::opacity_from_digits; use crate::keymap::{Context, GESTURES, Group, KEYS, Mods, Platform, display, lookup, matches, normalize, parse, primary}; use crate::params::id_of; use crate::tools::ToolId; +use crate::{EngineError, Session}; use figcraft_doc::NodeId; const PLATFORMS: [Platform; 2] = [Platform::Mac, Platform::Other]; @@ -186,6 +186,7 @@ fn opacity_digits() { assert_eq!(opacity_from_digits("00"), Some(0.0)); assert_eq!(opacity_from_digits(""), None); assert_eq!(opacity_from_digits("123"), None); + assert_eq!(opacity_from_digits("9999999999"), None, "ten digits must not overflow u32"); assert_eq!(opacity_from_digits("4x"), None); assert_eq!(opacity_from_digits("٣"), None, "ASCII digits only"); @@ -201,6 +202,10 @@ fn opacity_digits() { ex(&mut s, "edit.undo", json!({})); assert_eq!(op(&s, a), 0.3, "one undo step"); assert!(s.execute("object.opacityKeys", &json!({"digits": "123"})).is_err()); + assert!( + matches!(s.execute("object.opacityKeys", &json!({"digits": "9999999999"})), Err(EngineError::BadParams { .. })), + "an overflowing digit string is a parameter error, not a panic" + ); assert!(s.execute("object.opacityKeys", &json!({})).is_err()); ex(&mut s, "select.none", json!({})); assert!(s.execute("object.opacityKeys", &json!({"digits": "1"})).is_err(), "nothing selected");