Skip to content

chore: resolve 20 audit MEDIUM + LOW findings#96

Merged
nelsonduarte merged 12 commits into
mainfrom
chore/audit-mediums-lows
Jun 18, 2026
Merged

chore: resolve 20 audit MEDIUM + LOW findings#96
nelsonduarte merged 12 commits into
mainfrom
chore/audit-mediums-lows

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

Summary

Closes 20 polish/quality bugs from audit Rounds 5-11 (12 MEDIUM + 8 LOW). Low risk, focused improvements covering security hardening, i18n correctness, atomic IO, and UX consistency.

MEDIUM (12)

  • M1 compress error i18n — `RuntimeError`/`ValueError` raised raw English. Now translated via `tool.compress.deps_missing` and `tool.compress.no_gain_detail`.
  • M2 encrypt wipe-on-success — try/finally cleared password fields on EVERY failure, forcing redigit. Now only wipes on success path; wrong-pwd/mismatch preserves fields.
  • M3 split clamp endpoints — Loading a smaller PDF after a larger one left endpoints > new total, causing row_invalid warnings. New `_clamp_rows_to_total` runs on `_load_input`.
  • M4 decrypt() return validation — `reader.decrypt(pwd)` was ignored at 5 sites (base.py, editor/tab.py x2, merge.py, watermark.py x2). Return 0 (wrong pwd) was treated as success -> empty pages. Now raises `tool.err.wrong_password` consistently.
  • M5 atomic pipeline save — `shutil.copy2` in window._save_pipeline was non-atomic; crash mid-write corrupted destination. Now `mkstemp + os.replace` on same volume; `shutil.copy2` retained as fallback.
  • M6 symlink destination warn — `logging.warning` when destination is a symlink (heuristic via `os.path.realpath`).
  • M7 defer update check 2s — was triggered in `init` before `showMaximized()`. Now `QTimer.singleShot(2000, ...)` with `isValid` guard to avoid the startup window.
  • M8 atomic in nup + import_pdf — 9 `with open(out_path, "wb")` sites migrated to `_atomic_pdf_write`.
  • M9 shortcut setContext — PgUp/PgDown/Ctrl+S/Ctrl+W shortcuts now use `Qt.WidgetWithChildrenShortcut` so they don't fire from QTextEdit during dialogs.
  • M10 themed QToolTip — added `QToolTip` rule to both `STYLE` (dark) and `STYLE_LIGHT` (light) using existing palette tokens.
  • M11 destructive QMessageBox defaults — 4 sites without `defaultButton` (`_close_tab`, `closeEvent` x2, `page_numbers` replace) now default to `Cancel` / `No`.
  • M12 signature dialog setTabOrder — explicit chain so keyboard users reach the password field directly.

LOW (8 - 6 applied + 2 already implemented)

  • L1 `_IMG_EXTS` used as filter — was dead code; now drives the file dialog filter in `_convert_images`.
  • L2 focus indicator — already implemented in `styles.py:135,375`.
  • L3 QTimer.singleShot isValid guards — 2 critical lambda sites guarded (editor tab `_load_form_fields`, window `_check_for_updates_async`).
  • L4 non-Latin font warning — `page_numbers.py` and `editor/tab.py` now emit a `tool.warn.font_latin_only` status when input contains chars beyond Helvetica's Latin-1 range.
  • L5 PE version-info — `version_info.txt` template added; `pdfapps.spec` stamps Windows builds with Product Version / Company / Copyright (mitigates Bitdefender FP heuristics).
  • L6 decimal separatordeferred. Requires `QLocale` propagation across UI; will tackle as a focused PR.
  • L7 recent files lexists — already implemented in PR-G (`i18n.py:221`).
  • L8 viewer note-delete no-match — was running `saveIncr` even when no annotation matched. Now warns (`viewer.delete_no_match`) and returns early before disk write.

i18n

5 new keys x 8 languages: `tool.compress.deps_missing`, `tool.compress.no_gain_detail`, `tool.err.wrong_password`, `tool.warn.font_latin_only`, `viewer.delete_no_match`. Parity verified: 8 x 602 keys.

Tests

`tests/test_audit_mediums_lows.py` — 26 source-level + behavioral tests covering all 18 applied fixes. All pass.

Adversarial review

Review verdict: SAFE TO MERGE. Three cosmetic non-blocking notes:

  • Signature dialog tab chain skips `btn_clear` (Draw) and `_imp_btn` (Import) cosmetically.
  • Non-Latin warning detector covers `text`/`note` but not `text_edit` inline edits.
  • translations.json lacks trailing newline (style only).

These are tracked for a follow-up if needed; no dataloss / security risk.

Validation

@nelsonduarte nelsonduarte added bug Something isn't working enhancement New feature or request labels Jun 18, 2026
Comment thread app/window.py
import os

from PySide6.QtCore import Qt, QSize, Signal
from PySide6.QtCore import Qt, QSize, Signal, QTimer
Comment thread app/window.py
_logging.getLogger("pdfapps").warning(
"Pipeline save destination is a symlink: %s -> %s",
path, os.path.realpath(path))
except Exception:
The compress utility raised raw English RuntimeError ("Install pypdf
and/or PyMuPDF...") and ValueError ("No gain: ... KB") strings that
surfaced unchanged in error dialogs — Chinese / Russian / German users
saw English. Route both through t() with new tool.compress.deps_missing
and tool.compress.no_gain_detail keys (8 languages each).
The finally-clause unconditionally cleared every QLineEdit, so a wrong
password or owner/confirm mismatch forced the user to retype all four
fields. Gate the clear with a `success` flag — only sticky errors via
show_error keep the input; wrong_pass / mismatch warnings preserve it.
Pre-existing rows kept their start/end values when the user swapped a
50-page PDF for a 10-page one, then Apply tripped the row_invalid
error on every row. _load_input now calls _clamp_rows_to_total so
spinbox values are capped to the new total and end>=start is preserved.
pypdf.PdfReader.decrypt returns 0 on wrong password and silently exposes
a reader whose .pages is empty. Every downstream tool then wrote an
empty PDF over the user's destination. BasePage._open_reader, the
editor's _load_form_fields + _apply_forms paths, watermark
(source + watermark file) and merge per-input now raise a translated
"wrong password" error instead.
_save_pipeline used shutil.copy2 directly, so a crash mid-copy could
truncate a pre-existing destination PDF. Switched to mkstemp +
os.replace (same-volume) with a copy2 fallback when mkstemp can't
write next to the target. Also defer the startup update check via
QTimer.singleShot(2000) so it never races showMaximized, scope the
PgUp/PgDown/Ctrl+S/Ctrl+W shortcuts to WidgetWithChildrenShortcut so
editing a QTextEdit no longer pages the viewer behind, default
unsaved-pipeline prompts to Cancel, and log a warning when the save
destination is a symlink (audit trail for J3).
PR-A migrated 8 tools to BasePage._atomic_pdf_write, but nup.py and the
8 converters in import_pdf.py kept calling doc.save(out_path) directly.
A kill / crash mid-save could truncate a pre-existing output PDF and
none of them benefited from the same-source-path guard. The bulk
doc.save migration uses the helper's save_opts plumbing (garbage=4,
deflate=True for nup). Also wire the previously dead _IMG_EXTS tuple
in import_pdf as an input filter for the image-to-PDF converter.
Default Qt tooltips inherited the OS chrome (white-on-yellow on
Windows) which clashed with the dark palette and was barely readable
on top of the light theme too. Add a QToolTip block to STYLE and
STYLE_LIGHT using the existing card / border / text-primary tokens
so tooltips track the active theme.
_SignatureDialog has 5+ focusable widgets (tabs, type input, font
combo, save checkbox, cancel + ok). Focus jumped erratically between
the tabs widget and the buttons depending on layout add order. Lock
the path: tabs -> type input -> font combo -> save cb -> cancel -> ok.
…atin

The "replace existing page numbers" QMessageBox.question lacked an
explicit defaultButton — an Enter keystroke would Yes-confirm a
destructive overwrite with no undo (the tool routes through
saveIncr). Set defaultButton=No. Also detect non-Latin chars in the
selected format template and surface a status-bar warning, since the
built-in PyMuPDF "helv" font is Type-1 Latin-1 and renders CJK /
Cyrillic / Arabic / Hebrew as ? glyphs.
If the search loop in the right-click delete handler exits with
target_annot is None (annotation already wiped by another tool, or
the bbox / content match drifts after an external edit), the code
still bumped the incremental-update offset by calling saveIncr() — a
no-op write that changed the file hash for no user benefit. Show a
warning and return early instead. New translation key:
viewer.delete_no_match across all 8 languages.
Bitdefender + heuristic AV engines flag PyInstaller binaries with no
version-info resource as suspicious. Add a version='version_info.txt'
kwarg to the EXE() call on Windows builds only, and regenerate
version_info.txt from app.constants.APP_VERSION on each spec
evaluation so the PE header stays in sync with the macOS bundle. The
checked-in version_info.txt acts as a seed / fallback.
26 assertions covering M1-M12 and L1/L4/L5/L8: i18n parity for the
new keys, atomic write migration in nup/import_pdf/pipeline save,
decrypt return checks across 5 tools, QShortcut WidgetWithChildren
scope, themed QToolTip rule, defaultButton wiring on destructive
prompts, _SignatureDialog setTabOrder, _IMG_EXTS usage in import_pdf,
non-Latin warnings, version_info.txt + spec wiring, and the viewer
note-delete no-match no-op.
@nelsonduarte nelsonduarte force-pushed the chore/audit-mediums-lows branch from 97f2197 to 1252498 Compare June 18, 2026 15:32
@nelsonduarte nelsonduarte merged commit b5530de into main Jun 18, 2026
2 checks passed
@nelsonduarte nelsonduarte deleted the chore/audit-mediums-lows branch June 18, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants