#!/usr/bin/env bash # Phase 4 exit criterion: generated PDFs open cleanly in external viewers. # # No test inside this repository can assert that. Every reader here is an # implementation nigig-pdf shares no code with — qpdf for structure, poppler # for semantics — so a file that satisfies both is not merely self-consistent # with our own parser, which is the failure this exists to rule out. # # The distinction matters. `pdf-document`'s round-trip tests prove we can # read what we wrote; they cannot prove anyone *else* can. A writer and a # reader that share a bug agree perfectly. # # Usage: # ./tools/check-pdf-external-readers.sh # KEEP_PDF=1 ./tools/check-pdf-external-readers.sh # keep the artefacts # # Requires qpdf and poppler-utils. Skips with a clear message when absent, # rather than passing vacuously: # # sudo apt-get install -y qpdf poppler-utils set -Eeuo pipefail IFS=$'\n\t' ROOT="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" KEEP_PDF="${KEEP_PDF:-0}" missing=0 for tool in qpdf pdfinfo pdftotext pdffonts; do command -v "$tool" >/dev/null 2>&1 || { echo "missing: $tool" >&2; missing=1; } done if [ "$missing" = 1 ]; then echo "install qpdf and poppler-utils to run this check" >&2 exit 2 fi WORK="$(mktemp -d "${TMPDIR:-/tmp}/pdf-external.XXXXXXXX")" cleanup() { local status=$? if [ "$KEEP_PDF" = "1" ]; then printf 'kept artefacts in %s\n' "$WORK" >&2 else rm -rf -- "$WORK" fi exit "$status" } trap cleanup EXIT HUP INT TERM PDF="$WORK/sample.pdf" echo "generating the Phase 4 sample" ENCRYPTED="$WORK/encrypted.pdf" cargo run --quiet --manifest-path "$ROOT/crates/apps/pdf/pdf-graphics/Cargo.toml" \ --example generate_sample -- "$PDF" "$ENCRYPTED" fail=0 note() { printf ' %s\n' "$1"; } check() { local label="$1" haystack="$2" needle="$3" if printf '%s' "$haystack" | grep -qF -- "$needle"; then note "ok $label" else note "FAIL $label (expected to find: $needle)" fail=1 fi } echo echo "== qpdf: structural validity ==" # `--check` reports syntax and stream-encoding errors. A warning is not a # pass: qpdf warns where it had to recover, and recovering is exactly what a # stricter viewer will refuse to do. if qpdf_out="$(qpdf --check "$PDF" 2>&1)"; then if printf '%s' "$qpdf_out" | grep -q 'WARNING'; then note "FAIL qpdf reported warnings:" printf '%s\n' "$qpdf_out" | sed 's/^/ /' fail=1 else note "ok no syntax or stream encoding errors" fi else note "FAIL qpdf --check rejected the file:" printf '%s\n' "$qpdf_out" | sed 's/^/ /' fail=1 fi echo echo "== poppler: document semantics ==" info="$(pdfinfo "$PDF" 2>&1)" check "title survives" "$info" "Nigig PDF Phase 4 sample" check "author survives" "$info" "nigig" check "keywords survive" "$info" "phase4" # Three pages: text, the form, and the CFF sample. The count is asserted # exactly rather than as "more than one" — a page silently dropped by a # writer bug is precisely the kind of thing this catches, and it caught the # CFF page being added. check "all three pages are present" "$info" "Pages: 3" check "the AcroForm is recognised" "$info" "AcroForm" echo echo "== poppler: text extraction ==" # Text extraction is the strongest single signal available here: it only # works if the font, its encoding and the content stream all agree. The # em-dash is deliberate — it is outside ASCII, so it exercises the embedded # subset's cmap rather than a lucky byte-for-byte match. text="$(pdftotext "$PDF" - 2>&1)" check "embedded-subset heading extracts" "$text" "Nigig PDF — Phase 4" check "base-14 text extracts" "$text" "base-14 Helvetica" check "form field values extract" "$text" "Ada Lovelace" # Text set in the whole-embedded CFF font. This only extracts if the CFF # program loaded, /Identity-H addressed its glyphs, and /ToUnicode mapped # them back — the whole embedding path in one assertion. check "CFF-set text extracts" "$text" "Hello CFF 123" echo echo "== poppler: embedded fonts ==" # `pdffonts` is the only check here that inspects a font *program* rather # than the file structure. It is what proves a CFF font was written with # the right /FontFile key and descendant subtype: get either wrong and the # font either fails to load or loads as the wrong type, both of which show # up in this table rather than in qpdf --check. fonts="$(pdffonts "$PDF" 2>&1)" check "the subset TrueType font is embedded" "$fonts" "CID TrueType" check "the whole CFF font is embedded" "$fonts" "CID Type 0C" # `emb yes` for both, and `sub` distinguishing them: the TrueType one is a # real subset, the CFF one is the whole program. That column is the visible # consequence of `SubsetFont::subsetted`. if printf '%s' "$fonts" | grep -qE 'CID Type 0C.*[[:space:]]yes[[:space:]]+no[[:space:]]'; then note "ok the CFF font is embedded whole, not claiming to be subset" else note "FAIL the CFF font's embedded/subset columns are wrong" printf '%s\n' "$fonts" | sed 's/^/ /' fail=1 fi echo echo "== qpdf: attachments ==" attach="$(qpdf --list-attachments --verbose "$PDF" 2>&1)" check "the attachment is listed" "$attach" "readme.txt" check "its description survives" "$attach" "About this file" echo echo "== catalogue features ==" # Read as bytes: these are structural keys, and their presence is what a # viewer keys off. `/Metadata` is checked by pdfinfo above where set. for key in /Outlines /Names /EmbeddedFiles /PageLabels /Dests /PageMode \ /ViewerPreferences /AcroForm; do if grep -qa -- "$key" "$PDF"; then note "ok $key present" else note "FAIL $key missing" fail=1 fi done echo echo "== encrypted document (ADR 0024) ==" # The point of these three: a file we encrypt must be openable by an # implementation sharing no code with ours, must refuse the wrong # password, and must not carry its plaintext on disk. enc_check="$(qpdf --password=hunter2 --check "$ENCRYPTED" 2>&1 || true)" check "qpdf opens it with the password" "$enc_check" "No syntax or stream encoding errors" check "it really is AES-256" "$enc_check" "AESv3" if qpdf --password=WRONGPASSWORD --check "$ENCRYPTED" >/dev/null 2>&1; then note "FAIL the wrong password opened the document" fail=1 else note "ok the wrong password is refused" fi enc_text="$(pdftotext -upw hunter2 "$ENCRYPTED" - 2>/dev/null || true)" check "poppler decrypts the content" "$enc_text" "ENCRYPTEDSAMPLE" # The plaintext must not be in the bytes. grep -a because the file is # binary; a match here is a leak, so the sense of the test is inverted. if grep -qa "ENCRYPTEDSAMPLE" "$ENCRYPTED"; then note "FAIL plaintext found in the encrypted file" fail=1 else note "ok no plaintext in the encrypted file" fi echo if [ "$fail" = 0 ]; then echo "all external-reader checks passed" else echo "external-reader checks FAILED" >&2 echo "The file may still be structurally valid: a PDF that qpdf accepts" >&2 echo "and poppler renders incorrectly is the interesting case, and it is" >&2 echo "what these semantic checks are for." >&2 exit 1 fi