Robustness: VectorCraft never crashes — no panics in shipped code

Shipped code no longer unwraps, expects or panics: every one of the 124
unwrap()/expect()/panic!/unreachable! sites outside tests now handles the
failure (Result/Option, ?, let-else, defaults) and reports errors to the user.

- Lints: workspace clippy denies unwrap_used, expect_used, panic, todo,
  unimplemented and unreachable; clippy.toml allows them in tests, and
  integration tests, examples and testkit opt out explicitly.
- Safety net (vectorcraft_engine::guard): a panic in a top-level command rolls
  the document back and returns EngineError::Internal; a panicking tool is
  reset; the MCP server answers with an internal error and keeps serving; the
  UI frame and each control request are guarded.
- APIs that hid failures now return them: Rendered::to_png/to_jpeg/to_webp
  return Result (JPEG/WebP errors used to write empty files),
  FontDb::face returns Option, Region::top returns Option.
- Fuzzing: engine/tests/import_fuzz.rs feeds garbage, hostile and mutated SVG
  and PDF through import → render → export (30k cases locally, no panics).
- Docs: AGENTS.md/CLAUDE.md rule, docs/development.md "Robustness" section,
  README pointer and a PR template checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-05 06:28:59 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent 0f241aac9f
commit 3e323bfa2a
89 changed files with 1041 additions and 303 deletions
+33 -20
View File
@@ -188,8 +188,9 @@ pub fn remove_redundant_points(path: &PathData, tolerance: f64) -> PathData {
segs.remove(i1);
if sp.closed && i1 == 0 {
// The merged segment now starts the loop.
let last = segs.pop().expect("non-empty");
segs.insert(0, last);
if let Some(last) = segs.pop() {
segs.insert(0, last);
}
}
changed = true;
break;
@@ -287,18 +288,22 @@ pub fn join(paths: &[PathData], tolerance: f64) -> PathData {
}
}
}
if open.len() == 1 {
let mut sp = open.pop().expect("one");
let n = sp.anchors.len();
if n > 2 && sp.anchors[0].p.distance(sp.anchors[n - 1].p) <= tolerance {
let last = sp.anchors.pop().expect("n > 2");
let f = &mut sp.anchors[0];
if open.len() == 1
&& let Some(mut sp) = open.pop()
{
let ends_meet = sp.anchors.len() > 2 && end_point(&sp, false).zip(end_point(&sp, true)).is_some_and(|(a, b)| a.distance(b) <= tolerance);
if ends_meet
&& let Some(last) = sp.anchors.pop()
&& let Some(f) = sp.anchors.first_mut()
{
*f = Anchor::with_handles(f.p, last.h_in, f.h_out);
} else {
let f = &mut sp.anchors[0];
f.h_in = f.p;
let l = sp.anchors.last_mut().expect("n >= 2");
l.h_out = l.p;
if let Some(f) = sp.anchors.first_mut() {
f.h_in = f.p;
}
if let Some(l) = sp.anchors.last_mut() {
l.h_out = l.p;
}
}
sp.closed = true;
closed.push(sp);
@@ -311,8 +316,7 @@ pub fn join(paths: &[PathData], tolerance: f64) -> PathData {
for j in (i + 1)..open.len() {
for ei in [false, true] {
for ej in [false, true] {
let pi = if ei { open[i].anchors.last() } else { open[i].anchors.first() }.expect("len>=2").p;
let pj = if ej { open[j].anchors.last() } else { open[j].anchors.first() }.expect("len>=2").p;
let (Some(pi), Some(pj)) = (end_point(&open[i], ei), end_point(&open[j], ej)) else { continue };
let d = pi.distance(pj);
if d < best.0 {
best = (d, i, j, ei, ej);
@@ -330,14 +334,18 @@ pub fn join(paths: &[PathData], tolerance: f64) -> PathData {
if ej {
b.reverse();
}
if d <= tolerance {
if d <= tolerance && !b.anchors.is_empty() {
let first = b.anchors.remove(0);
let l = a.anchors.last_mut().expect("len>=2");
*l = Anchor::with_handles(l.p, l.h_in, first.h_out);
if let Some(l) = a.anchors.last_mut() {
*l = Anchor::with_handles(l.p, l.h_in, first.h_out);
}
} else {
let l = a.anchors.last_mut().expect("len>=2");
l.h_out = l.p;
b.anchors[0].h_in = b.anchors[0].p;
if let Some(l) = a.anchors.last_mut() {
l.h_out = l.p;
}
if let Some(f) = b.anchors.first_mut() {
f.h_in = f.p;
}
}
a.anchors.extend(b.anchors);
}
@@ -369,3 +377,8 @@ pub fn split_into_grid(rect: Rect, rows: usize, cols: usize, gutter: f64) -> Vec
}
out
}
/// The first (`last == false`) or last anchor point of a subpath.
fn end_point(sp: &SubPath, last: bool) -> Option<Point> {
if last { sp.anchors.last() } else { sp.anchors.first() }.map(|a| a.p)
}
+1 -2
View File
@@ -112,8 +112,7 @@ fn bern(t: f64) -> [f64; 4] {
/// Solve the 2x2 normal equations for the handle lengths along the fixed tangents.
fn generate(pts: &[Point], u: &[f64], t0: Vec2, t1: Vec2) -> CubicBez {
let p0 = pts[0];
let p3 = *pts.last().unwrap();
let (Some(&p0), Some(&p3)) = (pts.first(), pts.last()) else { return CubicBez::new(Point::ZERO, Point::ZERO, Point::ZERO, Point::ZERO) };
let dir_in = -t1; // handle at the end points backwards
let (mut c00, mut c01, mut c11, mut x0, mut x1) = (0.0, 0.0, 0.0, 0.0, 0.0);
for (p, &t) in pts.iter().zip(u) {
+13 -9
View File
@@ -57,9 +57,9 @@ pub struct Region {
}
impl Region {
/// Front-most covering shape.
pub fn top(&self) -> usize {
*self.sources.last().expect("regions are never uncovered")
/// Front-most covering shape (`None` only for an uncovered face, which regions never are).
pub fn top(&self) -> Option<usize> {
self.sources.last().copied()
}
/// Does the face contain `p`?
pub fn contains(&self, p: Point) -> bool {
@@ -87,23 +87,27 @@ pub fn pathfinder(op: PathfinderOp, shapes: &[Shape]) -> Vec<Shape> {
return Vec::new();
}
let front_key = shapes[n - 1].key;
if op == PathfinderOp::Unite {
let unite = || {
let v: Vec<(&PathData, FillRule)> = shapes.iter().map(|s| (&s.path, s.rule)).collect();
return one(unite_all(&v), front_key);
one(unite_all(&v), front_key)
};
// Unite doesn't need the arrangement.
if op == PathfinderOp::Unite {
return unite();
}
let Some(arr) = arrangement(shapes) else { return Vec::new() };
let p = &arr.tidy;
match op {
PathfinderOp::Unite => unreachable!(),
PathfinderOp::Unite => unite(),
PathfinderOp::MinusFront => one(all_contours_to_path(&arr.contours(|m| m[0] && !m[1..].iter().any(|&b| b)), p), shapes[0].key),
PathfinderOp::MinusBack => one(all_contours_to_path(&arr.contours(|m| m[n - 1] && !m[..n - 1].iter().any(|&b| b)), p), front_key),
PathfinderOp::Intersect => one(all_contours_to_path(&arr.contours(|m| m.iter().all(|&b| b)), p), front_key),
PathfinderOp::Exclude => one(all_contours_to_path(&arr.contours(|m| m.iter().filter(|&&b| b).count() % 2 == 1), p), front_key),
PathfinderOp::Divide => regions_of(&arr)
.into_iter()
.map(|r| {
let key = shapes[r.top()].key;
Shape::new(r.path, FillRule::NonZero, key)
.filter_map(|r| {
let key = shapes.get(r.top()?)?.key;
Some(Shape::new(r.path, FillRule::NonZero, key))
})
.collect(),
PathfinderOp::Trim => (0..n).flat_map(|i| one(all_contours_to_path(&arr.contours(|m| topmost(m) == Some(i)), p), shapes[i].key)).collect(),