Bound typed counts and values that could crash the editor

- ARRAYRECT, ARRAYPOLAR, ARRAYPATH, ARRAY3D and COPY Array reject item
  counts above 100,000 (checked as a product of every dimension) and keep
  the prompt open. Unbounded counts allocated one transform per item and
  aborted on allocation failure. Stored defaults are clamped as well.
- EXPLODE leaves a MINSERT above the same limit intact instead of
  materializing every cell.
- Typed lengths and angles reject non-finite values and magnitudes above
  1e15; SCALE rejects a non-finite factor.
- Zero-radius FILLET compares candidate intersections with total_cmp and
  rejects non-finite results; partial_cmp().unwrap() panicked on NaN.
- SOLID falls back to a minimal kernel tolerance when its corners' extent
  overflows, since Tolerance::new panics on infinity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christopher OverbeckandClaude Opus 5 committed 2026-09-17 10:33:18 -05:00
1 parent 38aa2ee34d
commit a3d65e2fc8
7 files changed
+223 -15

No files matched your search

+85 -8
View File
@@ -45,6 +45,27 @@ pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
),
];
// ── Item limit ─────────────────────────────────────────────────────────────
/// Most items one array may create, including the source. Every item becomes
/// a transform and then a copy of each selected object, so an unbounded typed
/// count exhausts memory before anything is drawn.
pub(crate) const MAX_ARRAY_ITEMS: u64 = 100_000;
/// Whether an array of `counts` (rows, columns, levels, ...) stays in bounds.
pub(crate) fn array_items_within_limit(counts: &[u64]) -> bool {
counts
.iter()
.try_fold(1u64, |total, &count| total.checked_mul(count))
.is_some_and(|total| total <= MAX_ARRAY_ITEMS)
}
pub(crate) fn array_limit_error() -> CmdResult {
CmdResult::ReportError(
crate::tf!("Array size exceeds the limit of {} items.", MAX_ARRAY_ITEMS).into_owned(),
)
}
// ── Rectangular Array ──────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy)]
@@ -72,8 +93,8 @@ impl ArrayRectCommand {
handles,
wire_models,
step: RectStep::Rows,
default_rows: defaults::get_array_rows() as u32,
default_cols: defaults::get_array_cols() as u32,
default_rows: (defaults::get_array_rows() as u32).clamp(1, MAX_ARRAY_ITEMS as u32),
default_cols: (defaults::get_array_cols() as u32).clamp(1, MAX_ARRAY_ITEMS as u32),
default_row_sp: defaults::get_array_row_sp(),
default_col_sp: defaults::get_array_col_sp(),
plane: WorkingPlane::default(),
@@ -154,6 +175,9 @@ impl CadCommand for ArrayRectCommand {
self.default_rows
} else {
let v = t.parse::<u32>().unwrap_or(self.default_rows).max(1);
if !array_items_within_limit(&[v as u64]) {
return Some(array_limit_error());
}
defaults::set_array_rows(v as f64);
self.default_rows = v;
v
@@ -165,11 +189,15 @@ impl CadCommand for ArrayRectCommand {
let cols = if t.is_empty() {
self.default_cols
} else {
let v = t.parse::<u32>().unwrap_or(self.default_cols).max(1);
defaults::set_array_cols(v as f64);
self.default_cols = v;
v
t.parse::<u32>().unwrap_or(self.default_cols).max(1)
};
if !array_items_within_limit(&[rows as u64, cols as u64]) {
return Some(array_limit_error());
}
if !t.is_empty() {
defaults::set_array_cols(cols as f64);
self.default_cols = cols;
}
self.step = RectStep::RowSp { rows, cols };
None
}
@@ -222,6 +250,10 @@ impl CadCommand for ArrayRectCommand {
}
RectStep::ColSp { rows, cols, row_sp } => (rows, cols, row_sp, self.default_col_sp),
};
// Defaults are only bounded one count at a time.
if !array_items_within_limit(&[rows as u64, cols as u64]) {
return vec![];
}
Self::build_transforms(rows, cols, row_sp, col_sp, self.plane)
.iter()
.flat_map(|t| {
@@ -274,7 +306,7 @@ impl ArrayPolarCommand {
handles,
wire_models,
step: PolarStep::Center,
default_count: defaults::get_array_p_count() as u32,
default_count: (defaults::get_array_p_count() as u32).clamp(2, MAX_ARRAY_ITEMS as u32),
default_angle: defaults::get_array_p_angle(),
plane: WorkingPlane::default(),
}
@@ -330,6 +362,9 @@ impl CadCommand for ArrayPolarCommand {
self.default_count
} else {
let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
if !array_items_within_limit(&[v as u64]) {
return Some(array_limit_error());
}
defaults::set_array_p_count(v as f64);
self.default_count = v;
v
@@ -461,7 +496,7 @@ impl ArrayPathCommand {
wire_models,
all_entities,
step: PathStep::SelectPath,
default_count: defaults::get_array_path_count() as u32,
default_count: (defaults::get_array_path_count() as u32).clamp(2, MAX_ARRAY_ITEMS as u32),
pick_pt: DVec3::ZERO,
}
}
@@ -686,6 +721,9 @@ impl CadCommand for ArrayPathCommand {
self.default_count
} else {
let v = t.parse::<u32>().unwrap_or(self.default_count).max(2);
if !array_items_within_limit(&[v as u64]) {
return Some(array_limit_error());
}
defaults::set_array_path_count(v as f64);
self.default_count = v;
v
@@ -891,6 +929,9 @@ impl CadCommand for Array3DCommand {
} else {
t.parse::<u32>().unwrap_or(2).max(1)
};
if !array_items_within_limit(&[v as u64]) {
return Some(array_limit_error());
}
self.step = Array3DStep::Cols { rows: v };
Some(CmdResult::NeedPoint)
}
@@ -900,6 +941,9 @@ impl CadCommand for Array3DCommand {
} else {
t.parse::<u32>().unwrap_or(2).max(1)
};
if !array_items_within_limit(&[rows as u64, v as u64]) {
return Some(array_limit_error());
}
self.step = Array3DStep::Levels { rows, cols: v };
Some(CmdResult::NeedPoint)
}
@@ -909,6 +953,9 @@ impl CadCommand for Array3DCommand {
} else {
t.parse::<u32>().unwrap_or(2).max(1)
};
if !array_items_within_limit(&[rows as u64, cols as u64, v as u64]) {
return Some(array_limit_error());
}
self.step = Array3DStep::RowSp {
rows,
cols,
@@ -987,3 +1034,33 @@ impl CadCommand for Array3DCommand {
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["ARRAY3D", "3DARRAY"] });
#[cfg(test)]
mod item_limit_tests {
use super::*;
#[test]
fn item_limit_counts_every_dimension_without_overflow() {
assert!(array_items_within_limit(&[MAX_ARRAY_ITEMS]));
assert!(!array_items_within_limit(&[MAX_ARRAY_ITEMS + 1]));
assert!(!array_items_within_limit(&[1_000, 1_000]));
assert!(!array_items_within_limit(&[u32::MAX as u64, u32::MAX as u64, u32::MAX as u64]));
}
#[test]
fn oversized_counts_are_rejected_at_the_prompt() {
let mut rect = ArrayRectCommand::new(vec![Handle::new(1)], Vec::new());
assert!(rect.on_text_input("100000").is_none());
assert!(matches!(rect.on_text_input("100000"), Some(CmdResult::ReportError(_))));
assert!(matches!(rect.step, RectStep::Cols { rows: 100_000 }));
let mut polar = ArrayPolarCommand::new(vec![Handle::new(1)], Vec::new());
polar.step = PolarStep::Count { center: DVec3::ZERO };
assert!(matches!(polar.on_text_input("4294967295"), Some(CmdResult::ReportError(_))));
let mut cube = Array3DCommand::new(vec![Handle::new(1)]);
assert!(matches!(cube.on_text_input("300"), Some(CmdResult::NeedPoint)));
assert!(matches!(cube.on_text_input("300"), Some(CmdResult::NeedPoint)));
assert!(matches!(cube.on_text_input("2"), Some(CmdResult::ReportError(_))));
}
}