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
+7 -10
View File
@@ -141,19 +141,16 @@ fn with_attrs<T: Serialize + DeserializeOwned>(base: &T, attrs: &Map<String, Val
}
/// `cur` after its style changed from `old` to `new`: attributes still equal to the old style's
/// value (or not set by it) take the new value; overrides stay.
fn restyle<T: Serialize + DeserializeOwned>(cur: &T, old: &Map<String, Value>, new: &Map<String, Value>) -> T {
let mut m = match serde_json::to_value(cur) {
Ok(Value::Object(m)) => m,
_ => return serde_json::from_value(serde_json::to_value(cur).unwrap_or_default()).unwrap_or_else(|_| unreachable!()),
};
/// value (or not set by it) take the new value; overrides stay. Unchanged if the style doesn't
/// round-trip through JSON.
fn restyle<T: Serialize + DeserializeOwned + Clone>(cur: &T, old: &Map<String, Value>, new: &Map<String, Value>) -> T {
let Ok(Value::Object(mut m)) = serde_json::to_value(cur) else { return cur.clone() };
for (k, v) in new {
if old.get(k).is_none_or(|o| m.get(k) == Some(o)) {
m.insert(k.clone(), v.clone());
}
}
serde_json::from_value(Value::Object(m.clone()))
.unwrap_or_else(|_| serde_json::from_value(serde_json::to_value(cur).unwrap_or_default()).expect("round trip"))
serde_json::from_value(Value::Object(m)).unwrap_or_else(|_| cur.clone())
}
/// Every text object id in the document.
@@ -276,7 +273,7 @@ fn new(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
let base = if kind == Kind::Char { "Character Style" } else { "Paragraph Style" };
let name = match str_param(p, "name") {
Some(n) => n.to_string(),
None => (1..).map(|i| format!("{base} {i}")).find(|n| kind.attrs(d, n).is_none()).expect("free name"),
None => (1..).map(|i| format!("{base} {i}")).find(|n| kind.attrs(d, n).is_none()).unwrap_or_else(|| base.to_string()),
};
if name.trim().is_empty() || kind.attrs(d, &name).is_some() {
return Err(bad(&c, format!("a style named `{name}` already exists")));
@@ -387,7 +384,7 @@ fn duplicate(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
let copy = (1..)
.map(|i| if i == 1 { format!("{name} copy") } else { format!("{name} copy {i}") })
.find(|n| kind.attrs(d, n).is_none())
.expect("free name");
.unwrap_or_else(|| format!("{name} copy"));
new(s, &json!({ "name": copy, "attrs": attrs }), kind)
}