chore: resolve SonarCloud maintainability issues

This commit is contained in:
Dylan M. Taylor
2026-10-24 02:19:25 +00:00
parent a2ec27b477
commit 2b04a06fba
51 changed files with 186 additions and 130 deletions
+2 -2
View File
@@ -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<PathBuf> {
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);
}
}
+12 -11
View File
@@ -53,6 +53,17 @@ 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 +79,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();
}
+1 -1
View File
@@ -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, "")
}
+2 -2
View File
@@ -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));
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -2700,7 +2700,7 @@ fn extract_parallel(bytes: &Arc<Vec<u8>>, password: Option<Arc<str>>, 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);
+1 -1
View File
@@ -269,7 +269,7 @@ fn jpeg(name: &str, bytes: &[u8]) -> Result<Embedded, CreateError> {
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));
}
}
+4 -1
View File
@@ -4,7 +4,10 @@
use std::cell::Cell;
use super::*;
use pdfcraft_cos::{Dict, Document, ObjRef, Object, PdfString};
use crate::EditError;
use super::{draw_laid_shaped, AddedText, GlyphError, GlyphOutline, LaidLine, Piece, ShapedCluster, ShapedScript, GLYPHS_PER_FONT};
/// Regression fingerprint for the small-item fixture: drawing bytes, font map and document
/// objects, with generated streams decoded so Flate backend selection cannot change the hash.
+7 -1
View File
@@ -1,4 +1,10 @@
use super::*;
use pdfcraft_cos::{Dict, Document, Object, Stream};
use super::{
added, add_header_footer, add_watermark, contents, is_wrap_close, page_list, place_tagged, remove_marks,
stamp, stream_bytes, stream_tag, wrapper_role, Context, EditError, HeaderFooter, MarkKind, Watermark,
ISOLATION_MAX_DEPTH, OLD_WRAP_CLOSE, WRAP_CLOSE, WRAP_OPEN,
};
fn fixture(parts: &[&[u8]]) -> Document {
let mut doc = Document::new_empty();
+13 -4
View File
@@ -1,6 +1,15 @@
//! Geometry-based regions and mixed-style rewriting of existing text.
use super::*;
use std::collections::HashMap;
use pdfcraft_cos::{Dict, Document, Object, PdfString};
use super::{
coincident_ops, content_streams, needs_type3, no_japanese_font, page_dict, reveal_ops, rewritten_contents,
source_encodes, source_family, source_program, source_show, source_space, source_width, splice, type3_font,
union, BlockStyle, EditError, LineEdit, Matrix, Metrics, Op, Pieces, RangeStyle, TextBlock, TextLine,
MAX_TYPE3_GLYPHS,
};
fn same_plane(a: &TextLine, b: &TextLine) -> bool {
let at = Matrix(a.origin.tm).then(&Matrix(a.origin.ctm));
@@ -166,8 +175,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 +187,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
+4 -1
View File
@@ -3,7 +3,10 @@
//! another one's output; the commit writes into the document it was opened on, and refuses if
//! that document has changed since.
use super::*;
use super::{
group_blocks, layout_draft, rewrite_block_from, text_lines, BlockStyle, Document, EditError, LineEdit,
TextBlock, TextEditLayout, TextLine, MAX_TYPE3_GLYPHS,
};
/// One paragraph being edited: the document as it was opened, and the page's lines then.
pub struct TextSession {
+3 -3
View File
@@ -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<Vec<(String, String)>> {
let docs: Vec<Option<pdfcraft_cos::Document>> =
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()
+1 -1
View File
@@ -75,7 +75,7 @@ impl OcrPage {
/// The recogniser, loaded once (it takes a moment) and shared.
pub fn engine() -> Result<Arc<Ocr>, String> {
static OCR: Mutex<Option<Arc<Ocr>>> = 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());
}
+1 -1
View File
@@ -74,5 +74,5 @@ pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
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"))
}
+1 -1
View File
@@ -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())?;
+2 -2
View File
@@ -246,7 +246,7 @@ pub(crate) fn tables(blocks: &[Block]) -> (Vec<Table>, Vec<bool>) {
used.sort_unstable();
used.dedup();
let mut edges: Vec<f64> = 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<Table>, Vec<bool>) {
let run_edges = |run: &[Vec<(usize, usize)>]| -> Vec<f64> {
let mut es: Vec<f64> =
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
};
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -980,7 +980,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
{
return invalid("alignment is 0 (left), 1 (centre) or 2 (right)");
}
if props.options.as_ref().is_some_and(|o| o.is_empty()) && matches!(f.kind, FieldKind::Combo | FieldKind::List) {
if props.options.as_ref().is_some_and(Vec::is_empty) && matches!(f.kind, FieldKind::Combo | FieldKind::List) {
return invalid("a list needs at least one option");
}
if props.check_style.is_some() && !matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) {
+11 -7
View File
@@ -243,13 +243,7 @@ fn components(links: &[Vec<usize>]) -> Vec<usize> {
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<usize>]) -> Vec<usize> {
component
}
fn pop_component(stack: &mut Vec<usize>, 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.
+1 -1
View File
@@ -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::<Vec<_>>().join(" "))
+8 -3
View File
@@ -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<char> = 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
}
+2 -2
View File
@@ -123,7 +123,7 @@ fn lex(src: &str) -> Result<Vec<Token>, 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<f64> {
}
fn is_numeric_text(s: &str) -> bool {
s.trim().parse::<f64>().is_ok_and(|v| v.is_finite())
s.trim().parse::<f64>().is_ok_and(f64::is_finite)
}
// ── the evaluator ───────────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1416,7 +1416,7 @@ fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current: usize) ->
"DataView",
"Intl",
];
let mut defined: Vec<String> = RESERVED.iter().map(|s| s.to_string()).collect();
let mut defined: Vec<String> = RESERVED.iter().map(ToString::to_string).collect();
for scope in scopes {
let kids: Vec<(usize, String, bool)> = {
let h = host(ctx)?.borrow();
+1 -1
View File
@@ -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"))
}
+1 -1
View File
@@ -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();
+2 -2
View File
@@ -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<String> {
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<String> {
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(),
+13 -8
View File
@@ -616,14 +616,7 @@ fn unlink_from_forms(doc: &mut Document, holders: &[ObjRef], doomed: &HashSet<Ob
continue;
}
let data = form.decoded()?;
let mut pruned = false;
for name in doomed_names {
match content_draws(doc, std::slice::from_ref(&data), &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() }),
}
}
let mut pruned = prune_doomed_xobjects(doc, &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<Ob
Ok(())
}
fn prune_doomed_xobjects(doc: &Document, data: &[u8], xo: &mut Dict, doomed_names: &[Vec<u8>]) -> Result<bool, CosError> {
let mut pruned = false;
for name in doomed_names {
match content_draws(doc, std::slice::from_ref(&data), 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).
+1 -1
View File
@@ -346,7 +346,7 @@ fn date(s: &[char], i: usize) -> Option<usize> {
// 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 })
+1 -1
View File
@@ -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<usize>]| ranges.iter().map(|r| r.len()).fold(0usize, usize::saturating_add);
let len = |ranges: &[Range<usize>]| ranges.iter().map(Range::len).fold(0usize, usize::saturating_add);
for (id, (old, _)) in &b.ids {
match a.ids.get(id) {
Some((new, draws)) => {
+1 -1
View File
@@ -51,7 +51,7 @@ fn main() -> Result<(), Box<dyn Error>> {
};
let threads: usize = match args.get(4) {
Some(s) => s.parse::<usize>()?.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)?);
+3 -3
View File
@@ -488,7 +488,7 @@ fn parse(bytes: &Arc<Vec<u8>>, password: Option<&str>) -> Option<Pdf> {
}
pub(crate) fn panic_message(p: &Box<dyn std::any::Any + Send>) -> String {
p.downcast_ref::<&str>().map(|s| s.to_string()).or_else(|| p.downcast_ref::<String>().cloned()).unwrap_or_else(|| "unknown panic".into())
p.downcast_ref::<&str>().map(ToString::to_string).or_else(|| p.downcast_ref::<String>().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<T>(m: &Mutex<T>) -> 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;
@@ -1,4 +1,6 @@
use super::*;
use std::sync::Arc;
use super::{PageRenderer, RenderConfig, RenderRequest, RenderedPage, Tile};
fn masked_shading(shading: &str, extra: &str, ink: &str) -> Vec<u8> {
let content = format!("20.25 20.25 279.5 279.5 re W n /M gs {ink} 0 0 320 320 re f");
+1 -1
View File
@@ -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;
}
}
+5 -5
View File
@@ -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
+19 -14
View File
@@ -294,6 +294,24 @@ fn parse_signer(si: &Tlv<'_>) -> Result<SignerInfo, SignError> {
}
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<Item = Tlv<'a>>) -> Result<(bool, Option<Vec<u8>>), SignError> {
let mut timestamp = false;
let mut timestamp_token = None;
for t in it {
@@ -309,20 +327,7 @@ fn parse_signer(si: &Tlv<'_>) -> Result<SignerInfo, SignError> {
}
}
}
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.
+8 -2
View File
@@ -1,7 +1,13 @@
use super::*;
use pdfcraft_cos::{Algorithm, NewEncryption, XrefEntry, write_full, write_incremental};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use pdfcraft_cos::{Algorithm, Dict, Document, NewEncryption, ObjRef, Object, SaveOptions, Stream, XrefEntry, write_full, write_incremental};
use super::{
is_document_timestamp, list_cached, only_grew, DigestCache, DssEvidence, TrustStore, DISCOVERY_CACHE_LIMIT,
MAX_DSS_BYTES, MAX_DSS_ENTRIES,
};
const STAMP: &str = "<< /Type /Sig /SubFilter /ETSI.RFC3161 /ByteRange [0 1 2 1] >>";
fn fixture(extra: &[&str]) -> Document {
+15 -16
View File
@@ -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 {
+3 -3
View File
@@ -56,9 +56,9 @@ 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(|c| c.is_ascii_digit()),
pw.chars().any(char::is_lowercase),
pw.chars().any(char::is_uppercase),
pw.chars().any(char::is_ascii_digit),
pw.chars().any(|c| !c.is_alphanumeric()),
]
.iter()
+3 -3
View File
@@ -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::<RecoveryMeta>(&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()
{
+1 -1
View File
@@ -18,7 +18,7 @@ fn pdf(annots: &[&str]) -> Vec<u8> {
"<< /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() {
+1 -1
View File
@@ -43,7 +43,7 @@ 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).
+1 -1
View File
@@ -15,7 +15,7 @@ fn pdf(acroform: &str, extra: &str, objects: &[&str]) -> Vec<u8> {
"<< /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() {
+1 -1
View File
@@ -18,7 +18,7 @@ fn load_average() -> Option<f64> {
/// 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");
+2 -2
View File
@@ -444,10 +444,10 @@ pub fn pattern_to_iso(text: &str, pattern: &str) -> Option<String> {
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 })
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()) {
if digits.is_empty() || !digits.chars().all(char::is_ascii_digit) {
return None;
}
let v: u32 = digits.parse().ok()?;
+4 -1
View File
@@ -14,7 +14,10 @@ use base64::Engine;
use crate::XfaError;
use crate::data::{DataNode, data_date_letters, data_path, data_to_iso, date_letters, is_on, iso_to_pattern};
use crate::model::*;
use crate::model::{
Access, Area, Border, ChoiceList, Common, Draw, ExclGroup, Field, Font, HAlign, Layout, Margin, Node, Occur,
PageArea, PageRole, Para, Placement, Presence, Rect, Script, Stroke, Subform, Template, Ui, VAlign, Value,
};
use crate::text::{self, Block, Face, Span};
/// Most pages a form may lay out to.
+5 -1
View File
@@ -5,7 +5,11 @@ use base64::Engine;
use roxmltree::Node as XmlNode;
use crate::XfaError;
use crate::model::*;
use crate::model::{
Access, Anchor, Area, Border, Caption, ChoiceList, Color, Common, Draw, Edge, ExclGroup, Field, FontSpec,
HAlign, Layout, Margin, Node, Occur, PageArea, PageRole, PageSet, ParaSpec, Paragraph, Placement, Presence,
Rect, RichText, Run, Script, Stroke, Subform, Template, Ui, VAlign, Value,
};
/// Deepest element nesting followed (templates are shallow; hostile ones are not).
const MAX_DEPTH: usize = 64;
+1 -1
View File
@@ -7,7 +7,7 @@ use std::collections::{BTreeMap, HashMap};
use pdfcraft_cos::{Dict, Document, Object, PdfString};
use crate::data::{DataNode, data_path, parse_datasets};
use crate::model::*;
use crate::model::{Access, Area, Draw, ExclGroup, Field, Node, Presence, Script, Subform, Template};
use crate::{Report, XfaError};
/// Private key on the AcroForm: presence scripts set, by SOM path.
+1 -1
View File
@@ -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),
})
+5 -4
View File
@@ -154,6 +154,7 @@ if has appimage; then
ln -s "$APP_ID.png" "$APPDIR/.DirIcon"
TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}"
SHA_FMT='%s %s\n'
if [[ -z "$TOOL" ]]; then
if [[ -z "$APPIMAGETOOL_SHA256" ]]; then
echo "no pinned appimagetool for $ARCH; set APPIMAGETOOL or put appimagetool on PATH" >&2
@@ -164,21 +165,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 -
printf "$SHA_FMT" "$APPIMAGETOOL_SHA256" "$download" | sha256sum -c -
chmod 755 "$download"
mv "$download" "$TOOL"
fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
printf "$SHA_FMT" "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c -
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 -
printf "$SHA_FMT" "$APPIMAGE_RUNTIME_SHA256" "$download" | sha256sum -c -
mv "$download" "$RUNTIME"
fi
printf '%s %s\n' "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
printf "$SHA_FMT" "$APPIMAGE_RUNTIME_SHA256" "$RUNTIME" | sha256sum -c -
# Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative, e.g. target/agent-<name>).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage"
+1 -1
View File
@@ -229,7 +229,7 @@ fn cargo_lock_versions(root: &Path) -> Result<BTreeSet<(String, String)>> {
}
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`).
+1 -1
View File
@@ -228,7 +228,7 @@ pub fn check(crates: &[Crate]) -> Vec<Violation> {
}
}
}
out.sort_by_key(|v| v.to_string());
out.sort_by_key(ToString::to_string);
out.dedup();
out
}