open-standards/openpay/crates/op-screening/src
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
..
audit.rs op-screening: the audit log can outlive the process that wrote it 2026-10-17 09:35:49 -04:00
bsa.rs Ingest batch 3: 9 more crates across openpay / smart-byte / eoc 2026-05-24 01:09:35 -04:00
error.rs Ingest batch 3: 9 more crates across openpay / smart-byte / eoc 2026-05-24 01:09:35 -04:00
lib.rs op-screening: the audit log can outlive the process that wrote it 2026-10-17 09:35:49 -04:00
lists.rs openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit 2026-10-15 20:47:42 -04:00
matching.rs Ingest batch 3: 9 more crates across openpay / smart-byte / eoc 2026-05-24 01:09:35 -04:00
normalize.rs Ingest batch 3: 9 more crates across openpay / smart-byte / eoc 2026-05-24 01:09:35 -04:00
screener.rs openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit 2026-10-15 20:47:42 -04:00
storage.rs openpay: a PEP match is recorded as a PEP match, not an OFAC sanctions hit 2026-10-15 20:47:42 -04:00
updater.rs trx, screening: resolve XML entity references, which quick-xml 0.41 now reports as their own event 2026-10-22 12:45:52 -04:00