Skip to content

fix: 12 Round 10 audit findings (2 CRIT + 5 HIGH + 3 MED + 2 LOW)#84

Merged
nelsonduarte merged 9 commits into
mainfrom
fix/audit-r10
Jun 9, 2026
Merged

fix: 12 Round 10 audit findings (2 CRIT + 5 HIGH + 3 MED + 2 LOW)#84
nelsonduarte merged 9 commits into
mainfrom
fix/audit-r10

Conversation

@nelsonduarte

Copy link
Copy Markdown
Owner

Summary

Closes 12 bugs from audit Round 10. 11 fixed in code, 1 (tab close OCR race) audited and locked-in via test invariant. Includes 2 CRITs (viewer dataloss + editor regression from PR-D) and 5 HIGHs.

Critical fixes

  • 🔴 CRIT-1 viewer saveIncr (app/viewer/canvas.py) — Delete comment via right-click wrote directly to the user's PDF via saveIncr() with no backup, no atomic write, no confirmation. Power loss = file corruption. PR-A fixed the equivalent in the editor; this completes the parity for the viewer. New flow: confirmation dialog (default=No), tempfile backup, two-step delete/save with restore-on-failure, and document reopen if save fails (to discard the in-memory mutation).
  • 🔴 CRIT-2 release_doc regression (PR-D) (app/editor/tab.py) — _canvas.release_doc() was called BEFORE _prompt_encryption_choice(). Cancelling the encryption dialog left the canvas in a dead state with placeholder pages permanently until manual reload. Now uses a short-lived peek-doc to drive the prompt; release_doc() happens only AFTER the user confirms.

HIGH

MEDIUM

LOW

i18n

1 new key × 8 languages: viewer.drop_url_not_supported. Parity verified by test.

Tests

tests/test_audit_r10.py — 19 new tests covering all 12 fixes (source-level + behavioral where feasible).

Final: 249 passed, 1 failed (test_flatpak_manifest_tag_is_current pre-existing — manifest in v1.13.9 vs APP_VERSION 1.13.15), 1 skipped.

Audit context

Round 10 was the focused viewer drill-down (suggested by R9 reviewer) plus PR-D regression check. After this PR, CRIT/HIGH-zero across the catalogued audit. Total bugs mapped: 78; closed: 37; remaining: 41 (24 MEDIUM + 17 LOW) — all polish, none release-blocking.

Non-blocking observations (from review)

  1. viewer/canvas.py:716-719 — backup+saveIncr cycle fires even when target_annot is None. Minor IO redundancy.
  2. editor/canvas.py:525clear_overlay_pixmap_cache() invalidates globally not per-tab. Functional; consider docstring clarification.
  3. window.py:1015-1018 — folder drop opens all PDFs without prompt. Consider if len > 20: confirm.
  4. viewer/canvas.py:630-639 — backup file briefly visible in same dir; OneDrive/Dropbox sync noise possible. Cross-FS trade-off.

Validation

  • 9 atomic commits
  • pytest 249/1/1
  • No APP_VERSION bump
  • Compatible with all merged PRs (A through E + snap + dependencies)
  • i18n parity verified

nelsonduarte and others added 9 commits June 9, 2026 11:15
…IT-1, A2, A3)

Comment delete from the viewer context menu now:
- copies the PDF to a same-directory .bak before delete_annot, restoring
  it on saveIncr failure so power loss or read-only writes cannot
  corrupt the user's file (CRIT-1);
- separates the in-memory delete from saveIncr so a disk write error
  reopens the doc to discard the stale in-memory mutation, preventing
  inconsistent state across the next paint (A2);
- breaks ties between annotations with identical content using bbox
  proximity (1pt) before falling back to content-only match, so two
  notes with the same text no longer cause the wrong one to be deleted
  (A3).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR-D's regression made _run() release the canvas's fitz.Document
BEFORE prompting the user how to save an encrypted file. Cancelling
the prompt then left the canvas with _doc=None, stranding the editor
on the placeholder until the user manually reloaded the PDF.

Open a short-lived peek doc to read needs_pass / drive the encryption
prompt. Only release the canvas once the user has confirmed the
choice; the keep-protection and save-unprotected paths are unchanged
because they reach the existing release_doc call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#11)

The module-level @lru_cache(maxsize=64) for _load_overlay_pixmap had
no clear() hook the tab close path could use — across long sessions
with multiple tabs each cache slot kept a multi-MB QPixmap, leaking
until process exit. Replace it with:
- an explicit FIFO dict + manual eviction so size is still capped at
  64 but close_doc() can drop everything via the new
  clear_overlay_pixmap_cache() helper;
- an uncached fallback path when os.path.getmtime() raises OSError,
  so a transient FS error no longer poisons the cache with a null
  QPixmap that stays for the rest of the process lifetime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The recents list was built once in PdfViewerPanel.__init__ from
get_recent_files() and never refreshed. add_recent_file() updates the
config.json behind the scenes, but the user kept seeing the stale UI
list until the next app launch.

Extract the population code into _refresh_recents() and call it from
window._load_and_track / _open_in_new_tab across every viewer tab, so
returning to the placeholder always shows the up-to-date list. No new
Qt signals required — the parent already knows when a load happens.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Clicking Apply in Forms mode on a PDF with no form fields raised
pypdf's PyPdfError("No /AcroForm dictionary in PDF") — a cryptic
internal error from update_page_form_field_values that looked like
a bug. Detect the missing /AcroForm root entry on the prepared
PdfWriter and short-circuit with the existing editor.forms.no_fields
status string instead, leaving the input file untouched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The viewer's note-delete context menu already prompts the user before
destroying a comment; the editor's equivalent silently deleted on
left-click, which was inconsistent and easy to trigger by accident
on _existing notes that persist to the saved file. Add the same
QMessageBox.question(default=No) using the existing
viewer.confirm_delete_comment / msg.confirm i18n strings — no new
keys required.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The release version-bump step rewrote ``v{ANY}`` and
``pdfapps-{ANY}`` unanchored in both .SRCINFO files. A future
historical comment such as ``# since v1.10.0`` would have been
silently rewritten when the next release bumped versions.

Anchor every replacement to the surrounding token (``.tar.gz``
suffix, ``/v.../`` path segment). Verified that both current AUR
.SRCINFOs still receive every required substitution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The drop handler previously checked path.lower().endswith('.pdf')
on the local file. Folder drops returned a directory path that
failed the suffix check and were silently ignored; web URL drops
(http/https/ftp) resolved to an empty toLocalFile() and were also
silently swallowed.

Handle both:
- folder drops iterate every *.pdf (and uppercase *.PDF) and open
  each via _load_and_track;
- web URL drops surface a one-shot translated warning telling the
  user to download the file first.

dragEnterEvent now accepts folder URLs too so the cursor reflects
the drop target.

Adds viewer.drop_url_not_supported (8 locales) — parity verified.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
19 source-level tests covering every fix in PR-F. Where the
production change is observable at module import time (cache name,
helper functions, regex anchors), we hit it directly; where it lives
inside Qt event-loop code (context menus, drop handlers), we read the
source and assert the new wiring is in place. Pair with manual QA on
the next release candidate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@nelsonduarte nelsonduarte added bug Something isn't working enhancement New feature or request labels Jun 9, 2026
Comment thread app/viewer/canvas.py
if backup_path:
with contextlib.suppress(Exception):
os.unlink(backup_path)
backup_path = None
Comment thread app/viewer/canvas.py
if backup_path:
with contextlib.suppress(Exception):
shutil.move(backup_path, self._path)
backup_path = None
Comment thread tests/test_audit_r10.py
import json
from pathlib import Path

import pytest
@nelsonduarte nelsonduarte merged commit 061519e into main Jun 9, 2026
3 checks passed
@nelsonduarte nelsonduarte deleted the fix/audit-r10 branch June 9, 2026 10:40
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