From 9fc3354fe6af3a37967b4adba21e4d5b159a994d Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 11:55:59 -0400 Subject: [PATCH 1/2] Never crash: filters, fonts, js, optimize and a11y deny the panic lints - js: `host()` returns a script error instead of panicking when the host state is missing. - optimize, a11y: the graphics-state stack is read with `last`/`last_mut` instead of `expect`. - fonts: the standard-14 ASCII map is built from `u8`, which can't fail. - filters: zlib encoding into a `Vec` keeps the standard's narrow, documented `expect` exception. Co-Authored-By: Claude Opus 5.5 --- crates/a11y/src/alt.rs | 8 ++++--- crates/a11y/src/lib.rs | 2 ++ crates/filters/src/flate.rs | 3 ++- crates/filters/src/lib.rs | 1 + crates/fonts/src/lib.rs | 2 ++ crates/fonts/src/pdf.rs | 2 +- crates/js/src/lib.rs | 45 +++++++++++++++++++---------------- crates/optimize/src/images.rs | 8 ++++--- crates/optimize/src/lib.rs | 2 ++ 9 files changed, 44 insertions(+), 29 deletions(-) diff --git a/crates/a11y/src/alt.rs b/crates/a11y/src/alt.rs index 5b13ee86..653a3a5a 100644 --- a/crates/a11y/src/alt.rs +++ b/crates/a11y/src/alt.rs @@ -141,7 +141,7 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet) -> Option<[f64; let mut path: Vec<(f64, f64)> = Vec::new(); for op in printcraft_content::parse(&page_content(doc, page)).ops { let inside = marks.iter().any(|m| *m); - let top = *ctm.last().expect("never empty"); + let top = ctm.last().copied().unwrap_or(Matrix([1.0, 0.0, 0.0, 1.0, 0.0, 0.0])); match op.op.as_slice() { b"q" => ctm.push(top), b"Q" => { @@ -150,8 +150,10 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet) -> Option<[f64; } } b"cm" => { - if let Some(m) = Matrix::from_operands(&op.operands) { - *ctm.last_mut().expect("never empty") = m.then(&top); + if let Some(m) = Matrix::from_operands(&op.operands) + && let Some(t) = ctm.last_mut() + { + *t = m.then(&top); } } b"BMC" => marks.push(false), diff --git a/crates/a11y/src/lib.rs b/crates/a11y/src/lib.rs index 51cd220f..281fa1c5 100644 --- a/crates/a11y/src/lib.rs +++ b/crates/a11y/src/lib.rs @@ -6,6 +6,8 @@ //! accessibility report. Rules read the document only; the fixes that need edits (language, //! title, tab order) are applied by the engine. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + use std::collections::BTreeSet; use printcraft_cos::Document; diff --git a/crates/filters/src/flate.rs b/crates/filters/src/flate.rs index 7bb8f53f..5aa6443f 100644 --- a/crates/filters/src/flate.rs +++ b/crates/filters/src/flate.rs @@ -67,9 +67,10 @@ fn inflate_raw(input: &[u8], max: usize) -> Step { } /// zlib at level 6. +// The documented never-crash exception: writing into a `Vec` cannot fail. +#[allow(clippy::expect_used)] pub(crate) fn encode(data: &[u8]) -> Vec { let mut e = flate2::write::ZlibEncoder::new(Vec::with_capacity(data.len() / 2 + 64), Compression::new(6)); - // Writing into a Vec cannot fail. e.write_all(data).expect("in-memory write"); e.finish().expect("in-memory write") } diff --git a/crates/filters/src/lib.rs b/crates/filters/src/lib.rs index ce91500e..6ec4a5de 100644 --- a/crates/filters/src/lib.rs +++ b/crates/filters/src/lib.rs @@ -11,6 +11,7 @@ //! Every decoder is bounded by a caller-supplied `max_output` (decompression-bomb //! defence) and never panics on malformed input. #![forbid(unsafe_code)] +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] mod ascii85; mod asciihex; diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index e9f07157..0d72c01c 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -3,6 +3,8 @@ //! See the README: the metrics are approximations by character class (no vendor metrics files //! are bundled). The full font subsystem lands in M2.2/M7. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + mod encodings; pub mod pdf; mod script; diff --git a/crates/fonts/src/pdf.rs b/crates/fonts/src/pdf.rs index 6c67b35d..a242f97d 100644 --- a/crates/fonts/src/pdf.rs +++ b/crates/fonts/src/pdf.rs @@ -138,7 +138,7 @@ impl Metrics { ascent: 0.9, descent: -0.25, composite: false, - unicode: (32..127u32).map(|c| (c, char::from_u32(c).expect("ascii").to_string())).collect(), + unicode: (32..127u8).map(|c| (u32::from(c), char::from(c).to_string())).collect(), base_font: "Helvetica".into(), subset: false, code_len: 1, diff --git a/crates/js/src/lib.rs b/crates/js/src/lib.rs index eedb26e9..1de46e91 100644 --- a/crates/js/src/lib.rs +++ b/crates/js/src/lib.rs @@ -21,6 +21,8 @@ //! are bounded. Side effects (alerts, field changes, resets, printing, navigation) are returned //! in an [`Outcome`] for the caller to apply. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + use std::cell::RefCell; use std::collections::BTreeSet; @@ -200,8 +202,9 @@ struct Host { type Shared = RefCell; -fn host(ctx: &Context) -> &Shared { - ctx.get_data::().expect("host state is installed before scripts run") +/// The host state, installed before scripts run. +fn host(ctx: &Context) -> JsResult<&Shared> { + ctx.get_data::().ok_or_else(|| error("the script host is not set up")) } fn s(v: &str) -> JsValue { @@ -267,17 +270,17 @@ fn colour(c: &Option>, ctx: &mut Context) -> JsValue { fn field_index(ctx: &Context, name: &JsString) -> JsResult { let n = name.to_std_string_escaped(); - host(ctx).borrow().fields.iter().position(|f| f.name == n).ok_or_else(|| error(format!("no field named {n}"))) + host(ctx)?.borrow().fields.iter().position(|f| f.name == n).ok_or_else(|| error(format!("no field named {n}"))) } fn read(ctx: &Context, name: &JsString) -> JsResult { let i = field_index(ctx, name)?; - Ok(host(ctx).borrow().fields[i].clone()) + Ok(host(ctx)?.borrow().fields[i].clone()) } fn write(ctx: &Context, name: &JsString, change: impl FnOnce(&mut FieldState)) -> JsResult<()> { let i = field_index(ctx, name)?; - let mut h = host(ctx).borrow_mut(); + let mut h = host(ctx)?.borrow_mut(); change(&mut h.fields[i]); h.changed.insert(i); Ok(()) @@ -428,7 +431,7 @@ fn field_object(ctx: &mut Context, name: &str) -> JsObject { Ok(JsValue::undefined()) } fn set_focus(_: &JsValue, _: &[JsValue], name: &JsString, c: &mut Context) -> JsResult { - host(c).borrow_mut().requests.push(Request::Focus(name.to_std_string_escaped())); + host(c)?.borrow_mut().requests.push(Request::Focus(name.to_std_string_escaped())); Ok(JsValue::undefined()) } @@ -464,30 +467,30 @@ fn field_object(ctx: &mut Context, name: &str) -> JsObject { fn get_field(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let name = text(&arg(args, 0), ctx)?; - let exists = host(ctx).borrow().fields.iter().any(|f| f.name == name); + let exists = host(ctx)?.borrow().fields.iter().any(|f| f.name == name); if exists { return Ok(field_object(ctx, &name).into()); } // A parent name (e.g. "total" for "total.0"): its first kid stands in, as for groups. let prefix = format!("{name}."); - let kid = host(ctx).borrow().fields.iter().find(|f| f.name.starts_with(&prefix)).map(|f| f.name.clone()); + let kid = host(ctx)?.borrow().fields.iter().find(|f| f.name.starts_with(&prefix)).map(|f| f.name.clone()); Ok(kid.map_or(JsValue::null(), |k| field_object(ctx, &k).into())) } fn get_nth_field_name(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let i = arg(args, 0).to_number(ctx)?; - let name = host(ctx).borrow().fields.get(i.max(0.0) as usize).map(|f| f.name.clone()); + let name = host(ctx)?.borrow().fields.get(i.max(0.0) as usize).map(|f| f.name.clone()); Ok(name.map_or(JsValue::null(), |n| s(&n))) } fn reset_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let names = strings(&arg(args, 0), ctx)?; - host(ctx).borrow_mut().requests.push(Request::Reset(names)); + host(ctx)?.borrow_mut().requests.push(Request::Reset(names)); Ok(JsValue::undefined()) } fn print(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { - host(ctx).borrow_mut().requests.push(Request::Print); + host(ctx)?.borrow_mut().requests.push(Request::Print); Ok(JsValue::undefined()) } @@ -500,7 +503,7 @@ fn submit_form(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult text(&a, ctx)?, }; - host(ctx).borrow_mut().requests.push(Request::Submit(url)); + host(ctx)?.borrow_mut().requests.push(Request::Submit(url)); Ok(JsValue::undefined()) } @@ -509,16 +512,16 @@ fn noop(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { } fn num_fields(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { - Ok(JsValue::from(host(ctx).borrow().fields.len() as f64)) + Ok(JsValue::from(host(ctx)?.borrow().fields.len() as f64)) } fn page_num_get(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { - Ok(JsValue::from(host(ctx).borrow().doc.page as f64)) + Ok(JsValue::from(host(ctx)?.borrow().doc.page as f64)) } fn page_num_set(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let n = arg(args, 0).to_number(ctx)?; - let mut h = host(ctx).borrow_mut(); + let mut h = host(ctx)?.borrow_mut(); let last = h.doc.num_pages.saturating_sub(1); let p = (n.max(0.0) as usize).min(last); h.doc.page = p; @@ -541,19 +544,19 @@ fn message(v: &JsValue, key: &str, ctx: &mut Context) -> JsResult { fn alert(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let m = message(&arg(args, 0), "cMsg", ctx)?; - host(ctx).borrow_mut().alerts.push(m); + host(ctx)?.borrow_mut().alerts.push(m); // The OK button. Ok(JsValue::from(1)) } fn beep(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { - host(ctx).borrow_mut().requests.push(Request::Beep); + host(ctx)?.borrow_mut().requests.push(Request::Beep); Ok(JsValue::undefined()) } fn launch_url(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let u = message(&arg(args, 0), "cURL", ctx)?; - host(ctx).borrow_mut().requests.push(Request::LaunchUrl(u)); + host(ctx)?.borrow_mut().requests.push(Request::LaunchUrl(u)); Ok(JsValue::undefined()) } @@ -564,12 +567,12 @@ fn response(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { fn println(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { let m = text(&arg(args, 0), ctx)?; - host(ctx).borrow_mut().console.push(m); + host(ctx)?.borrow_mut().console.push(m); Ok(JsValue::undefined()) } fn console_clear(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult { - host(ctx).borrow_mut().console.clear(); + host(ctx)?.borrow_mut().console.clear(); Ok(JsValue::undefined()) } @@ -889,7 +892,7 @@ fn install(ctx: &mut Context) -> JsResult<()> { let pg = function(ctx, NativeFunction::from_fn_ptr(page_num_get)); let ps = function(ctx, NativeFunction::from_fn_ptr(page_num_set)); let (file, pages, info) = { - let h = host(ctx).borrow(); + let h = host(ctx)?.borrow(); (h.doc.file_name.clone(), h.doc.num_pages, h.doc.info.clone()) }; let mut io = ObjectInitializer::new(ctx); diff --git a/crates/optimize/src/images.rs b/crates/optimize/src/images.rs index 7b6a275f..1b224124 100644 --- a/crates/optimize/src/images.rs +++ b/crates/optimize/src/images.rs @@ -50,7 +50,7 @@ fn walk(doc: &Document, data: &[u8], resources: &Dict, ctm: Matrix, depth: usize let xobjects = resources.get(b"XObject").map(|x| doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default(); let mut stack = vec![ctm]; for op in printcraft_content::parse(data).ops { - let top = *stack.last().expect("never empty"); + let top = stack.last().copied().unwrap_or(ctm); match op.op.as_slice() { b"q" => stack.push(top), b"Q" => { @@ -59,8 +59,10 @@ fn walk(doc: &Document, data: &[u8], resources: &Dict, ctm: Matrix, depth: usize } } b"cm" => { - if let Some(m) = Matrix::from_operands(&op.operands) { - *stack.last_mut().expect("never empty") = m.then(&top); + if let Some(m) = Matrix::from_operands(&op.operands) + && let Some(t) = stack.last_mut() + { + *t = m.then(&top); } } b"Do" => { diff --git a/crates/optimize/src/lib.rs b/crates/optimize/src/lib.rs index 85e189df..3b0607ca 100644 --- a/crates/optimize/src/lib.rs +++ b/crates/optimize/src/lib.rs @@ -15,6 +15,8 @@ //! fields are discarded by `printcraft-redact`'s Remove Hidden Information, which the engine //! runs alongside. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] + mod audit; mod images; mod links; From cfe84da3518a3ebc2141b9d7137e6a71c6807c3b Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 13:35:00 -0400 Subject: [PATCH 2/2] Root clippy.toml: tests may unwrap, expect, panic and index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same file as the steering PR (#11), so this PR stands on its own. The crate-level `#![deny(clippy::unwrap_used, …)]` applies to production code only. Co-Authored-By: Claude Opus 5.5 --- clippy.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 clippy.toml diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..4b6c79f5 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,4 @@ +allow-unwrap-in-tests = true +allow-expect-in-tests = true +allow-panic-in-tests = true +allow-indexing-slicing-in-tests = true