Merge pull request #16 from storytold/pr/never-crash-small

Never crash: filters, fonts, js, optimize and a11y deny the panic lints
This commit is contained in:
Brandon Thomas 2026-10-04 14:46:43 -04:00 committed by GitHub
commit 414060a3ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 44 additions and 29 deletions

View File

@ -141,7 +141,7 @@ fn content_bbox(doc: &Document, page: &Dict, ids: &HashSet<i64>) -> 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<i64>) -> 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),

View File

@ -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;

View File

@ -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<u8> {
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")
}

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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<Host>;
fn host(ctx: &Context) -> &Shared {
ctx.get_data::<Shared>().expect("host state is installed before scripts run")
/// The host state, installed before scripts run.
fn host(ctx: &Context) -> JsResult<&Shared> {
ctx.get_data::<Shared>().ok_or_else(|| error("the script host is not set up"))
}
fn s(v: &str) -> JsValue {
@ -267,17 +270,17 @@ fn colour(c: &Option<Vec<String>>, ctx: &mut Context) -> JsValue {
fn field_index(ctx: &Context, name: &JsString) -> JsResult<usize> {
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<FieldState> {
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<JsValue> {
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<JsValue> {
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<JsValue> {
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<JsValue> {
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<JsValue> {
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<JsV
}
_ => 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<JsValue> {
}
fn num_fields(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
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<JsValue> {
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<JsValue> {
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<String> {
fn alert(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
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<JsValue> {
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<JsValue> {
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<JsValue> {
fn println(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
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<JsValue> {
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);

View File

@ -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" => {

View File

@ -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;