DSpace9.3/Fix OAI double-escaping of metadata values (upstream #10721 regression) - #1403
Merged
milanmajchrak merged 2 commits intoAug 10, 2026
Merged
Conversation
…ression) ItemUtils.sanitize() is applied to every metadata value on its way into the XOAI intermediate document, but the XOAI serializer escapes for XML itself. Escaping twice means any value containing & < > " ' reaches harvesters as literal entity text: a title stored as "Corpus Tecnic de l'IULA" is emitted as "Corpus Tecnic de l&apos;IULA", which decodes to l'IULA. sanitize() now REMOVES the characters XML 1.0 forbids instead of escaping the whole string, which is what its own javadoc always claimed it did. Where the defect comes from: upstream PR DSpace#11139 "fix(DSpace#10721): Sanitize non-characters during OAI indexing" (d8fbe16), ported to dspace-9_x as 6a69e87. dtq-dev (7.6.5) predates it entirely and is unaffected, which is why this only shows on the 9 base. Blast radius is every OAI format built on the xoai document, not just cmdi. Measured on dev-6.pc:8603 (9.3) vs lindat.mff.cuni.cz (CLARIN 7): 11372/LRT-420 title, cmdi and xoai 9.3 Tecnic de l&apos;IULA CLARIN Tecnic de l'IULA 11234/1-3743 dc.description, oai_dc and olac 9.3 en-&gt;fr: 38.2 CLARIN en->fr: 38.2 It also loses data, not only formatting. & and " contain a ';', and the CLARIN crosswalk splits composite fields (local.sponsor, local.contact.person) on ';' -- so the entity's own semicolon is read as a field separator and the rest of the value is discarded: 11234/1-3734 <projectName> from local.sponsor 9.3 SSHOC - Social Sciences &amp <- truncated CLARIN SSHOC - Social Sciences & Humanities Open Cloud 11372/LRT-1661 <affiliation> from local.contact.person 9.3 ...Heinrich-Heine University D\&quot <- truncated CLARIN ...Heinrich-Heine University D\"{u}sseldorf The splitting code is correct and unchanged; no stored data is affected, so no migration or data repair is needed. This is a dissemination-only defect. Cherry-picked from the CLARIN 7.6.7 upgrade branch (upgrade/vanilla-7.6.7), where the identical upstream defect arrived via 7.6.5 -> 7.6.7. (cherry picked from commit c6a1101) The content-disposition half of that source commit is deliberately NOT taken: it edits webui.content_disposition_inline in dspace/config/clarin-dspace.cfg, a key that does not exist on this branch, and is an unrelated 7.6.7 concern. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit replaced escapeXml10 with a regex to stop the double escaping, but escapeXml10 was an AggregateTranslator that ALSO ran UnicodeUnpairedSurrogateRemover, and the regex reproduced only the control-char part. Consequence if that half is missed: a lone surrogate in metadata (a truncated 4-byte character, a mojibake corpus) makes the StAX writer throw "Broken surrogate pair" inside XOAI.index(). Because XOAI.index() catches that per item, the record is SILENTLY DROPPED from the OAI index -- trading a visible double-escape for an invisible missing record. This commit is therefore not optional polish; the two commits must travel together. Measured on the 7.6.7 branch before this fix: 2048 code units broke indexing, against 0 for vanilla. Verified over the whole BMP: 0 XML-1.0-illegal code units leak, 0 legal ones are removed, valid surrogate pairs survive, unpaired ones do not, and XML metacharacters still pass through unescaped. Known and intentional difference from escapeXml10: C0/DEL controls U+007F-U+009F are left as-is instead of being turned into &#NNN; entities. They are legal XML 1.0 characters (only XML 1.1 requires escaping them), so well-formedness is unaffected. Cherry-picked from the CLARIN 7.6.7 upgrade branch (upgrade/vanilla-7.6.7). (cherry picked from commit b284571) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
References
d8fbe16ede(main) → ported todspace-9_xas6a69e87406, both present ondtq-dev-9-base.main,9_x,8_xand7.6.7. Worth filing.Where this fix comes from
This is not new code. Both commits are cherry-picked from our own
upgrade/vanilla-7.6.7branch (author: @milanmajchrak), where the identical upstreamdefect arrived when that branch took vanilla 7.6.7.
-xprovenance lines are in bothcommit messages.
040cddc29dc6a11014107f2fa13263b28457142adtq-dev(7.6.5) is unaffected and needs nothing — it predates upstream DSpace#11139entirely, so
ItemUtils.sanitize()does not exist there. That is why this only shows upon the v9 base.
Deliberately NOT taken from
c6a1101410That source commit also rewrote
webui.content_disposition_inlineindspace/config/clarin-dspace.cfg. Excluded here: that key does not exist inclarin-dspace.cfgon this branch (on v9 it lives indspace.cfg), and it is anunrelated 7.6.7 download-behaviour concern. Only the
ItemUtilshalf is in this PR, sothe diff is one file, +31/-4.
Description
ItemUtils.sanitize()is applied to every metadata value on its way into the XOAIintermediate document — but the XOAI serializer already escapes for XML itself. Escaping
twice means every value containing
&<>"'reaches harvesters as literalentity text.
sanitize()now removes the characters XML 1.0 forbids instead ofescaping the whole string, which is what its own javadoc always claimed it did.
This affects every OAI format built on the xoai document (
cmdi,olac,oai_dc,xoai, and the not-currently-exercisedelg/oai_datacite) — so it breaks CLARIN VLOand OLAC harvesting, not just one crosswalk.
Instructions for Reviewers
List of changes in this PR — all in
dspace-oai/.../xoai/util/ItemUtils.java:StringEscapeUtils.escapeXml10(value)insanitize()with aPatternthatdeletes XML-1.0-illegal code units. Dropped the now-unused
org.apache.commons.text.StringEscapeUtilsimport.escapeXml10removed,including unpaired surrogates. This is the whole point of the second commit and is
not optional polish:
escapeXml10was anAggregateTranslatorthat also ranUnicodeUnpairedSurrogateRemover. A regex covering only the control-char part lets alone surrogate (truncated 4-byte char, mojibake corpus) reach the StAX writer, which
throws "Broken surrogate pair" inside
XOAI.index(). That is caught per item, sothe record is silently dropped from the OAI index — trading a visible bug for an
invisible one. Measured on the 7.6.7 branch before that commit: 2048 code units broke
indexing, vs 0 for vanilla.
"fixed" back to
escapeXml10by a future reader.escapeXml10: C0/DEL controls U+007F–U+009F areleft as-is rather than converted to
&#NNN;. They are legal XML 1.0 characters (onlyXML 1.1 requires escaping them), so well-formedness is unaffected.
Symptom 1 — corrupt display text (227 OAI items)
Measured on
dev-6.pc:8603(9.3, this branch's deployment) againstlindat.mff.cuni.cz(CLARIN 7, production):Symptom 2 — actual data loss (2 OAI items)
&and"contain a;, and the CLARIN crosswalk splits composite fieldson
;. So the entity's own semicolon is read as a field separator and everything afterit is discarded:
The splitting code (
SpecialItemService.getFunding()/getContact(),DCInput.ComplexDefinitions.separator = ";") is correct and untouched — it isbyte-identical to
dtq-dev. Fixing the escaping fixes both symptoms; they are one bug.No stored data is affected.
GET /api/pid/find?id=hdl:...returns byte-identicalmetadata on both instances, so this is dissemination-only — no migration or data repair.
How to test
Important
The corruption is baked into the OAI Solr index, not computed per request. After
deploying, run
[dspace]/bin/dspace oai import -cor nothing will change and thefix will look like a no-op.
Compare each against
https://lindat.mff.cuni.cz/repository/server/oai/requestwithidentifier=oai:lindat.mff.cuni.cz:<handle>.Also worth confirming no records went missing after re-index (the surrogate half):
verb=ListIdentifiers&metadataPrefix=cmdirecord count should not drop.Verification already done
mvn -pl dspace-oai compile checkstyle:check→ BUILD SUCCESS, 0 Checkstyle violations.through, 0 legal ones are removed, valid surrogate pairs survive, unpaired ones do not,
and
& < > " 'all pass through unescaped. Tab/LF/CR, accents and CJK preserved.fix; they reproduce exactly as described.
Expected effect on the REST test suite
Tracked in
dspace-rest-test. This should clear 229 of the 235 differing OAI items.The summary line
FAILED (failures=9, errors=2, skipped=6)will not move — the OAIcomparison is a single test, so shrinking its item count cannot change the counter.
Read the differing-item count, not the test count. The remaining known items are separate
issues (CLARIN allowance link rels; 6 baseline-side normalizer crashes).
Checklist
dtq-dev-9-base) — this is a v9-base fix for an issue specific to that branch.commons-text.sanitize()isprivate staticanddspace-oaihas no existing test forItemUtils. Behaviour was verified by exhaustive BMP sweep instead. Happy to add a package-visible test +ItemUtilsTestif reviewers prefer it in-tree.🤖 Generated with Claude Code