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]>