name: PDF engine on: push: paths: - 'crates/apps/pdf/**' - 'tools/test-rust-clean.sh' - 'rust-toolchain.toml' - '.forgejo/workflows/pdf.yml' pull_request: paths: - 'crates/apps/pdf/**' - 'tools/test-rust-clean.sh' - 'rust-toolchain.toml' - '.forgejo/workflows/pdf.yml' jobs: # The engine crates parse untrusted input and carry no UI dependency, so # they run everywhere and are the gate that must never be skipped. engine: runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v4 # Runs pdf-cos, pdf-document and pdf-graphics bottom-up under the # toolchain declared in rust-toolchain.toml, then rustfmt --check and # clippy -D warnings. Includes the corpus and robustness suites. - name: Engine tests, fmt and clippy run: TEST_TARGET=pdf ./tools/test-rust-clean.sh - name: Reject whitespace errors run: git diff --check # The corpus is generated by a checked-in script so every fixture is # reviewable. If the generator and the fixtures disagree, one of them # was edited by hand and the corpus no longer means what it says. - name: Corpus fixtures match their generator run: | set -e python3 crates/apps/pdf/tests/corpus/generate.py git diff --exit-code -- crates/apps/pdf/tests/corpus \ || { echo 'corpus fixtures differ from generate.py output'; exit 1; } # Golden render expectations must be regenerated deliberately, never # silently by a test run. - name: Golden render expectations are unchanged run: git diff --exit-code -- crates/apps/pdf/pdf-graphics/tests/golden # The scheduled fuzz job enumerates targets with `cargo fuzz list`, # which reads the manifest. A target file added without its [[bin]] # entry would therefore never be fuzzed while looking present in the # tree. This runs per-push so the omission is caught immediately # rather than by a silent no-op months later. - name: Every fuzz target file is declared in the manifest run: | set -e cd crates/apps/pdf/pdf-cos/fuzz files="$(ls fuzz_targets/*.rs | xargs -n1 basename | sed 's/\.rs$//' | sort)" declared="$(grep -A1 '^\[\[bin\]\]' Cargo.toml \ | grep 'name = ' | sed 's/name = //; s/"//g' | sort)" if [ "$files" != "$declared" ]; then echo 'fuzz_targets/*.rs and the [[bin]] entries disagree:' >&2 diff <(echo "$declared") <(echo "$files") >&2 || true echo 'an undeclared target is never fuzzed' >&2 exit 1 fi echo "all $(echo "$files" | wc -l) fuzz targets are declared" # Boundary enforcement: the engine crates must never reach for the UI # framework. This checks real manifests and imports, not doc comments. - name: Engine crates must not depend on Makepad run: | set -e for crate in pdf-cos pdf-document pdf-graphics; do dir="crates/apps/pdf/$crate" if grep -n -i 'makepad' "$dir/Cargo.toml"; then echo "Makepad dependency declared in $crate" exit 1 fi if grep -rn --include='*.rs' \ -E '^[[:space:]]*(use|extern crate)[[:space:]]+makepad' "$dir"; then echo "Makepad imported in $crate" exit 1 fi done echo 'no Makepad dependency in the PDF engine crates' # Rule 5: the viewer emits actions, the host acts on them. A direct # process launch or URL open from the engine is a policy violation. - name: Engine must not launch processes or open URLs run: | set -e if grep -rn --include='*.rs' \ -E 'std::process::Command|open::that|webbrowser::' \ crates/apps/pdf/pdf-cos crates/apps/pdf/pdf-document \ crates/apps/pdf/pdf-graphics; then echo 'the PDF engine must not launch anything' exit 1 fi echo 'no process or URL launching in the engine' # The Makepad integration needs system GUI libraries, so it is a separate # job: a missing X11 package must not be reported as a PDF regression. makepad-integration: runs-on: ubuntu-latest timeout-minutes: 45 steps: - uses: actions/checkout@v4 - name: Install Makepad's system dependencies run: | sudo apt-get update -qq sudo apt-get install -y -qq \ libwayland-dev libxkbcommon-dev libxcursor-dev \ libpulse-dev libx11-dev libssl-dev libasound2-dev pkg-config # Adds pdf-makepad: the interactive widget, its event routing and the # Phase 4 exit-criterion tests. - name: Widget tests, fmt and clippy run: TEST_TARGET=pdf-ui ./tools/test-rust-clean.sh # The Studio-driven UI tests are #[ignore] by default because they need # a Studio hub. Where one is available they are the only coverage of # real Makepad event delivery, so run them rather than let them rot. # Not allowed to fail silently: if the hub is present they must pass. - name: Studio UI tests continue-on-error: true run: | set -e sudo apt-get install -y -qq xvfb cd crates/apps/pdf/pdf-makepad xvfb-run -a cargo test --test ui -- --ignored --test-threads=1 # Coverage-guided fuzzing needs nightly and libFuzzer. It runs on a # schedule rather than per-push so it cannot block a merge, but the # targets are compile-checked on every push by the engine job's clippy. fuzz: runs-on: ubuntu-latest # Sized for every target in fuzz_targets/ at 180s each, plus build time. timeout-minutes: 75 # Only on demand or on a schedule: a five-minute run per target is too # slow to gate every push, and a fuzzer finding nothing proves little. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' steps: - uses: actions/checkout@v4 - name: Install nightly and cargo-fuzz run: | set -e curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal --default-toolchain nightly . "$HOME/.cargo/env" cargo install cargo-fuzz --locked # Phase 6 step 6.3: zero panics on arbitrary input. # # The target list is derived from `cargo fuzz list`, never written out # here. A hardcoded list silently stopped covering four targets # (parse_revision_chain, decrypt, eval_function, parse_colorspace) as # they were added, which is the failure this loop must not repeat: # a fuzz job that skips a target reports success for code it never ran. - name: Fuzz the parsers run: | set -e . "$HOME/.cargo/env" cd crates/apps/pdf/pdf-cos/fuzz targets="$(cargo +nightly fuzz list)" if [ -z "$targets" ]; then echo "no fuzz targets found - the job would pass vacuously" >&2 exit 1 fi echo "fuzzing $(echo "$targets" | wc -l) targets:" echo "$targets" for target in $targets; do echo "== fuzzing $target ==" cargo +nightly fuzz run "$target" -- -max_total_time=180 done