fix(tools): confine verify_pdf --dump-text and drop the bare pdftotext allowance

settings.json pre-approves verify_pdf.py with any arguments, and
--dump-text wrote the PDF's text layer to any path it was given,
creating missing parent directories and overwriting existing files.
Reproduced on master through the real CLI: a one-page PDF's text was
written to a new directory outside the repo with no prompt. The CLI now
writes only a .txt under the repo's cv/ or cover_letters/ (where
apply.md Step 5d puts it) and refuses a symlinked target.

Bash(pdftotext:*) is dropped for the same reason: Poppler writes to any
output path, and `pdftotext <file> -` prints any PDF on disk into the
session. verify_pdf.py already wraps it; the documented manual fallback
still works and now asks first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MaverickHQandClaude Opus 5.5 authored and Mads Lorentzen committed 2026-10-27 16:07:33 +01:00
1 parent 9ca456318c
commit bc2c583c44
7 files changed
+91 -5

No files matched your search

+46
View File
@@ -5,12 +5,15 @@ from pathlib import Path
from unittest.mock import patch
from tools.verify_pdf import (
ROOT,
VerificationError,
build_parser,
check_dump_target,
extract_text_layer,
find_non_ascii_date_ranges,
normalize_text,
parse_page_count,
main,
run_tool,
verify_pdf,
)
@@ -331,5 +334,48 @@ class RunToolTests(unittest.TestCase):
run_tool(["pdfinfo", "example.pdf"])
class DumpTargetTests(unittest.TestCase):
"""`.claude/settings.json` pre-approves verify_pdf.py with any arguments, so
`--dump-text` was a prompt-free write to any path on disk (it also created
missing parent directories). The CLI now writes only a .txt under the
repo's cv/ or cover_letters/, the place apply.md Step 5d documents."""
def setUp(self):
self.temp_dir = tempfile.TemporaryDirectory()
self.addCleanup(self.temp_dir.cleanup)
self.root = Path(self.temp_dir.name)
(self.root / "cv").mkdir()
def test_documented_target_is_allowed(self):
self.assertIsNone(check_dump_target(self.root / "cv" / "main_acme_analyst.txt", self.root))
self.assertIsNone(check_dump_target(ROOT / "cv" / "main_acme_analyst.txt"))
def test_targets_outside_the_output_dirs_are_refused(self):
outside = Path(self.temp_dir.name).parent / "elsewhere.txt"
for path, reason in [
(outside, "inside this repo's cv/"),
(self.root / "tools" / "notes.txt", "inside this repo's cv/"),
(self.root / "cv" / ".." / "tools" / "x.txt", "inside this repo's cv/"),
(self.root / "cv" / "main_acme.py", "must name a .txt file"),
(Path.home() / ".zshrc", "must name a .txt file"),
]:
with self.subTest(path=path):
self.assertIn(reason, check_dump_target(path, self.root))
def test_symlink_out_of_cv_is_refused(self):
link = self.root / "cv" / "main_acme.txt"
link.symlink_to(self.root / "outside.txt")
self.assertEqual(check_dump_target(link, self.root), "must not be a symlink")
@patch("tools.verify_pdf.verify_pdf")
def test_cli_refuses_before_extracting_or_writing(self, mock_verify):
target = Path(self.temp_dir.name) / "victim" / "rc.txt"
with patch("sys.stderr"):
code = main(["cv/main_example.pdf", "--dump-text", str(target)])
self.assertEqual(code, 1)
mock_verify.assert_not_called()
self.assertFalse(target.parent.exists(), "the CLI must not create the parent directory")
if __name__ == "__main__":
unittest.main()