Commit Graph
4 Commits
Author SHA1 Message Date
aiongg 9526363152 Build each crate's integration tests as one binary (#4087)
* Build each crate's integration tests as one binary

Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos,
crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and
the support modules move into tests/it/ too. Crates with a single test
file keep it. The test executables go from 150 to 52, and every crate
runs the same number of tests as before.

ui-egui perf keeps its own binary (own process): its frame-time budgets
would charge each measurement for the other tests running beside it.

What the move needs:
- include_str!/include_bytes! and #[path] paths gain one "../".
- ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file
  ones (content, document_links, editing, fill_sign, view), so one
  process creates one wgpu device at a time. The drop-line test in ui.rs
  and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and
  CRAFT_UI_DOCKING_DIR, which render without holding it, take it too.
- pdfcraft winit_wayland_scroll: the vendored winit source it compiles
  names crate::event, so that shim lives in main.rs.
- parity evidence names the new paths.

* Name the one-binary test paths in docs and comments

- READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs.
- The oracle README's example filters with --test it encryption::qpdf_agrees.
- pdfcraft's GPU lock comment points at the UI tests' shared lock in
  crates/ui-egui/tests/it/main.rs.
2026-10-22 11:28:58 -04:00
ecaa2980ba crypt: say how the quoted vectors reproduce, and name what nothing verifies
FredApps' approval on #2359 carried one non-blocking nit that is a real defect: the header told
contributors to "regenerate with make-oracle-fixtures.py in the workspace root" - a script that
exists only in my checkout, next to a venv (.dc-tools/) likewise outside the repository. A
contributor following it finds nothing, and this repository has no committed generator for these fixtures
(crates/oracle treats qpdf strictly as an external process, and CLAUDE.md keeps the product and build in
Rust).

The header now carries a self-contained recipe, measured on this host rather than asserted:
  * one blank 200x200 page, pikepdf 9.11 / qpdf core 12.2.0, Encryption(owner=OWNER,
    user=USER, allow=Permissions(extract=False), R=<revision>, aes=…, metadata=…),
    save(static_id=True), then read /Encrypt. At revisions 2-3 `aes=False, metadata=False` are
    FORCED - pikepdf refuses AES ("Cannot encrypt with AES when R < 4") and refuses encrypted
    metadata ("Cannot encrypt metadata when R < 4") - so leaving those flags to the reader makes
    the export-grade row unreproducible. My first draft of this comment made exactly that mistake.
  * Reproduction scope, by revision: each of the four deterministic rows quotes eight dictionary
    fields (32 per comparison leg). Revisions 2 and 3 reproduce as whole files - fresh writes today
    carry the same sha256 (63f2d95b…, 39afbdd5…) as the October originals these literals came
    from. Revision 4 reproduces all eight fields, but its file moves by exactly one ~32-byte
    stream because /CFM /AESV2 prepends a fresh random IV (ISO 32000-2 §7.6.4); the dictionary
    and /ID are unchanged. Revision 6 reproduces structurally only (/ID, R/V/Length/P and the
    metadata flag); its salts are new each run.

Running the generator at the reviewer's prompting exposed that it had not been executed since its
last edit: unless both aes=False and metadata=False are forced pikepdf refuses R<4, checking metadata first, so it wrote 4 of the 6 fixtures - revisions 2
and 3 missing, including this file's headline export-grade row - while printing a "skipped:" line
and exiting 0 regardless. It writes 6 of 6 now, and every quoted field reproduces as above.

The overstatements flagged during review are corrected in this file; they are quoted verbatim,
each with the basis for its new wording, in the PR description rather than here - commit messages
cannot be edited after a push, descriptions can. Two further fixes live in this message alone: an
ISO subsection written as §7.6.4.3, a form this repository does not cite (it uses bare §7.6.4 or the
deeper §7.6.4.x.y), and one clause duplicated mid-cycle.

What nothing here verifies, stated instead of implied - established by deliberately breaking the
table (4 mutations: 2 red, 2 green): authentication never reads /Perms (a corrupted one fails
only this file's own decrypt), and renaming a row's /StmF to Identity or replacing its
crypt-filter set leaves all three tests green, so the filter names are transcription held to
review. Pinning them, and re-deriving these rows through pdfcraft_oracle::qpdf() on the runner
that installs qpdf, belong in a follow-up. Also recorded honestly: the revision-6 fixtures these
rows were copied from are gone - my own regeneration overwrote them before I archived the folder -
so those two rows rest on internal consistency plus this record, not a surviving artifact. Revisions 2 and 3 carry their own proof, byte for byte (sha256 63f2d95b… and
39afbdd5…, October and today); revision 4 reproduces every quoted field, with only the one
IV-carrying stream moving.

Scope: comments plus one assertion message (a revision 3 /U description that wrongly claimed qpdf
leaves sixteen zero bytes; its condition, len == 64, is unchanged). No assertion logic, fixture,
dependency or product code touched. Parent remains the approved cdbce8ac, so this lands as a
fast-forward rather than a rewrite of the head FredApps reviewed. Gates at this tip
(upstream/main 22bedbf2; branch 326 behind, trial merge clean): cargo fmt --all -- --check rc=0;
clippy -p pdfcraft-crypt --all-targets -D warnings rc=0; cargo test -p pdfcraft-crypt
--no-fail-fast 28 passed / 0 failed (17 lib + the 8 from #2358 plus these 3); cargo xtask layers
OK, 37 crates; cargo xtask assets ok (272 repository, 6 bundled by dependencies, 22 fetched at
build time). CI had not run on this tip when this was written - the commit was local.

Co-authored-by: Qwen3.8-Flash-Next-oQ4e-WLSRefit-MTP <noreply@qwen.ai>
Co-authored-by: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-10-20 21:33:55 -07:00
Nick Nyssen cdbce8ac17 crypt: authenticate against encryption numbers we did not compute
Adds crates/crypt/tests/qpdf_literals.rs (305 lines): three tests that hand our security
handler /O, /U, /OE, /UE and /Perms values from outside implementations, then require our
reader to authenticate them and report the intended role. Tests only - no product code, no
new dependency; PDFs stay outside the repository and only quoted hex is committed.

Why outsiders wrote these numbers: create() and open() share one derivation, so a
wrong-but-self-consistent implementation passes every internal test. Two review rounds proved
it by mutation - several key-derivation breaks stayed green in both suites because writer and
reader drifted together. Quoting bytes we did not compute breaks the circle: open() recomputes
/U (or the revision 6 hash chain) and compares it to what was stored, so a divergent derivation
cannot authenticate.

Six cases come from qpdf core 12.2.0 via pikepdf (R2/R3, R4 in both metadata modes, R6 in both).
Revisions 2-4 are deterministic given /ID and passwords, so the generator reproduces their quoted bytes
byte for byte (verified). Revision 6 salts are fresh on every run by design: these literals match the
fixtures as captured, and re-running the generator will produce different - equally valid - revision 6
numbers, so compare R/V/Length structurally there and re-quote if you regenerate. One
case is HAND-DERIVED, labelled as such in the source: an independent re-implementation of ISO
Algorithms 2/3 at n = 5, cross-validated by having that same routine reproduce qpdf's n=16 /O
byte for byte. It exists because no conforming producer emits R3 at 40 bits - qpdf writes 128
there and this crate's create() maps Rc4_40 to revision 2 - yet the derivation loop only runs
for r >= 3, so without a crafted row its truncation is unobservable and mutating it changes
behaviour that nothing catches.

Measured effect, same battery, upstream --lib run separately each time:
  r6-hash-selector      (%3 collapsed to %2)      CAUGHT here; upstream stays green
  r6-sha-arms-swapped   (SHA-384 <-> SHA-512)     CAUGHT here; upstream stays green
  r6-selector-window    (sums 17 bytes not 16)    CAUGHT here; upstream stays green
  r4-key-chain-truncation (loop hashes whole digest) CAUGHT here, via the crafted row;
      upstream stays green. Pristine 3/3 pass, mutated 2 passed / 1 FAILED - verified twice.
Still invisible, named rather than glossed: r6-round-floor (minimum rounds 64 -> 63) - the
terminator does not separate the two floors on these inputs; /OE,/UE length loosening and the
/Perms filler source, both unreached here.

Corrections carried from review: qpdf's advice against a static /ID does not weaken these
revision 6 vectors, because key_r6 never receives the file /ID at all; it matters only to the
revision 2-4 paths, where we quote /ID literally so the numbers stay exact. And no ATTRIBUTION
entry is added, matching crates/crypt/src/tests.rs:237 which already quotes qpdf vectors
unattributed; cargo xtask assets stays ok.

Gates on a clean tree: cargo test -p pdfcraft-crypt 28 passed / 0 failed (17 lib + the 8 properties from #2358, now merged,
plus 3 oracle tests - one
per concern - role authentication across seven dictionaries, metadata-flag key separation, /Perms
decode);
clippy --all-targets -D warnings clean; cargo fmt --check clean; xtask layers OK (35 crates).

Integrity note: the literals in this commit were checked field by field against the PDFs on disk -
27 of 27 fields quoted verbatim, no transcription drift. A regression test for that check belongs in the
generator's output comparison rather than here, since it needs pikepdf.
2026-10-15 16:41:20 -07:00
amoisttowe1andNick Nyssen 21e7903ff2 crypt: property tests for encrypted content, crypt filters and damaged ciphertext (#2358)
Adds crates/crypt/tests/proptests.rs (464 lines): eight properties over the
payload and ciphertext side of the standard security handler. Tests only - no
product code, no new dependency (proptest was already a dev-dependency here).

Upstream's src/tests.rs covers this handler well with fixed vectors (including
files qpdf wrote) and one property over malformed dictionaries. Its encrypted
round trips run three shapes - a 30-byte text, a 1000-byte ramp and a 10-byte
attachment. Not walked before: empty content, block boundaries (15/17), bytes
handed to the decoder at lengths it never encrypted, key widths only key_len
decides (5/16/16/32), /Perms verified by nothing, and Algorithm 2 step f for
unencrypted metadata on revisions below 4.

Evidence: 25 one-line breakages of crypt/src, each run against these properties
AND upstream --lib; restored and byte-compared per row; verdicts from cargo exit
codes. 16 seen here, several by nothing else in the repository (/Num fold width
both directions, RC4-40 key length, /EFF selection, Identity name guard, IV
scoping, step f, /Perms tail). Named blind spots: four hash_r6 breaks stay
green because create() and open() share that function - the companion branch
quotes qpdf's own numbers to close them.

Gates: cargo test -p pdfcraft-crypt 25 passed / 0 failed; clippy --all-targets
-D warnings clean; fmt clean. Default 256 cases, nothing lowered.

Co-authored-by: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com>
2026-10-15 02:57:44 -04:00