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<u8>].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
This commit is contained in:
Dylan M. Taylor
2026-10-23 23:28:18 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Dylan Taylor
parent 0c748e34a8
commit a047c6ba09
43 changed files with 146 additions and 124 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 dpi = parse_dpi(flag(args, "--dpi").unwrap_or("96"))?;
let out = flag(args, "--out").ok_or("render: missing --out (.png, .jpg, .tif or .pam)")?; 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. // 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("png") => Some(ImageFormat::Png),
Some("jpg" | "jpeg") => Some(ImageFormat::Jpeg { quality: 90 }), Some("jpg" | "jpeg") => Some(ImageFormat::Jpeg { quality: 90 }),
Some("tif" | "tiff") => Some(ImageFormat::Tiff), 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) { if let Ok(rd) = std::fs::read_dir(&p) {
stack.extend(rd.flatten().map(|e| e.path())); 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); out.push(p);
} }
} }
+10 -11
View File
@@ -53,6 +53,15 @@ struct Walk<'a> {
budget: DecodeBudget, 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<'_> { impl Walk<'_> {
fn sub(&self, res: &Dict, key: &[u8]) -> Dict { 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() 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); let inside = covered || stack.last().copied().unwrap_or(false);
match op.op.as_slice() { match op.op.as_slice() {
b"BMC" => stack.push(inside || op.name(0) == Some(b"Artifact")), b"BMC" => stack.push(inside || op.name(0) == Some(b"Artifact")),
b"BDC" => { b"BDC" => stack.push(inside || bdc_marks(self.doc, &op, &props)),
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"EMC" => { b"EMC" => {
stack.pop(); stack.pop();
} }
+1 -1
View File
@@ -380,7 +380,7 @@ fn table_doc(th: &str, extra: &[&str], kids: &str) -> Document {
th.to_string(), th.to_string(),
"<< /S /TD >>".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, "") 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) grow(bounds([*from, *to].into_iter()).unwrap_or_default(), pad + 1.0)
} }
Shape::Ink { strokes } | Shape::Signature { strokes } => { 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)")); return Err(bad("drawing (no points)"));
} }
// Strokes of three or more points are drawn as curves, which stay within their // 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)); d.set(b"PCOutline".to_vec(), Object::Array(list));
} }
Shape::CustomStamp { name, picture, image, size, .. } => { 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"Name".to_vec(), Object::name(&format!("PCCustom{clean}")));
d.set(b"PCPicture".to_vec(), Object::Ref(*picture)); d.set(b"PCPicture".to_vec(), Object::Ref(*picture));
d.set(b"PCPictureImage".to_vec(), Object::Bool(*image)); 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 { 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())) 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 { if input.len() as u64 > MAX_INPUT {
return Err(failed("input exceeds 512 MiB")); 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 bytes = Arc::new(input);
let mut log = Vec::new(); let mut log = Vec::new();
for step in &self.steps { 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 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. // Small jobs aren't worth a second parse of the document.
if workers == 1 || pages.len() < 8 { if workers == 1 || pages.len() < 8 {
let mut r = PageRenderer::new(bytes.clone(), config); 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, 0xEE if seg.starts_with(b"Adobe") => adobe = true,
// APP2 ICC profile, possibly split over several chunks. // APP2 ICC profile, possibly split over several chunks.
0xE2 => { 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)); icc_chunks.push((*seq, *count, data));
} }
} }
+3 -3
View File
@@ -166,8 +166,8 @@ fn opens_with_marker(text: &str) -> bool {
if !first.is_ascii_digit() && !first.is_ascii_alphabetic() { if !first.is_ascii_digit() && !first.is_ascii_alphabetic() {
return false; return false;
} }
let digits = text.chars().take_while(|c| c.is_ascii_digit()).count(); let digits = text.chars().take_while(char::is_ascii_digit).count();
let letters = text.chars().take_while(|c| c.is_ascii_alphabetic()).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: // Both runs are ASCII characters, so their length in characters is their length in bytes:
// the slice below starts on a character boundary. // the slice below starts on a character boundary.
let rest = if digits > 0 { let rest = if digits > 0 {
@@ -178,7 +178,7 @@ fn opens_with_marker(text: &str) -> bool {
return false; return false;
}; };
let mut after = rest.chars(); 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 /// A row that opens with a list marker opens its own item: joining it to the row above is what
+3 -3
View File
@@ -1050,7 +1050,7 @@ fn shown_under_redactions(
} }
fn render_threads() -> usize { 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 /// 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"), (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"); return bad("passwords can't be empty");
} }
if self.algorithm != pdfcraft_cos::Algorithm::Aes256 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)>> { pub fn combine_field_conflicts(&self, sources: &[CombineSource], passwords: &[Option<&str>]) -> Vec<Vec<(String, String)>> {
let docs: Vec<Option<pdfcraft_cos::Document>> = 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(); 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(); 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. // 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() 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. /// The recogniser, loaded once (it takes a moment) and shared.
pub fn engine() -> Result<Arc<Ocr>, String> { pub fn engine() -> Result<Arc<Ocr>, String> {
static OCR: Mutex<Option<Arc<Ocr>>> = Mutex::new(None); 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() { if let Some(o) = slot.as_ref() {
return Ok(o.clone()); return Ok(o.clone());
} }
+1 -1
View File
@@ -74,5 +74,5 @@ pub fn discover(roots: &[PathBuf]) -> Vec<FontFile> {
files files
} }
fn font_extension(path: &Path) -> bool { 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 { } else {
Some(by_som.iter().filter(|(som, _)| names.iter().any(|n| som_matches(som, n))).map(|(_, f)| f.clone()).collect()) 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(()); return Ok(());
} }
pdfcraft_forms::reset(self.doc, listed.as_deref()).map_err(|e| e.to_string())?; 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.sort_unstable();
used.dedup(); 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(); 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); edges.dedup_by(|a, b| (*a - *b).abs() <= COL_TOL);
let ncols = edges.len(); let ncols = edges.len();
let ok = (ncols >= 3 || (ncols == 2 && run.len() >= 3)) && ncols <= MAX_COLS; 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 run_edges = |run: &[Vec<(usize, usize)>]| -> Vec<f64> {
let mut es: 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(); 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.dedup_by(|a, b| (*a - *b).abs() <= COL_TOL);
es 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. /// report that called it a standard-14 name would be telling the reader something untrue.
pub fn recognise(base: &str) -> Option<(Self, Match)> { pub fn recognise(base: &str) -> Option<(Self, Match)> {
let base = base.rsplit('+').next().unwrap_or(base); 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 has = |w: &str| flat.contains(w);
let (bold, italic) = (has("bold"), has("italic") || has("oblique")); let (bold, italic) = (has("bold"), has("italic") || has("oblique"));
// Two bools index a four-element array, so the subscript is provably 0..=3. // 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)"); 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"); return invalid("a list needs at least one option");
} }
if props.check_style.is_some() && !matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) { 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; continue;
} }
if low[v] == index[v] { if low[v] == index[v] {
while let Some(w) = stack.pop() { pop_component(&mut stack, &mut on_stack, &mut component, v, count);
on_stack[w] = false;
component[w] = count;
if w == v {
break;
}
}
count += 1; count += 1;
} }
if let Some(&(parent, _)) = work.last() { if let Some(&(parent, _)) = work.last() {
@@ -260,6 +254,16 @@ fn components(links: &[Vec<usize>]) -> Vec<usize> {
component 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 /// 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 /// 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. /// 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() .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) .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(); .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(); 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])); 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(" ")) 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 { if numbered {
return out; return out;
} }
substitute_isolated_n(&out, &row_number)
}
fn substitute_isolated_n(out: &str, row_number: &str) -> String {
if out == "N" || out == "n" { if out == "N" || out == "n" {
return row_number; return row_number.to_string();
} }
let chars: Vec<char> = out.chars().collect(); let chars: Vec<char> = out.chars().collect();
let mut named = String::with_capacity(out.len() + row_number.len()); let mut named = String::with_capacity(out.len() + row_number.len());
let mut numbered = false;
for (i, &ch) in chars.iter().enumerate() { 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 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)); let before_separator = chars.get(i + 1).is_none_or(|c| SEPARATORS.contains(c));
if matches!(ch, 'N' | 'n') && after_separator && before_separator { if matches!(ch, 'N' | 'n') && after_separator && before_separator {
named.push_str(&row_number); named.push_str(row_number);
numbered = true; numbered = true;
} else { } else {
named.push(ch); named.push(ch);
} }
} }
if !numbered { if !numbered {
named.push_str(&row_number); named.push_str(row_number);
} }
named 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 }); 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; let start = i;
while i < chars.len() && (chars[i].is_ascii_digit() || (chars[i] == '.' && chars.get(i + 1) != Some(&'.'))) { while i < chars.len() && (chars[i].is_ascii_digit() || (chars[i] == '.' && chars.get(i + 1) != Some(&'.'))) {
i += 1; i += 1;
@@ -760,7 +760,7 @@ fn parse_num(s: &str) -> Option<f64> {
} }
fn is_numeric_text(s: &str) -> bool { 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 ─────────────────────────────────────────────────────────────────────────── // ── the evaluator ───────────────────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1416,7 +1416,7 @@ fn install(ctx: &mut Context, event: &XfaEvent, doc: &XfaDoc, current: usize) ->
"DataView", "DataView",
"Intl", "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 { for scope in scopes {
let kids: Vec<(usize, String, bool)> = { let kids: Vec<(usize, String, bool)> = {
let h = host(ctx)?.borrow(); 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()), format!("<< /Type /Outlines /First 5 0 R /Count {} >>", items.len()),
"<< /Type /Page /Parent 2 0 R >>".into(), "<< /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")) 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")); 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 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. // A file that is replaced keeps its permissions (#1790) and group. Unix only: Windows is as before.
#[cfg(unix)] #[cfg(unix)]
let keep = std::fs::metadata(path).ok(); 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. /// A PDF date (`D:YYYYMMDDHHmmSSOHH'mm'`) as an XMP (ISO 8601) date.
pub fn iso_date(pdf: &str) -> Option<String> { pub fn iso_date(pdf: &str) -> Option<String> {
let d = pdf.trim().trim_start_matches("D:"); 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 { if digits.len() < 4 {
return None; return None;
} }
@@ -161,7 +161,7 @@ pub fn iso_date(pdf: &str) -> Option<String> {
let tz = match rest.chars().next() { let tz = match rest.chars().next() {
Some('Z') | None => "Z".to_string(), Some('Z') | None => "Z".to_string(),
Some(sign @ ('+' | '-')) => { 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")) format!("{sign}{}:{}", t.get(0..2).unwrap_or("00"), t.get(2..4).unwrap_or("00"))
} }
_ => "Z".to_string(), _ => "Z".to_string(),
+15 -10
View File
@@ -443,8 +443,8 @@ fn content_draws(doc: &Document, content: &[Vec<u8>], xo: &Dict, name: &[u8], de
if depth >= MAX_DEPTH { if depth >= MAX_DEPTH {
return None; return None;
} }
let Ok(data) = s.decoded() else { return None }; let Ok(_data) = s.decoded() else { return None };
match content_draws(doc, std::slice::from_ref(&data), xo, name, depth + 1, seen) { match content_draws(doc, content, xo, name, depth + 1, seen) {
Some(false) => {} Some(false) => {}
other => return other, other => return other,
} }
@@ -616,14 +616,7 @@ fn unlink_from_forms(doc: &mut Document, holders: &[ObjRef], doomed: &HashSet<Ob
continue; continue;
} }
let data = form.decoded()?; let data = form.decoded()?;
let mut pruned = false; let mut pruned = prune_doomed_xobjects(doc, std::slice::from_ref(&data), &mut xo, &doomed_names)?;
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() }),
}
}
if pruned { if pruned {
res.set(b"XObject".to_vec(), Object::Dict(xo)); 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(()) Ok(())
} }
fn prune_doomed_xobjects(doc: &Document, content: &[Vec<u8>], xo: &mut Dict, doomed_names: &[Vec<u8>]) -> Result<bool, CosError> {
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 /// 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 /// 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). /// 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. // Numeric: m/d/yy(yy), m-d-yyyy, d.m.yyyy, yyyy-mm-dd.
if let Some(a) = number(s, i, 1, 4) { if let Some(a) = number(s, i, 1, 4) {
let sep = s.get(i + a).copied(); 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) && let Some(b) = number(s, i + a + 1, 1, 2)
&& s.get(i + a + 1 + b).copied() == sep && 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 }) && 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 changed = HashSet::new();
let mut empty = HashSet::new(); let mut empty = HashSet::new();
let mut budget = MAX_COMPARED; 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 { for (id, (old, _)) in &b.ids {
match a.ids.get(id) { match a.ids.get(id) {
Some((new, draws)) => { Some((new, draws)) => {
+1 -1
View File
@@ -51,7 +51,7 @@ fn main() -> Result<(), Box<dyn Error>> {
}; };
let threads: usize = match args.get(4) { let threads: usize = match args.get(4) {
Some(s) => s.parse::<usize>()?.max(1), 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)?); 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 { 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). /// 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> { 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"))] #[cfg(not(target_arch = "wasm32"))]
@@ -666,7 +666,7 @@ impl PoolParser {
// Drop never joins a parser. A waiting worker must still notice // Drop never joins a parser. A waiting worker must still notice
// its pool's disconnected wake channel while another thread is // its pool's disconnected wake channel while another thread is
// stuck in initialization and cannot signal this condition. // 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 => { ParserState::Empty => {
*state = ParserState::Loading; *state = ParserState::Loading;
+1 -1
View File
@@ -220,7 +220,7 @@ impl WorkQueue {
} }
// The parser validity token can change independently of this queue. Bound the // The parser validity token can change independently of this queue. Bound the
// wait so failed generations are released even while the consumer is paused. // 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; return;
} }
let Some(glyph) = merge(&drawn, text.clone()) else { 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; 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 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(|c| c.is_control()) { let at = if text.chars().all(char::is_control) {
None None
} else { } else {
self.glyphs.push(glyph); self.glyphs.push(glyph);
@@ -594,7 +594,7 @@ impl TextDevice {
let text = replacement.to_owned(); let text = replacement.to_owned();
#[cfg(test)] #[cfg(test)]
REDRAW_TEXT_BYTES.with(|c| c.set(c.get() + text.len())); 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) { match merge(run, text) {
Some(glyph) => self.glyphs.push(glyph), Some(glyph) => self.glyphs.push(glyph),
None => self.glyphs.extend_from_slice(run), None => self.glyphs.extend_from_slice(run),
@@ -640,7 +640,7 @@ impl<'a> Device<'a> for TextDevice {
return; return;
} }
}; };
if !replaced && text.chars().all(|c| c.is_control()) { if !replaced && text.chars().all(char::is_control) {
return; return;
} }
// Glyph space → view space. Glyph space uses 1000 units per em for outline glyphs; the em // 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 (scheme, scheme_digest) = signature_algorithm(&next)?;
let signature = it.next().ok_or_else(|| bad("signature"))?.octets("signature")?.into_owned(); 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 = false;
let mut timestamp_token = None; let mut timestamp_token = None;
for t in it { for t in it {
@@ -309,20 +327,7 @@ fn parse_signer(si: &Tlv<'_>) -> Result<SignerInfo, SignError> {
} }
} }
} }
Ok(SignerInfo { Ok((timestamp, timestamp_token))
sid,
digest,
signed_attrs,
message_digest,
content_type,
signing_time,
signing_certificate,
scheme,
scheme_digest,
signature,
timestamp,
timestamp_token,
})
} }
/// Attach an RFC 3161 `signatureTimeStampToken` unsigned attribute to a detached CMS object. /// Attach an RFC 3161 `signatureTimeStampToken` unsigned attribute to a detached CMS object.
+15 -16
View File
@@ -657,22 +657,21 @@ impl PdfCraftApp {
} }
}); });
return; return;
} else { }
match self.session.extract(id, &pages) { match self.session.extract(id, &pages) {
Ok(bytes) => { Ok(bytes) => {
let message = if pages.len() == 1 { let message = if pages.len() == 1 {
tl!("Extracted 1 page").to_string() tl!("Extracted 1 page").to_string()
} else { } else {
crate::i18n::fmt(tl!("Extracted {n} pages"), &[("n", &pages.len().to_string())]) 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. // 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") }; let name = if renamed { format!("{base}.pdf") } else { format!("{stem} (extract).pdf") };
self.open_created(&name, bytes, &message) self.open_created(&name, bytes, &message)
} }
Err(e) => { Err(e) => {
self.notify_fmt("Couldn't extract pages: {e}", &[("e", &e.to_string())]); self.notify_fmt("Couldn't extract pages: {e}", &[("e", &e.to_string())]);
return; return;
}
} }
} }
if opts.delete { if opts.delete {
+2 -2
View File
@@ -56,8 +56,8 @@ impl ProtectDraft {
/// A rough password strength, as Acrobat's meter shows (Weak / Medium / Strong). /// A rough password strength, as Acrobat's meter shows (Weak / Medium / Strong).
pub fn strength(pw: &str) -> (&'static str, Color32) { pub fn strength(pw: &str) -> (&'static str, Color32) {
let classes = [ let classes = [
pw.chars().any(|c| c.is_lowercase()), pw.chars().any(char::is_lowercase),
pw.chars().any(|c| c.is_uppercase()), pw.chars().any(char::is_uppercase),
pw.chars().any(|c| c.is_ascii_digit()), pw.chars().any(|c| c.is_ascii_digit()),
pw.chars().any(|c| !c.is_alphanumeric()), pw.chars().any(|c| !c.is_alphanumeric()),
] ]
+3 -3
View File
@@ -206,10 +206,10 @@ impl RecoveryStore {
let mut out = Vec::new(); let mut out = Vec::new();
for entry in dir.flatten() { for entry in dir.flatten() {
let path = entry.path(); 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; 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")) 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(|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")) }); .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() { for entry in dir.flatten() {
let path = entry.path(); let path = entry.path();
// Only sidecars whose PDF is gone: they hold no document content. // 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()) && entry.file_type().is_ok_and(|t| t.is_file())
&& !path.with_extension("pdf").exists() && !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(), "<< /Length 0 >>\nstream\n\nendstream".into(),
"<< /Type /XObject /Subtype /Form /BBox [0 0 200 100] /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 out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new(); let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() { for (i, o) in objs.iter().enumerate() {
+3 -1
View File
@@ -43,7 +43,9 @@ fn store(tag: &str) -> RecoveryStore {
/// Snapshot files (the PDF and its sidecar), not the running instances' lock files. /// Snapshot files (the PDF and its sidecar), not the running instances' lock files.
fn files_in(s: &RecoveryStore) -> usize { 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). /// 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(), "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R >>".into(),
"<< /Length 0 >>\nstream\n\nendstream".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 out = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new(); let mut offsets = Vec::new();
for (i, o) in objs.iter().enumerate() { 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 /// 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. /// average above twice the core count (other builds running), skip and say so.
fn overloaded() -> bool { 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() { match load_average() {
Some(l) if l > 2.0 * cores => { Some(l) if l > 2.0 * cores => {
eprintln!("PERF skipped: load average {l:.0} on {cores} cores; frame budgets need a quiet machine"); eprintln!("PERF skipped: load average {l:.0} on {cores} cores; frame budgets need a quiet machine");
+2 -5
View File
@@ -441,11 +441,8 @@ pub fn pattern_to_iso(text: &str, pattern: &str) -> Option<String> {
"yyyy" | "yy" | "mm" | "m" | "dd" | "d" => { "yyyy" | "yy" | "mm" | "m" | "dd" | "d" => {
// Digits up to the next literal (or a fixed width). // Digits up to the next literal (or a fixed width).
let fixed = matches!(t.as_str(), "yyyy" | "yy" | "mm" | "dd"); let fixed = matches!(t.as_str(), "yyyy" | "yy" | "mm" | "dd");
let width = if fixed { let width =
t.len() if fixed { t.len() } else { rest.chars().take_while(char::is_ascii_digit).count().min(if t == "d" || t == "m" { 2 } else { 4 }) };
} else {
rest.chars().take_while(|c| c.is_ascii_digit()).count().min(if t == "d" || t == "m" { 2 } else { 4 })
};
let digits: String = rest.chars().take(width).collect(); 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(|c| c.is_ascii_digit()) {
return None; return None;
+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 { .filter_map(|c| match c {
'\r' | '\n' | '\t' | '\u{2028}' | '\u{2029}' | '\u{2009}' | '\u{202F}' | '\u{3000}' => Some(' '), '\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{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('\''), '\u{2018}' | '\u{2019}' => Some('\''),
_ => Some(c), _ => Some(c),
}) })
+10 -4
View File
@@ -92,6 +92,12 @@ has() {
case " $FORMATS " in *" $format "*) return 0 ;; *) return 1 ;; esac 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 ------------------------------------------------------------------------------------ # ---- .tar.gz ------------------------------------------------------------------------------------
if has tar; then if has tar; then
mkdir -p "$WORK/tar" mkdir -p "$WORK/tar"
@@ -164,21 +170,21 @@ if has appimage; then
download="$TOOL.download" download="$TOOL.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/appimagetool/releases/download/$APPIMAGETOOL_VERSION/appimagetool-$ARCH.AppImage" "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" chmod 755 "$download"
mv "$download" "$TOOL" mv "$download" "$TOOL"
fi fi
printf '%s %s\n' "$APPIMAGETOOL_SHA256" "$TOOL" | sha256sum -c - verify_sha256 "$APPIMAGETOOL_SHA256" "$TOOL"
fi fi
RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH" RUNTIME="$CARGO_TARGET_DIR/appimage-runtime-$APPIMAGE_RUNTIME_VERSION-$ARCH"
if [[ ! -f "$RUNTIME" ]]; then if [[ ! -f "$RUNTIME" ]]; then
download="$RUNTIME.download" download="$RUNTIME.download"
curl --proto '=https' -fsSL -o "$download" \ curl --proto '=https' -fsSL -o "$download" \
"https://github.com/AppImage/type2-runtime/releases/download/$APPIMAGE_RUNTIME_VERSION/runtime-$ARCH" "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" mv "$download" "$RUNTIME"
fi 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 # Absolute, because appimagetool runs in $DIST below (CARGO_TARGET_DIR or APPIMAGETOOL may be
# relative, e.g. target/agent-<name>). # relative, e.g. target/agent-<name>).
OUT="$(cd "$DIST" && pwd)/$BASENAME.AppImage" 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 { 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`). /// 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.dedup();
out out
} }