From 4bcc40058e57b77114b8ab922335c269f89dc51f Mon Sep 17 00:00:00 2001 From: Sahana Bogar Date: Thu, 20 Aug 2026 18:20:50 +0530 Subject: [PATCH] stop dropping ENTITY_REFERENCE events from text in XmlTokenStream --- .../dataformat/xml/deser/XmlTokenStream.java | 15 +++- .../xml/stream/EntityReferenceReadTest.java | 90 +++++++++++++++++++ 2 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java diff --git a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java index b5963d38..774bf2b9 100644 --- a/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java +++ b/src/main/java/tools/jackson/dataformat/xml/deser/XmlTokenStream.java @@ -644,6 +644,9 @@ private final String _collectUntilTag() throws XMLStreamException // note: SPACE is ignorable (and seldom seen), not to be included case XMLStreamConstants.CHARACTERS: case XMLStreamConstants.CDATA: + // Only reported if the reader does not replace entity references + // (or could not expand this one): still part of text content + case XMLStreamConstants.ENTITY_REFERENCE: // 17-Jul-2017, tatu: as per [dataformat-xml#236], need to try to... { String str = _getText(_xmlReader); @@ -684,6 +687,7 @@ private final int _skipAndCollectTextUntilTag() throws XMLStreamException // note: SPACE is ignorable (and seldom seen), not to be included case XMLStreamConstants.CHARACTERS: case XMLStreamConstants.CDATA: + case XMLStreamConstants.ENTITY_REFERENCE: { String str = _getText(_xmlReader); if (chars == null) { @@ -705,8 +709,9 @@ private final int _skipAndCollectTextUntilTag() throws XMLStreamException private final String _getText(XMLStreamReader2 r) throws XMLStreamException { + final String text; try { - return r.getText(); + text = r.getText(); } catch (RuntimeException e) { Throwable cause = e.getCause(); if (cause instanceof XMLStreamException xse) { @@ -714,6 +719,14 @@ private final String _getText(XMLStreamReader2 r) throws XMLStreamException } throw e; } + // An entity reference the reader did not (or could not) expand has no + // replacement text to offer: fail rather than silently drop it from content + if (text == null && r.getEventType() == XMLStreamConstants.ENTITY_REFERENCE) { + throw new XMLStreamException("Unexpanded entity reference '&"+r.getLocalName() + +";' in text content (entity not declared, or not replaced by XMLStreamReader)", + r.getLocation()); + } + return text; } /* diff --git a/src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java b/src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java new file mode 100644 index 00000000..cab33124 --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/stream/EntityReferenceReadTest.java @@ -0,0 +1,90 @@ +package tools.jackson.dataformat.xml.stream; + +import java.util.Map; + +import javax.xml.stream.XMLInputFactory; + +import org.junit.jupiter.api.Test; + +import com.ctc.wstx.stax.WstxInputFactory; + +import tools.jackson.core.exc.StreamReadException; + +import tools.jackson.dataformat.xml.XmlFactory; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.xml.XmlTestUtil; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// Readers configured with `IS_REPLACING_ENTITY_REFERENCES` disabled report +// general entity references in text content as `ENTITY_REFERENCE` events. +// `XmlTokenStream` used to ignore those while collecting text, silently +// dropping part of the content: either the replacement text has to be used, +// or (if the reader has none to offer) reading must fail. +public class EntityReferenceReadTest extends XmlTestUtil +{ + private final static String DOC_INTERNAL_ENTITY = + "]>\n" + +"foo&e;bar"; + + private final static String DOC_UNDECLARED_ENTITY = + "foo&e;bar"; + + // Text collected after an END_ELEMENT goes through a different code path + // (mixed content), so cover that one too + private final static String DOC_MIXED_CONTENT = + "]>\n" + +"1foo&e;bar2"; + + private final XmlMapper NON_REPLACING_MAPPER = _nonReplacingMapper(true); + + private final XmlMapper NON_REPLACING_NO_DTD_MAPPER = _nonReplacingMapper(false); + + private static XmlMapper _nonReplacingMapper(boolean supportDTD) { + XMLInputFactory f = new WstxInputFactory(); + f.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.FALSE); + f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); + f.setProperty(XMLInputFactory.SUPPORT_DTD, supportDTD); + return mapperBuilder(XmlFactory.builder().xmlInputFactory(f).build()).build(); + } + + @Test + public void testDeclaredEntityNotReplacedByReader() throws Exception + { + Map result = NON_REPLACING_MAPPER.readValue(DOC_INTERNAL_ENTITY, Map.class); + assertEquals("fooxxbar", result.get("a")); + } + + @Test + public void testDeclaredEntityNotReplacedByReaderMixedContent() throws Exception + { + Map result = NON_REPLACING_MAPPER.readValue(DOC_MIXED_CONTENT, Map.class); + assertEquals("1", result.get("a")); + assertEquals("2", result.get("b")); + assertEquals("fooxxbar", result.get("")); + } + + @Test + public void testUndeclaredEntityNotReplacedByReader() throws Exception + { + // No DTD, so no replacement text for the reader to hand out: must fail, + // not quietly produce "foobar" + StreamReadException e = assertThrows(StreamReadException.class, + () -> NON_REPLACING_NO_DTD_MAPPER.readValue(DOC_UNDECLARED_ENTITY, Map.class)); + verifyException(e, "Unexpanded entity reference '&e;'"); + } + + @Test + public void testDefaultReaderStillReplaces() throws Exception + { + // Default (replacing) reader with DTD support enabled expands the entity + // itself; no change in behavior there + XMLInputFactory f = new WstxInputFactory(); + f.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, Boolean.FALSE); + f.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.TRUE); + XmlMapper mapper = mapperBuilder(XmlFactory.builder().xmlInputFactory(f).build()).build(); + Map result = mapper.readValue(DOC_INTERNAL_ENTITY, Map.class); + assertEquals("fooxxbar", result.get("a")); + } +}