WUT_Computer_Science/scripts/tex_fingerprint.sh
Krzysztof kuhy Rudnicki ffa5dad02a Split 19 over-cap LaTeX reports, proving each PDF is unchanged
The 250-line cap applies to prose too, and the reports were the worst
offenders -- ENUME's projectA.tex alone was 1,876 lines. Each is now a
preamble plus one \input{} per section, which is how a long LaTeX document is
normally organised; not one byte of prose was rewritten.

"The PDF is unchanged" is checked by a script rather than by eye, because
hashing the PDF cannot work: pdflatex stamps a /CreationDate and an /ID into
every build, so two runs of the SAME source already differ. tex_fingerprint.sh
compares what a reader sees instead -- page count plus a hash of the extracted
text -- and it is stable across rebuilds of untouched sources, which is the
property that makes it usable as a gate.

It earned its keep immediately. Both ECOTE reports quote an example LaTeX
document inside an lstlisting block, so \end{document} appears in the middle
of the file; splitting on the FIRST one stranded everything after it. Same
page count, different text, caught automatically. EOPSY lab3 exposed a second
bug: a \section{} title that runs onto the next line.

Result: 19 .tex over the cap -> 1. The survivor,
ELAC/.../03-building-nfa-...tex, is 418 lines of consecutive tikz figures
with no subsection to split on and needs a hand-placed boundary.

Verified: 29 splits across two passes (section, then subsection for the
pieces still over), every one reporting `unchanged` from
scripts/tex_fingerprint.sh --check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-28 19:47:12 +02:00

135 lines
4.0 KiB
Bash
Executable File

#!/bin/bash
#
# Print a content fingerprint of the PDF a .tex file builds to.
#
# Splitting a 1,876-line report into \input{} sections is only safe if the
# document it produces is unchanged, and "unchanged" cannot be checked by
# hashing the PDF: pdflatex stamps /CreationDate and an /ID into every build,
# so two runs of the SAME source already differ. This fingerprints what the
# reader sees instead -- the extracted text and the page count.
#
# Usage:
# scripts/tex_fingerprint.sh <file.tex> print the fingerprint
# scripts/tex_fingerprint.sh --save <file.tex> write it next to /tmp
# scripts/tex_fingerprint.sh --check <file.tex> compare against the saved one
#
# Typical use, per report:
# scripts/tex_fingerprint.sh --save report.tex # before touching it
# ...split it into sections...
# scripts/tex_fingerprint.sh --check report.tex # exits 1 if it moved
set -euo pipefail
readonly SCRIPT_NAME="${0##*/}"
MODE="print"
TEX=""
WORK_DIR=""
cleanup() {
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
rm -rf "$WORK_DIR"
fi
}
trap cleanup EXIT
usage() {
echo "Usage: $SCRIPT_NAME [--save|--check] <file.tex>"
exit 0
}
validate_requirements() {
if [[ -z "$TEX" ]]; then
echo "Error: a .tex file is required" >&2
exit 1
fi
if [[ ! -f "$TEX" ]]; then
echo "Error: no such file: $TEX" >&2
exit 1
fi
local tool
for tool in latexmk pdftotext sha256sum; do
command -v "$tool" > /dev/null 2>&1 || {
echo "Error: $tool is not installed" >&2
exit 1
}
done
}
# Where the saved fingerprint for a given .tex lives. Keyed by the absolute
# path so two reports named report.tex cannot overwrite each other.
saved_path() {
local abs key
abs="$(readlink -f "$TEX")"
key="$(printf '%s' "$abs" | sha256sum | cut -c1-16)"
printf '/tmp/tex_fingerprint_%s' "$key"
}
# Build into a scratch directory so nothing lands beside the source, and a
# stale .aux from an earlier build cannot influence the result.
fingerprint() {
local dir base
dir="$(dirname "$TEX")"
base="$(basename "$TEX")"
WORK_DIR="$(mktemp -d)"
# -bibtex- for the same reason Programming/ZPOB passes it: a report that
# declares a bibliography it never cites makes bibtex exit non-zero, and
# here we only care whether the rendered text moved.
( cd "$dir" && latexmk -pdf -bibtex- -interaction=nonstopmode \
-outdir="$WORK_DIR" "$base" ) > "$WORK_DIR/build.log" 2>&1 || true
local pdf="$WORK_DIR/${base%.tex}.pdf"
if [[ ! -f "$pdf" ]]; then
echo "Error: no PDF produced; see the tail of the build log:" >&2
tail -20 "$WORK_DIR/build.log" >&2
exit 1
fi
local pages text_hash
pages="$(pdftotext "$pdf" - 2> /dev/null | grep -c $'\f' || true)"
text_hash="$(pdftotext -layout "$pdf" - 2> /dev/null | sha256sum | cut -c1-32)"
printf 'pages=%s text=%s\n' "$pages" "$text_hash"
}
main() {
validate_requirements
local fp saved
fp="$(fingerprint)"
case "$MODE" in
print) printf '%s\n' "$fp" ;;
save)
printf '%s\n' "$fp" > "$(saved_path)"
printf 'saved %s %s\n' "$fp" "$TEX"
;;
check)
saved="$(saved_path)"
if [[ ! -f "$saved" ]]; then
echo "Error: nothing saved for $TEX -- run --save first" >&2
exit 1
fi
local before
before="$(cat "$saved")"
if [[ "$before" == "$fp" ]]; then
printf 'unchanged %s %s\n' "$fp" "$TEX"
else
printf 'CHANGED %s\n before: %s\n after: %s\n' \
"$TEX" "$before" "$fp" >&2
exit 1
fi
;;
esac
}
while [[ $# -gt 0 ]]; do
case "$1" in
--save) MODE="save"; shift ;;
--check) MODE="check"; shift ;;
-h | --help) usage ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) TEX="$1"; shift ;;
esac
done
main "$@"