Review by Losminek on #2539:
- The work cap was checked per value, so very many small /Info or XMP
values could add up without bound. One budget now covers the whole
scrub; past it every further entry or packet counts as matching
(fail-closed). Identical removed runs are searched once.
- unescape looked for ';' through the rest of the text before checking
the 12-byte entity limit, so a packet of bare '&'s was quadratic. It
now looks only at the next 13 characters (200,000 '&': 943 ms -> 5 ms).
- A form XObject removed whole past the depth limit, or when its stream
doesn't decode, never had its text read; both paths now set
removed_cut so the metadata check clears everything instead of
searching incomplete text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>