open-standards/openpay/crates
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Transaction Science 4d7edf3b36 trx, screening: resolve XML entity references, which quick-xml 0.41 now reports as their own event
The quick-xml 0.41 bump did not just rename `BytesText::unescape`. 0.41 reports
every entity reference — `&`, `'`, `&` — as its own
`Event::GeneralRef` instead of folding it into the surrounding text. Both XML
readers here matched only `Event::Text`, so after the bump each reference was
dropped **silently**: the document still parsed, every existing test still
passed, and the values were wrong.

That is the dangerous shape of this change, and it is why it needed more than a
compile fix:

  - `op-screening` parses the OFAC/EU/UN sanctions lists. An entry published as
    "Smith & Co" was being stored as "Smith  Co" — a name that no longer matches
    the party it names, with nothing downstream able to tell. A screening miss
    reads exactly like a clean screen.
  - `jouleclaw-transform-trx` carries JSON property values through TRX XML. The
    quotes arrive escaped, so dropping the references turned `{"k":1}` into
    `{k:1}`, which fails to parse and falls back to being stored as a bare
    string. One round-trip test caught this; nothing else did.

Both readers now accumulate an element's character data across events and
resolve each reference: a numeric reference through `resolve_char_ref`, a named
one through `resolve_predefined_entity`. TRX carries no DTD, so an unknown name
there is refused rather than dropped. The sanctions readers keep an unknown
reference in its literal `&name;` form, so the anomaly stays visible in the
record instead of silently altering a name.

`trim_text(true)` had to go with it, on all four readers. Trimming is applied
per text event, so once a run is split by a reference the trim eats the spaces
on either side of it — "Smith & Co" became "Smith&Co", which the new tests
caught. The assembled value is trimmed once where it is consumed instead; the
screening readers already did exactly that at every consumer, making the
reader-level trim redundant there even before this change.

Tests: the fixtures had no entity in them anywhere, which is precisely why a
silent data-corruption bug could pass CI. Added, failing first: a sanctioned
name carrying `&` and one carrying `&` (different code paths — a named
predefine and a numeric reference), entity references in TRX text asserted
against hand-written XML rather than our own writer's escaping, and a refusal
for an unresolvable name.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-22 12:45:52 -04:00
..
op-3ds2
op-batch
op-bin
op-bnpl
op-cli
op-connect
op-core
op-crypto-extra
op-dispute
op-dispute-evidence
op-driver-sdk
op-emv
op-ewallets-asia
op-ffi-jni
op-ffi-swift
op-fraud
op-fraud-graph
op-fx
op-graph
op-iso8583
op-iso20022
op-ledger
op-mobile-wallets
op-openbanking
op-orchestrator
op-payouts
op-rails-a2a
op-rails-card
op-rails-crypto
op-rails-x402
op-reconciliation
op-refund
op-revrec
op-routing
op-screening
op-server
op-settlement
op-statements
op-subscriptions
op-tax
op-vault
op-wasm
op-webhook