From a047c6ba0983464647953169e9a3e34237dc8ff2 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 23 Oct 2026 23:28:18 -0400 Subject: [PATCH] chore: resolve SonarCloud maintainability issues (#4356) * chore: resolve SonarCloud maintainability issues * fix(packaging): use verify_sha256 helper function * style: cargo fmt * fix: restore wildcard imports and fix method-reference type errors Revert the glob-import expansions (they missed SomIndices and left unused imports), revert char::is_ascii_digit where the iterator yields char by value, and pass the content slice to prune_doomed_xobjects as &[Vec]. Co-Authored-By: Claude Opus 5.5 * Keep XObject decodability check without binding content in interp --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Dylan Taylor --- apps/pdfcraft-cli/src/main.rs | 4 ++-- crates/a11y/src/content.rs | 21 ++++++++-------- crates/a11y/src/tests.rs | 2 +- crates/annot/src/lib.rs | 4 ++-- crates/automation/src/batch.rs | 4 ++-- crates/automation/src/lib.rs | 2 +- crates/create/src/lib.rs | 2 +- crates/edit/src/text_regions.rs | 6 ++--- crates/engine/src/lib.rs | 6 ++--- crates/engine/src/ocr.rs | 2 +- crates/engine/src/text_fonts.rs | 2 +- crates/engine/src/xfa.rs | 2 +- crates/export/src/lib.rs | 4 ++-- crates/fonts/src/std14.rs | 2 +- crates/forms/src/author.rs | 2 +- crates/forms/src/calc_order.rs | 18 ++++++++------ crates/forms/src/detect.rs | 2 +- crates/forms/src/table.rs | 11 ++++++--- crates/js/src/formcalc.rs | 4 ++-- crates/js/src/xfa.rs | 2 +- crates/organize/src/tests.rs | 2 +- crates/platform/src/staging.rs | 2 +- crates/preflight/src/xmp.rs | 4 ++-- crates/redact/src/interp.rs | 25 +++++++++++-------- crates/redact/src/patterns.rs | 2 +- crates/redact/src/tags.rs | 2 +- crates/render/examples/page_timing.rs | 2 +- crates/render/src/raster.rs | 6 ++--- crates/render/src/raster/work_queue.rs | 2 +- crates/render/src/text.rs | 10 ++++---- crates/sign/src/cms.rs | 33 +++++++++++++++----------- crates/ui-egui/src/files.rs | 31 ++++++++++++------------ crates/ui-egui/src/protect.rs | 4 ++-- crates/ui-egui/src/recovery.rs | 6 ++--- crates/ui-egui/tests/it/animate.rs | 2 +- crates/ui-egui/tests/it/recovery.rs | 4 +++- crates/ui-egui/tests/it/xfa.rs | 2 +- crates/ui-egui/tests/perf.rs | 2 +- crates/xfa/src/data.rs | 7 ++---- crates/xfa/src/text.rs | 2 +- packaging/linux/package.sh | 14 +++++++---- xtask/src/assets.rs | 2 +- xtask/src/layers.rs | 2 +- 43 files changed, 146 insertions(+), 124 deletions(-) diff --git a/apps/pdfcraft-cli/src/main.rs b/apps/pdfcraft-cli/src/main.rs index 14500a384..d0f23242d 100644 --- a/apps/pdfcraft-cli/src/main.rs +++ b/apps/pdfcraft-cli/src/main.rs @@ -644,7 +644,7 @@ fn render(args: &[String]) -> Result<(), CliError> { let dpi = parse_dpi(flag(args, "--dpi").unwrap_or("96"))?; let out = flag(args, "--out").ok_or("render: missing --out (.png, .jpg, .tif or .pam)")?; // The file is what its name says (#248): a `.png` used to get a netpbm PAM stream. - let format = match Path::new(out).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase).as_deref() { + let format = match Path::new(out).extension().and_then(std::ffi::OsStr::to_str).map(str::to_ascii_lowercase).as_deref() { Some("png") => Some(ImageFormat::Png), Some("jpg" | "jpeg") => Some(ImageFormat::Jpeg { quality: 90 }), Some("tif" | "tiff") => Some(ImageFormat::Tiff), @@ -779,7 +779,7 @@ fn collect(paths: &[&str]) -> Vec { if let Ok(rd) = std::fs::read_dir(&p) { stack.extend(rd.flatten().map(|e| e.path())); } - } else if p.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("pdf")) { + } else if p.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("pdf")) { out.push(p); } } diff --git a/crates/a11y/src/content.rs b/crates/a11y/src/content.rs index fd5f0d305..d548c57f9 100644 --- a/crates/a11y/src/content.rs +++ b/crates/a11y/src/content.rs @@ -53,6 +53,15 @@ struct Walk<'a> { budget: DecodeBudget, } +fn bdc_marks(doc: &Document, op: &pdfcraft_content::Op, props: &Dict) -> bool { + op.name(0) == Some(b"Artifact") + || match op.operands.get(1) { + Some(Object::Dict(d)) => d.contains(b"MCID"), + Some(Object::Name(n)) => props.get(n).map(|x| doc.resolve(x)).and_then(|x| x.as_dict().map(|d| d.contains(b"MCID"))).unwrap_or(false), + _ => false, + } +} + impl Walk<'_> { fn sub(&self, res: &Dict, key: &[u8]) -> Dict { res.get(key).map(|x| self.doc.resolve(x)).and_then(|x| x.as_dict().cloned()).unwrap_or_default() @@ -68,17 +77,7 @@ impl Walk<'_> { let inside = covered || stack.last().copied().unwrap_or(false); match op.op.as_slice() { b"BMC" => stack.push(inside || op.name(0) == Some(b"Artifact")), - b"BDC" => { - let marks = op.name(0) == Some(b"Artifact") - || match op.operands.get(1) { - Some(Object::Dict(d)) => d.contains(b"MCID"), - Some(Object::Name(n)) => { - props.get(n).map(|x| self.doc.resolve(x)).and_then(|x| x.as_dict().map(|d| d.contains(b"MCID"))).unwrap_or(false) - } - _ => false, - }; - stack.push(inside || marks); - } + b"BDC" => stack.push(inside || bdc_marks(self.doc, &op, &props)), b"EMC" => { stack.pop(); } diff --git a/crates/a11y/src/tests.rs b/crates/a11y/src/tests.rs index be46ef61c..a6dcdba19 100644 --- a/crates/a11y/src/tests.rs +++ b/crates/a11y/src/tests.rs @@ -380,7 +380,7 @@ fn table_doc(th: &str, extra: &[&str], kids: &str) -> Document { th.to_string(), "<< /S /TD >>".to_string(), ]; - objs.extend(extra.iter().map(|s| s.to_string())); + objs.extend(extra.iter().map(ToString::to_string)); pdf(&objs, "") } diff --git a/crates/annot/src/lib.rs b/crates/annot/src/lib.rs index 720d39cd8..09a081c43 100644 --- a/crates/annot/src/lib.rs +++ b/crates/annot/src/lib.rs @@ -1010,7 +1010,7 @@ fn rect_for(shape: &Shape, style: &Style) -> Result<[f64; 4], AnnotError> { grow(bounds([*from, *to].into_iter()).unwrap_or_default(), pad + 1.0) } Shape::Ink { strokes } | Shape::Signature { strokes } => { - if strokes.iter().all(|s| s.is_empty()) || !strokes.iter().flatten().all(|p| finite(p)) { + if strokes.iter().all(Vec::is_empty) || !strokes.iter().flatten().all(|p| finite(p)) { return Err(bad("drawing (no points)")); } // Strokes of three or more points are drawn as curves, which stay within their @@ -1221,7 +1221,7 @@ pub fn add_annotation(doc: &mut Document, new: &NewAnnotation, meta: &Meta) -> R d.set(b"PCOutline".to_vec(), Object::Array(list)); } Shape::CustomStamp { name, picture, image, size, .. } => { - let clean: String = name.chars().filter(|c| c.is_ascii_alphanumeric()).collect(); + let clean: String = name.chars().filter(char::is_ascii_alphanumeric).collect(); d.set(b"Name".to_vec(), Object::name(&format!("PCCustom{clean}"))); d.set(b"PCPicture".to_vec(), Object::Ref(*picture)); d.set(b"PCPictureImage".to_vec(), Object::Bool(*image)); diff --git a/crates/automation/src/batch.rs b/crates/automation/src/batch.rs index 99310832f..47cf714ec 100644 --- a/crates/automation/src/batch.rs +++ b/crates/automation/src/batch.rs @@ -58,7 +58,7 @@ impl<'a> Recipe<'a> { } fn accepts(&self, path: &Path) -> bool { - let extension = path.extension().and_then(|s| s.to_str()).unwrap_or("").to_ascii_lowercase(); + let extension = path.extension().and_then(std::ffi::OsStr::to_str).unwrap_or("").to_ascii_lowercase(); extension == "pdf" || (self.steps.contains(&"convert") && pdfcraft_engine::CONVERTIBLE.contains(&extension.as_str())) } @@ -70,7 +70,7 @@ impl<'a> Recipe<'a> { if input.len() as u64 > MAX_INPUT { return Err(failed("input exceeds 512 MiB")); } - let name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| failed("input name is not UTF-8"))?; + let name = path.file_name().and_then(std::ffi::OsStr::to_str).ok_or_else(|| failed("input name is not UTF-8"))?; let mut bytes = Arc::new(input); let mut log = Vec::new(); for step in &self.steps { diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index 1dae412e4..d2114ac7b 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -2700,7 +2700,7 @@ fn extract_parallel(bytes: &Arc>, password: Option>, pages: &[u } }; let config = RenderConfig { password, ..Default::default() }; - let workers = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |n| n.get()).min(8) }; + let workers = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get).min(8) }; // Small jobs aren't worth a second parse of the document. if workers == 1 || pages.len() < 8 { let mut r = PageRenderer::new(bytes.clone(), config); diff --git a/crates/create/src/lib.rs b/crates/create/src/lib.rs index 7d1fdfc36..02bccd5fa 100644 --- a/crates/create/src/lib.rs +++ b/crates/create/src/lib.rs @@ -269,7 +269,7 @@ fn jpeg(name: &str, bytes: &[u8]) -> Result { 0xEE if seg.starts_with(b"Adobe") => adobe = true, // APP2 ICC profile, possibly split over several chunks. 0xE2 => { - if let Some(([seq, count], data)) = seg.strip_prefix(b"ICC_PROFILE\0").and_then(|r| r.split_first_chunk::<2>()) { + if let Some(([seq, count], data)) = seg.strip_prefix(b"ICC_PROFILE\0").and_then(<[u8]>::split_first_chunk::<2>) { icc_chunks.push((*seq, *count, data)); } } diff --git a/crates/edit/src/text_regions.rs b/crates/edit/src/text_regions.rs index a77559494..71ccbc683 100644 --- a/crates/edit/src/text_regions.rs +++ b/crates/edit/src/text_regions.rs @@ -166,8 +166,8 @@ fn opens_with_marker(text: &str) -> bool { if !first.is_ascii_digit() && !first.is_ascii_alphabetic() { return false; } - let digits = text.chars().take_while(|c| c.is_ascii_digit()).count(); - let letters = text.chars().take_while(|c| c.is_ascii_alphabetic()).count(); + let digits = text.chars().take_while(char::is_ascii_digit).count(); + let letters = text.chars().take_while(char::is_ascii_alphabetic).count(); // Both runs are ASCII characters, so their length in characters is their length in bytes: // the slice below starts on a character boundary. let rest = if digits > 0 { @@ -178,7 +178,7 @@ fn opens_with_marker(text: &str) -> bool { return false; }; let mut after = rest.chars(); - matches!(after.next(), Some('.' | ')')) && after.next().is_none_or(|c| c.is_whitespace()) + matches!(after.next(), Some('.' | ')')) && after.next().is_none_or(char::is_whitespace) } /// A row that opens with a list marker opens its own item: joining it to the row above is what diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 68fc076b9..86fc9dce4 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1050,7 +1050,7 @@ fn shown_under_redactions( } fn render_threads() -> usize { - std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4).clamp(2, 8) - 1 + std::thread::available_parallelism().map(std::num::NonZeroUsize::get).unwrap_or(4).clamp(2, 8) - 1 } /// Write `layers`' visibility into the default optional-content configuration of `cos` (a copy @@ -1311,7 +1311,7 @@ impl Protection { (Some(a), Some(b)) if a == b => return bad("the open password and the permissions password must be different"), _ => {} } - if [&self.open_password, &self.permissions_password].into_iter().flatten().any(|p| p.is_empty()) { + if [&self.open_password, &self.permissions_password].into_iter().flatten().any(String::is_empty) { return bad("passwords can't be empty"); } if self.algorithm != pdfcraft_cos::Algorithm::Aes256 @@ -4692,7 +4692,7 @@ impl Session { pub fn combine_field_conflicts(&self, sources: &[CombineSource], passwords: &[Option<&str>]) -> Vec> { let docs: Vec> = sources.iter().enumerate().map(|(i, (n, b, _))| open_source_with(n, b, passwords.get(i).copied().flatten()).ok()).collect(); - let refs: Vec<&pdfcraft_cos::Document> = docs.iter().filter_map(|d| d.as_ref()).collect(); + let refs: Vec<&pdfcraft_cos::Document> = docs.iter().filter_map(Option::as_ref).collect(); let mut opened = pdfcraft_organize::combine_field_conflicts(&refs).into_iter(); // One preview per source, in step with it: a file that cannot be opened has none. docs.iter().map(|d| if d.is_some() { opened.next().unwrap_or_default() } else { Default::default() }).collect() diff --git a/crates/engine/src/ocr.rs b/crates/engine/src/ocr.rs index dcd4142d7..c73788902 100644 --- a/crates/engine/src/ocr.rs +++ b/crates/engine/src/ocr.rs @@ -75,7 +75,7 @@ impl OcrPage { /// The recogniser, loaded once (it takes a moment) and shared. pub fn engine() -> Result, String> { static OCR: Mutex>> = Mutex::new(None); - let mut slot = OCR.lock().unwrap_or_else(|e| e.into_inner()); + let mut slot = OCR.lock().unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(o) = slot.as_ref() { return Ok(o.clone()); } diff --git a/crates/engine/src/text_fonts.rs b/crates/engine/src/text_fonts.rs index f4945f756..859bc1a6f 100644 --- a/crates/engine/src/text_fonts.rs +++ b/crates/engine/src/text_fonts.rs @@ -74,5 +74,5 @@ pub fn discover(roots: &[PathBuf]) -> Vec { files } fn font_extension(path: &Path) -> bool { - path.extension().and_then(|e| e.to_str()).is_some_and(|e| e.eq_ignore_ascii_case("ttf") || e.eq_ignore_ascii_case("otf")) + path.extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("ttf") || e.eq_ignore_ascii_case("otf")) } diff --git a/crates/engine/src/xfa.rs b/crates/engine/src/xfa.rs index 6ce76548f..6166426bb 100644 --- a/crates/engine/src/xfa.rs +++ b/crates/engine/src/xfa.rs @@ -532,7 +532,7 @@ impl<'a, 'b> Runner<'a, 'b> { } else { Some(by_som.iter().filter(|(som, _)| names.iter().any(|n| som_matches(som, n))).map(|(_, f)| f.clone()).collect()) }; - if listed.as_ref().is_some_and(|l| l.is_empty()) { + if listed.as_ref().is_some_and(Vec::is_empty) { return Ok(()); } pdfcraft_forms::reset(self.doc, listed.as_deref()).map_err(|e| e.to_string())?; diff --git a/crates/export/src/lib.rs b/crates/export/src/lib.rs index 8be472e98..c8dc65951 100644 --- a/crates/export/src/lib.rs +++ b/crates/export/src/lib.rs @@ -246,7 +246,7 @@ pub(crate) fn tables(blocks: &[Block]) -> (Vec, Vec) { used.sort_unstable(); used.dedup(); let mut edges: Vec = used.iter().filter_map(|&i| col_of.get(i).copied().flatten()).filter_map(|ci| cols.get(ci)).map(|c| c.0).collect(); - edges.sort_by(|a, b| a.total_cmp(b)); + edges.sort_by(f64::total_cmp); edges.dedup_by(|a, b| (*a - *b).abs() <= COL_TOL); let ncols = edges.len(); let ok = (ncols >= 3 || (ncols == 2 && run.len() >= 3)) && ncols <= MAX_COLS; @@ -313,7 +313,7 @@ pub(crate) fn tables(blocks: &[Block]) -> (Vec
, Vec) { let run_edges = |run: &[Vec<(usize, usize)>]| -> Vec { let mut es: Vec = run.iter().flatten().filter_map(|(_, i)| col_of.get(*i).copied().flatten()).filter_map(|ci| cols.get(ci)).map(|c| c.0).collect(); - es.sort_by(|a, b| a.total_cmp(b)); + es.sort_by(f64::total_cmp); es.dedup_by(|a, b| (*a - *b).abs() <= COL_TOL); es }; diff --git a/crates/fonts/src/std14.rs b/crates/fonts/src/std14.rs index aae657501..13bb5b771 100644 --- a/crates/fonts/src/std14.rs +++ b/crates/fonts/src/std14.rs @@ -59,7 +59,7 @@ impl Std14 { /// report that called it a standard-14 name would be telling the reader something untrue. pub fn recognise(base: &str) -> Option<(Self, Match)> { let base = base.rsplit('+').next().unwrap_or(base); - let flat: String = base.chars().filter(|c| c.is_ascii_alphanumeric()).map(|c| c.to_ascii_lowercase()).collect(); + let flat: String = base.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase()).collect(); let has = |w: &str| flat.contains(w); let (bold, italic) = (has("bold"), has("italic") || has("oblique")); // Two bools index a four-element array, so the subscript is provably 0..=3. diff --git a/crates/forms/src/author.rs b/crates/forms/src/author.rs index c3424050d..95cd9d9a8 100644 --- a/crates/forms/src/author.rs +++ b/crates/forms/src/author.rs @@ -980,7 +980,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result]) -> Vec { continue; } if low[v] == index[v] { - while let Some(w) = stack.pop() { - on_stack[w] = false; - component[w] = count; - if w == v { - break; - } - } + pop_component(&mut stack, &mut on_stack, &mut component, v, count); count += 1; } if let Some(&(parent, _)) = work.last() { @@ -260,6 +254,16 @@ fn components(links: &[Vec]) -> Vec { component } +fn pop_component(stack: &mut Vec, on_stack: &mut [bool], component: &mut [usize], v: usize, count: usize) { + while let Some(w) = stack.pop() { + on_stack[w] = false; + component[w] = count; + if w == v { + break; + } + } +} + /// Fix order: every node after the nodes it reads in other components, the earliest ready node /// first, so an order that is already right stays as it is and otherwise only what must move /// moves. Reads within a component (a loop) are left out: no order satisfies them. diff --git a/crates/forms/src/detect.rs b/crates/forms/src/detect.rs index 8c980c09f..386671f43 100644 --- a/crates/forms/src/detect.rs +++ b/crates/forms/src/detect.rs @@ -307,7 +307,7 @@ fn label_for(words: &[Word], rect: [f64; 4], right_of: bool) -> String { .iter() .filter(|w| w.rect[1] >= rect[3] - 1.0 && w.rect[1] - rect[3] < 24.0 && w.rect[0] < rect[2] && w.rect[2] > rect[0] - 4.0) .collect(); - let Some(line) = above.iter().map(|w| mid(w.rect)).min_by(|a, b| a.total_cmp(b)) else { return String::new() }; + let Some(line) = above.iter().map(|w| mid(w.rect)).min_by(f64::total_cmp) else { return String::new() }; let mut on: Vec<&&Word> = above.iter().filter(|w| (mid(w.rect) - line).abs() < 3.0).collect(); on.sort_by(|a, b| a.rect[0].total_cmp(&b.rect[0])); clean(&on.iter().map(|w| w.text.as_str()).collect::>().join(" ")) diff --git a/crates/forms/src/table.rs b/crates/forms/src/table.rs index 23ad18c8b..ae565052f 100644 --- a/crates/forms/src/table.rs +++ b/crates/forms/src/table.rs @@ -269,23 +269,28 @@ pub fn cell_name(pattern: &str, row: usize, column: usize, seq: usize) -> String if numbered { return out; } + substitute_isolated_n(&out, &row_number) +} + +fn substitute_isolated_n(out: &str, row_number: &str) -> String { if out == "N" || out == "n" { - return row_number; + return row_number.to_string(); } let chars: Vec = out.chars().collect(); let mut named = String::with_capacity(out.len() + row_number.len()); + let mut numbered = false; for (i, &ch) in chars.iter().enumerate() { let after_separator = i.checked_sub(1).and_then(|p| chars.get(p)).is_some_and(|c| SEPARATORS.contains(c)); let before_separator = chars.get(i + 1).is_none_or(|c| SEPARATORS.contains(c)); if matches!(ch, 'N' | 'n') && after_separator && before_separator { - named.push_str(&row_number); + named.push_str(row_number); numbered = true; } else { named.push(ch); } } if !numbered { - named.push_str(&row_number); + named.push_str(row_number); } named } diff --git a/crates/js/src/formcalc.rs b/crates/js/src/formcalc.rs index 2348ce220..4f31d04d3 100644 --- a/crates/js/src/formcalc.rs +++ b/crates/js/src/formcalc.rs @@ -123,7 +123,7 @@ fn lex(src: &str) -> Result, String> { } out.push(Token { tok: Tok::Str(s), line }); } - c if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(|d| d.is_ascii_digit())) => { + c if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(char::is_ascii_digit)) => { let start = i; while i < chars.len() && (chars[i].is_ascii_digit() || (chars[i] == '.' && chars.get(i + 1) != Some(&'.'))) { i += 1; @@ -760,7 +760,7 @@ fn parse_num(s: &str) -> Option { } fn is_numeric_text(s: &str) -> bool { - s.trim().parse::().is_ok_and(|v| v.is_finite()) + s.trim().parse::().is_ok_and(f64::is_finite) } // ── the evaluator ─────────────────────────────────────────────────────────────────────────── diff --git a/crates/js/src/xfa.rs b/crates/js/src/xfa.rs index 8a60e8ba9..45abeeb02 100644 --- a/crates/js/src/xfa.rs +++ b/crates/js/src/xfa.rs @@ -1416,7 +1416,7 @@ fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current: usize) -> "DataView", "Intl", ]; - let mut defined: Vec = RESERVED.iter().map(|s| s.to_string()).collect(); + let mut defined: Vec = RESERVED.iter().map(ToString::to_string).collect(); for scope in scopes { let kids: Vec<(usize, String, bool)> = { let h = host(ctx)?.borrow(); diff --git a/crates/organize/src/tests.rs b/crates/organize/src/tests.rs index f0bb3738d..b5c448882 100644 --- a/crates/organize/src/tests.rs +++ b/crates/organize/src/tests.rs @@ -1692,7 +1692,7 @@ fn outline_with(items: &[&str]) -> Document { format!("<< /Type /Outlines /First 5 0 R /Count {} >>", items.len()), "<< /Type /Page /Parent 2 0 R >>".into(), ]; - b.extend(items.iter().map(|s| s.to_string())); + b.extend(items.iter().map(ToString::to_string)); open(build(&b, "/Root 1 0 R")) } diff --git a/crates/platform/src/staging.rs b/crates/platform/src/staging.rs index f6a98c4fe..c0e8f990b 100644 --- a/crates/platform/src/staging.rs +++ b/crates/platform/src/staging.rs @@ -145,7 +145,7 @@ pub fn write_atomically_with( return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "the file is read-only; save a copy with Save As")); } let dir = path.parent().filter(|d| !d.as_os_str().is_empty()).unwrap_or(Path::new(".")); - let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("save"); + let name = path.file_name().and_then(std::ffi::OsStr::to_str).unwrap_or("save"); // A file that is replaced keeps its permissions (#1790) and group. Unix only: Windows is as before. #[cfg(unix)] let keep = std::fs::metadata(path).ok(); diff --git a/crates/preflight/src/xmp.rs b/crates/preflight/src/xmp.rs index 0da4869cc..0aaab12b2 100644 --- a/crates/preflight/src/xmp.rs +++ b/crates/preflight/src/xmp.rs @@ -151,7 +151,7 @@ fn esc(s: &str) -> String { /// A PDF date (`D:YYYYMMDDHHmmSSOHH'mm'`) as an XMP (ISO 8601) date. pub fn iso_date(pdf: &str) -> Option { let d = pdf.trim().trim_start_matches("D:"); - let digits: String = d.chars().take_while(|c| c.is_ascii_digit()).collect(); + let digits: String = d.chars().take_while(char::is_ascii_digit).collect(); if digits.len() < 4 { return None; } @@ -161,7 +161,7 @@ pub fn iso_date(pdf: &str) -> Option { let tz = match rest.chars().next() { Some('Z') | None => "Z".to_string(), Some(sign @ ('+' | '-')) => { - let t: String = rest[1..].chars().filter(|c| c.is_ascii_digit()).collect(); + let t: String = rest[1..].chars().filter(char::is_ascii_digit).collect(); format!("{sign}{}:{}", t.get(0..2).unwrap_or("00"), t.get(2..4).unwrap_or("00")) } _ => "Z".to_string(), diff --git a/crates/redact/src/interp.rs b/crates/redact/src/interp.rs index fcd2ebe83..5ac6c24ea 100644 --- a/crates/redact/src/interp.rs +++ b/crates/redact/src/interp.rs @@ -443,8 +443,8 @@ fn content_draws(doc: &Document, content: &[Vec], xo: &Dict, name: &[u8], de if depth >= MAX_DEPTH { return None; } - let Ok(data) = s.decoded() else { return None }; - match content_draws(doc, std::slice::from_ref(&data), xo, name, depth + 1, seen) { + let Ok(_data) = s.decoded() else { return None }; + match content_draws(doc, content, xo, name, depth + 1, seen) { Some(false) => {} other => return other, } @@ -616,14 +616,7 @@ fn unlink_from_forms(doc: &mut Document, holders: &[ObjRef], doomed: &HashSet {} - Some(false) => pruned |= xo.remove(&name).is_some(), - None => return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect shared form resources".into() }), - } - } + let mut pruned = prune_doomed_xobjects(doc, std::slice::from_ref(&data), &mut xo, &doomed_names)?; if pruned { res.set(b"XObject".to_vec(), Object::Dict(xo)); } @@ -637,6 +630,18 @@ fn unlink_from_forms(doc: &mut Document, holders: &[ObjRef], doomed: &HashSet], xo: &mut Dict, doomed_names: &[Vec]) -> Result { + let mut pruned = false; + for name in doomed_names { + match content_draws(doc, content, xo, name, 0, &mut HashSet::new()) { + Some(true) => {} + Some(false) => pruned |= xo.remove(name).is_some(), + None => return Err(CosError::Syntax { offset: 0, detail: "redaction could not fully inspect shared form resources".into() }), + } + } + Ok(pruned) +} + /// The clip that hides everything inside each region: one even-odd clip per region (a big /// rectangle with the region cut out), in the current user space. Returns `None` when the CTM /// can't be inverted (nothing is painted then anyway). diff --git a/crates/redact/src/patterns.rs b/crates/redact/src/patterns.rs index f707f179a..88e85fb46 100644 --- a/crates/redact/src/patterns.rs +++ b/crates/redact/src/patterns.rs @@ -346,7 +346,7 @@ fn date(s: &[char], i: usize) -> Option { // Numeric: m/d/yy(yy), m-d-yyyy, d.m.yyyy, yyyy-mm-dd. if let Some(a) = number(s, i, 1, 4) { let sep = s.get(i + a).copied(); - if matches!(sep, Some('/' | '-' | '.')) + if matches!(sep, Some('-'..='/')) && let Some(b) = number(s, i + a + 1, 1, 2) && s.get(i + a + 1 + b).copied() == sep && let Some(c) = number(s, i + a + b + 2, if a == 4 { 1 } else { 2 }, if a == 4 { 2 } else { 4 }) diff --git a/crates/redact/src/tags.rs b/crates/redact/src/tags.rs index 60c158b18..961f016e9 100644 --- a/crates/redact/src/tags.rs +++ b/crates/redact/src/tags.rs @@ -108,7 +108,7 @@ pub(crate) fn touched(doc: &Document, resources: &Dict, before: &[u8], after: &[ let mut changed = HashSet::new(); let mut empty = HashSet::new(); let mut budget = MAX_COMPARED; - let len = |ranges: &[Range]| ranges.iter().map(|r| r.len()).fold(0usize, usize::saturating_add); + let len = |ranges: &[Range]| ranges.iter().map(Range::len).fold(0usize, usize::saturating_add); for (id, (old, _)) in &b.ids { match a.ids.get(id) { Some((new, draws)) => { diff --git a/crates/render/examples/page_timing.rs b/crates/render/examples/page_timing.rs index f1d143a21..5fc60c6ab 100644 --- a/crates/render/examples/page_timing.rs +++ b/crates/render/examples/page_timing.rs @@ -51,7 +51,7 @@ fn main() -> Result<(), Box> { }; let threads: usize = match args.get(4) { Some(s) => s.parse::()?.max(1), - None => std::thread::available_parallelism().map_or(4, |n| n.get()).clamp(2, 8) - 1, + None => std::thread::available_parallelism().map_or(4, std::num::NonZeroUsize::get).clamp(2, 8) - 1, }; let bytes = Arc::new(std::fs::read(path)?); diff --git a/crates/render/src/raster.rs b/crates/render/src/raster.rs index 0f43b0636..64e6c5596 100644 --- a/crates/render/src/raster.rs +++ b/crates/render/src/raster.rs @@ -488,7 +488,7 @@ fn parse(bytes: &Arc>, password: Option<&str>) -> Option { } pub(crate) fn panic_message(p: &Box) -> String { - p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::().cloned()).unwrap_or_else(|| "unknown panic".into()) + p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::().cloned()).unwrap_or_else(|| "unknown panic".into()) } /// How long one page may render before the pool gives up on it (the watchdog). @@ -587,7 +587,7 @@ struct Shared { } fn lock(m: &Mutex) -> std::sync::MutexGuard<'_, T> { - m.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) + m.lock().unwrap_or_else(std::sync::PoisonError::into_inner) } #[cfg(not(target_arch = "wasm32"))] @@ -666,7 +666,7 @@ impl PoolParser { // Drop never joins a parser. A waiting worker must still notice // its pool's disconnected wake channel while another thread is // stuck in initialization and cannot signal this condition. - state = self.ready.wait_timeout(state, std::time::Duration::from_millis(50)).unwrap_or_else(|p| p.into_inner()).0; + state = self.ready.wait_timeout(state, std::time::Duration::from_millis(50)).unwrap_or_else(std::sync::PoisonError::into_inner).0; } ParserState::Empty => { *state = ParserState::Loading; diff --git a/crates/render/src/raster/work_queue.rs b/crates/render/src/raster/work_queue.rs index eb1d05b45..ff20bbf1f 100644 --- a/crates/render/src/raster/work_queue.rs +++ b/crates/render/src/raster/work_queue.rs @@ -220,7 +220,7 @@ impl WorkQueue { } // The parser validity token can change independently of this queue. Bound the // wait so failed generations are released even while the consumer is paused. - state = self.changed.wait_timeout(state, Duration::from_millis(25)).unwrap_or_else(|p| p.into_inner()).0; + state = self.changed.wait_timeout(state, Duration::from_millis(25)).unwrap_or_else(std::sync::PoisonError::into_inner).0; } } diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index c9e89248f..1e522b83f 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -499,11 +499,11 @@ impl TextDevice { return; } let Some(glyph) = merge(&drawn, text.clone()) else { - self.glyphs.extend(drawn.into_iter().filter(|d| !d.2.text.chars().all(|c| c.is_control()))); + self.glyphs.extend(drawn.into_iter().filter(|d| !d.2.text.chars().all(char::is_control))); return; }; - let originals = drawn.iter().filter(|d| !d.2.text.chars().all(|c| c.is_control())).map(|d| (d.2.text.clone(), d.2.rect)).collect(); - let at = if text.chars().all(|c| c.is_control()) { + let originals = drawn.iter().filter(|d| !d.2.text.chars().all(char::is_control)).map(|d| (d.2.text.clone(), d.2.rect)).collect(); + let at = if text.chars().all(char::is_control) { None } else { self.glyphs.push(glyph); @@ -594,7 +594,7 @@ impl TextDevice { let text = replacement.to_owned(); #[cfg(test)] REDRAW_TEXT_BYTES.with(|c| c.set(c.get() + text.len())); - if !text.chars().all(|c| c.is_control()) { + if !text.chars().all(char::is_control) { match merge(run, text) { Some(glyph) => self.glyphs.push(glyph), None => self.glyphs.extend_from_slice(run), @@ -640,7 +640,7 @@ impl<'a> Device<'a> for TextDevice { return; } }; - if !replaced && text.chars().all(|c| c.is_control()) { + if !replaced && text.chars().all(char::is_control) { return; } // Glyph space → view space. Glyph space uses 1000 units per em for outline glyphs; the em diff --git a/crates/sign/src/cms.rs b/crates/sign/src/cms.rs index 3568cbf45..b148ea4d1 100644 --- a/crates/sign/src/cms.rs +++ b/crates/sign/src/cms.rs @@ -294,6 +294,24 @@ fn parse_signer(si: &Tlv<'_>) -> Result { } let (scheme, scheme_digest) = signature_algorithm(&next)?; let signature = it.next().ok_or_else(|| bad("signature"))?.octets("signature")?.into_owned(); + let (timestamp, timestamp_token) = parse_timestamp_token(it)?; + Ok(SignerInfo { + sid, + digest, + signed_attrs, + message_digest, + content_type, + signing_time, + signing_certificate, + scheme, + scheme_digest, + signature, + timestamp, + timestamp_token, + }) +} + +fn parse_timestamp_token<'a>(it: impl Iterator>) -> Result<(bool, Option>), SignError> { let mut timestamp = false; let mut timestamp_token = None; for t in it { @@ -309,20 +327,7 @@ fn parse_signer(si: &Tlv<'_>) -> Result { } } } - Ok(SignerInfo { - sid, - digest, - signed_attrs, - message_digest, - content_type, - signing_time, - signing_certificate, - scheme, - scheme_digest, - signature, - timestamp, - timestamp_token, - }) + Ok((timestamp, timestamp_token)) } /// Attach an RFC 3161 `signatureTimeStampToken` unsigned attribute to a detached CMS object. diff --git a/crates/ui-egui/src/files.rs b/crates/ui-egui/src/files.rs index 4a4f5333a..aa2aad02d 100644 --- a/crates/ui-egui/src/files.rs +++ b/crates/ui-egui/src/files.rs @@ -657,22 +657,21 @@ impl PdfCraftApp { } }); return; - } else { - match self.session.extract(id, &pages) { - Ok(bytes) => { - let message = if pages.len() == 1 { - tl!("Extracted 1 page").to_string() - } else { - crate::i18n::fmt(tl!("Extracted {n} pages"), &[("n", &pages.len().to_string())]) - }; - // Save As offers the tab's name, so a name typed in the dialog is kept. - let name = if renamed { format!("{base}.pdf") } else { format!("{stem} (extract).pdf") }; - self.open_created(&name, bytes, &message) - } - Err(e) => { - self.notify_fmt("Couldn't extract pages: {e}", &[("e", &e.to_string())]); - return; - } + } + match self.session.extract(id, &pages) { + Ok(bytes) => { + let message = if pages.len() == 1 { + tl!("Extracted 1 page").to_string() + } else { + crate::i18n::fmt(tl!("Extracted {n} pages"), &[("n", &pages.len().to_string())]) + }; + // Save As offers the tab's name, so a name typed in the dialog is kept. + let name = if renamed { format!("{base}.pdf") } else { format!("{stem} (extract).pdf") }; + self.open_created(&name, bytes, &message) + } + Err(e) => { + self.notify_fmt("Couldn't extract pages: {e}", &[("e", &e.to_string())]); + return; } } if opts.delete { diff --git a/crates/ui-egui/src/protect.rs b/crates/ui-egui/src/protect.rs index a4336d546..fb48c369d 100644 --- a/crates/ui-egui/src/protect.rs +++ b/crates/ui-egui/src/protect.rs @@ -56,8 +56,8 @@ impl ProtectDraft { /// A rough password strength, as Acrobat's meter shows (Weak / Medium / Strong). pub fn strength(pw: &str) -> (&'static str, Color32) { let classes = [ - pw.chars().any(|c| c.is_lowercase()), - pw.chars().any(|c| c.is_uppercase()), + pw.chars().any(char::is_lowercase), + pw.chars().any(char::is_uppercase), pw.chars().any(|c| c.is_ascii_digit()), pw.chars().any(|c| !c.is_alphanumeric()), ] diff --git a/crates/ui-egui/src/recovery.rs b/crates/ui-egui/src/recovery.rs index 17a3410d1..68d82922c 100644 --- a/crates/ui-egui/src/recovery.rs +++ b/crates/ui-egui/src/recovery.rs @@ -206,10 +206,10 @@ impl RecoveryStore { let mut out = Vec::new(); for entry in dir.flatten() { let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("pdf") || !entry.file_type().is_ok_and(|t| t.is_file()) { + if path.extension().and_then(std::ffi::OsStr::to_str) != Some("pdf") || !entry.file_type().is_ok_and(|t| t.is_file()) { continue; } - let Some(key) = path.file_stem().and_then(|s| s.to_str()) else { continue }; + let Some(key) = path.file_stem().and_then(std::ffi::OsStr::to_str) else { continue }; let meta = std::fs::read(path.with_extension("json")) .and_then(|b| serde_json::from_slice::(&b).map_err(std::io::Error::other)) .and_then(|m| if m.key == key { Ok(m) } else { Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "snapshot key mismatch")) }); @@ -266,7 +266,7 @@ impl RecoveryStore { for entry in dir.flatten() { let path = entry.path(); // Only sidecars whose PDF is gone: they hold no document content. - if path.extension().and_then(|e| e.to_str()) == Some("json") + if path.extension().and_then(std::ffi::OsStr::to_str) == Some("json") && entry.file_type().is_ok_and(|t| t.is_file()) && !path.with_extension("pdf").exists() { diff --git a/crates/ui-egui/tests/it/animate.rs b/crates/ui-egui/tests/it/animate.rs index d01bf6de1..c2ab58e92 100644 --- a/crates/ui-egui/tests/it/animate.rs +++ b/crates/ui-egui/tests/it/animate.rs @@ -18,7 +18,7 @@ fn pdf(annots: &[&str]) -> Vec { "<< /Length 0 >>\nstream\n\nendstream".into(), "<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /Length 0 >>\nstream\n\nendstream".into(), ]; - objs.extend(annots.iter().map(|o| o.to_string())); + objs.extend(annots.iter().map(ToString::to_string)); let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); for (i, o) in objs.iter().enumerate() { diff --git a/crates/ui-egui/tests/it/recovery.rs b/crates/ui-egui/tests/it/recovery.rs index 5e2ffaad7..3e3f3a593 100644 --- a/crates/ui-egui/tests/it/recovery.rs +++ b/crates/ui-egui/tests/it/recovery.rs @@ -43,7 +43,9 @@ fn store(tag: &str) -> RecoveryStore { /// Snapshot files (the PDF and its sidecar), not the running instances' lock files. fn files_in(s: &RecoveryStore) -> usize { - std::fs::read_dir(s.dir()).map(|d| d.flatten().filter(|e| e.path().extension().and_then(|x| x.to_str()) != Some("lock")).count()).unwrap_or(0) + std::fs::read_dir(s.dir()) + .map(|d| d.flatten().filter(|e| e.path().extension().and_then(std::ffi::OsStr::to_str) != Some("lock")).count()) + .unwrap_or(0) } /// A session that edits a document, autosaves, and then "crashes" (is dropped). diff --git a/crates/ui-egui/tests/it/xfa.rs b/crates/ui-egui/tests/it/xfa.rs index e66658b0e..cf0fc193b 100644 --- a/crates/ui-egui/tests/it/xfa.rs +++ b/crates/ui-egui/tests/it/xfa.rs @@ -15,7 +15,7 @@ fn pdf(acroform: &str, extra: &str, objects: &[&str]) -> Vec { "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R >>".into(), "<< /Length 0 >>\nstream\n\nendstream".into(), ]; - objs.extend(objects.iter().map(|o| o.to_string())); + objs.extend(objects.iter().map(ToString::to_string)); let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); for (i, o) in objs.iter().enumerate() { diff --git a/crates/ui-egui/tests/perf.rs b/crates/ui-egui/tests/perf.rs index 319edeffc..61d3965bd 100644 --- a/crates/ui-egui/tests/perf.rs +++ b/crates/ui-egui/tests/perf.rs @@ -18,7 +18,7 @@ fn load_average() -> Option { /// Frame budgets only mean something on a machine that isn't overcommitted: with the load /// average above twice the core count (other builds running), skip and say so. fn overloaded() -> bool { - let cores = std::thread::available_parallelism().map_or(4, |n| n.get()) as f64; + let cores = std::thread::available_parallelism().map_or(4, std::num::NonZeroUsize::get) as f64; match load_average() { Some(l) if l > 2.0 * cores => { eprintln!("PERF skipped: load average {l:.0} on {cores} cores; frame budgets need a quiet machine"); diff --git a/crates/xfa/src/data.rs b/crates/xfa/src/data.rs index 2b80d8b96..95f59a44b 100644 --- a/crates/xfa/src/data.rs +++ b/crates/xfa/src/data.rs @@ -441,11 +441,8 @@ pub fn pattern_to_iso(text: &str, pattern: &str) -> Option { "yyyy" | "yy" | "mm" | "m" | "dd" | "d" => { // Digits up to the next literal (or a fixed width). let fixed = matches!(t.as_str(), "yyyy" | "yy" | "mm" | "dd"); - let width = if fixed { - t.len() - } else { - rest.chars().take_while(|c| c.is_ascii_digit()).count().min(if t == "d" || t == "m" { 2 } else { 4 }) - }; + let width = + if fixed { t.len() } else { rest.chars().take_while(char::is_ascii_digit).count().min(if t == "d" || t == "m" { 2 } else { 4 }) }; let digits: String = rest.chars().take(width).collect(); if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { return None; diff --git a/crates/xfa/src/text.rs b/crates/xfa/src/text.rs index 7e8321268..e1f4c548b 100644 --- a/crates/xfa/src/text.rs +++ b/crates/xfa/src/text.rs @@ -213,7 +213,7 @@ fn words(runs: &[Run], base: &Face, embed: &dyn Fn(&str, usize) -> String, embed .filter_map(|c| match c { '\r' | '\n' | '\t' | '\u{2028}' | '\u{2029}' | '\u{2009}' | '\u{202F}' | '\u{3000}' => Some(' '), '\u{00AD}' | '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{FEFF}' => None, - '\u{2011}' | '\u{2010}' | '\u{2012}' | '\u{2013}' => Some('-'), + '\u{2010}'..='\u{2013}' => Some('-'), '\u{2018}' | '\u{2019}' => Some('\''), _ => Some(c), }) diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 595ca7b18..b7813920e 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -92,6 +92,12 @@ has() { case " $FORMATS " in *" $format "*) return 0 ;; *) return 1 ;; esac } +verify_sha256() { + local expected="$1" + local target="$2" + printf '%s %s\n' "$expected" "$target" | sha256sum -c - +} + # ---- .tar.gz ------------------------------------------------------------------------------------ if has tar; then mkdir -p "$WORK/tar" @@ -164,21 +170,21 @@ if has appimage; then download="$TOOL.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" - printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c - + verify_sha256 "$APPIMAGETOOL_SHA256" "$download" chmod 755 "$download" mv "$download" "$TOOL" fi - printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - + verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL" fi RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" if [[ ! -f "$RUNTIME" ]]; then download="$RUNTIME.download" curl --proto '=https' -fsSL -o "$download" \ "https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" - printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c - + verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$download" mv "$download" "$RUNTIME" fi - printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c - + verify_sha256 "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be # relative, e.g. target/agent-). OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" diff --git a/xtask/src/assets.rs b/xtask/src/assets.rs index b3393d770..ac836eae6 100644 --- a/xtask/src/assets.rs +++ b/xtask/src/assets.rs @@ -229,7 +229,7 @@ fn cargo_lock_versions(root: &Path) -> Result> { } pub fn is_asset_path(path: &str) -> bool { - Path::new(path).extension().and_then(|e| e.to_str()).is_some_and(|e| ASSET_EXTENSIONS.contains(&e.to_ascii_lowercase().as_str())) + Path::new(path).extension().and_then(std::ffi::OsStr::to_str).is_some_and(|e| ASSET_EXTENSIONS.contains(&e.to_ascii_lowercase().as_str())) } /// Every licence id in an SPDX expression (`MIT OR Apache-2.0`, `MIT AND Bitstream-Vera`). diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index a0907cb1a..8ad15733a 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -228,7 +228,7 @@ pub fn check(crates: &[Crate]) -> Vec { } } } - out.sort_by_key(|v| v.to_string()); + out.sort_by_key(ToString::to_string); out.dedup(); out }