diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 3ab3ce0f..1bd0772a 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -189,3 +189,5 @@ Christian Beikov (@beikov) * Fixed #899: Return `null` from `nextStringValue()` at end-of-input (instead of throwing `IllegalStateException`) (3.3.0) + * Fixed #903: Split embedded `]]>` across CDATA sections when serializing + (3.3.0) diff --git a/release-notes/VERSION b/release-notes/VERSION index a108056f..e6244306 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -33,6 +33,8 @@ Version: 3.x (for earlier see VERSION-2.x) #899: Return `null` from `nextStringValue()` at end-of-input (instead of throwing `IllegalStateException`) (fix by @Sahana2524) +#903: Split embedded `]]>` across CDATA sections when serializing + (fix by @Sahana2524) 3.2.2 (14-Aug-2026) diff --git a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java index 1fde0228..23974075 100644 --- a/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java +++ b/src/main/java/tools/jackson/dataformat/xml/ser/ToXmlGenerator.java @@ -856,7 +856,7 @@ public JsonGenerator writeString(String text) throws JacksonException // but for now, let's just make sure structure is correct //if (_xmlPrettyPrinter != null) { ... } if(_nextIsCData) { - _xmlWriter.writeCData(text); + StaxUtil.writeCData(_xmlWriter, text); } else { _xmlWriter.writeCharacters(text); } @@ -867,7 +867,7 @@ public JsonGenerator writeString(String text) throws JacksonException } else { _xmlWriter.writeStartElement(_nextName.getNamespaceURI(), _nextName.getLocalPart()); if(_nextIsCData) { - _xmlWriter.writeCData(text); + StaxUtil.writeCData(_xmlWriter, text); } else { _xmlWriter.writeCharacters(text); } @@ -892,7 +892,7 @@ public JsonGenerator writeString(char[] text, int offset, int len) throws Jackso } else if (checkNextIsUnwrapped()) { // should we consider pretty-printing or not? if(_nextIsCData) { - _xmlWriter.writeCData(text, offset, len); + StaxUtil.writeCData(_xmlWriter, text, offset, len); } else { _xmlWriter.writeCharacters(text, offset, len); } @@ -903,7 +903,7 @@ public JsonGenerator writeString(char[] text, int offset, int len) throws Jackso } else { _xmlWriter.writeStartElement(_nextName.getNamespaceURI(), _nextName.getLocalPart()); if(_nextIsCData) { - _xmlWriter.writeCData(text, offset, len); + StaxUtil.writeCData(_xmlWriter, text, offset, len); } else { _xmlWriter.writeCharacters(text, offset, len); } diff --git a/src/main/java/tools/jackson/dataformat/xml/util/DefaultXmlPrettyPrinter.java b/src/main/java/tools/jackson/dataformat/xml/util/DefaultXmlPrettyPrinter.java index 568d8de8..330c1675 100644 --- a/src/main/java/tools/jackson/dataformat/xml/util/DefaultXmlPrettyPrinter.java +++ b/src/main/java/tools/jackson/dataformat/xml/util/DefaultXmlPrettyPrinter.java @@ -290,7 +290,7 @@ public void writeLeafElement(XMLStreamWriter2 sw, } sw.writeStartElement(nsURI, localName); if(isCData) { - sw.writeCData(text); + StaxUtil.writeCData(sw, text); } else { sw.writeCharacters(text); } @@ -309,7 +309,7 @@ public void writeLeafElement(XMLStreamWriter2 sw, } sw.writeStartElement(nsURI, localName); if(isCData) { - sw.writeCData(buffer, offset, len); + StaxUtil.writeCData(sw, buffer, offset, len); } else { sw.writeCharacters(buffer, offset, len); } diff --git a/src/main/java/tools/jackson/dataformat/xml/util/StaxUtil.java b/src/main/java/tools/jackson/dataformat/xml/util/StaxUtil.java index 5b4fd1f1..47adf2fa 100644 --- a/src/main/java/tools/jackson/dataformat/xml/util/StaxUtil.java +++ b/src/main/java/tools/jackson/dataformat/xml/util/StaxUtil.java @@ -5,6 +5,8 @@ import javax.xml.stream.*; +import org.codehaus.stax2.XMLStreamWriter2; + import tools.jackson.core.*; import tools.jackson.core.exc.StreamReadException; import tools.jackson.core.exc.StreamWriteException; @@ -36,6 +38,46 @@ private static Throwable _unwrap(Throwable t) { return t; } + /** + * Writes {@code text} as one or more CDATA sections. XML does not allow the + * sequence {@code "]]>"} inside a CDATA block, so where it occurs the value is + * split: the {@code "]]"} ends one section and the {@code ">"} starts the next. + * A coalescing reader (the default for this module) reads the pieces back as + * the original text, so a value containing {@code "]]>"} round-trips instead of + * making the underlying Stax writer reject it. + */ + public static void writeCData(XMLStreamWriter2 sw, String text) + throws XMLStreamException + { + int ix = text.indexOf("]]>"); + if (ix < 0) { + sw.writeCData(text); + return; + } + int start = 0; + do { + // keep the "]]" in this section, push the ">" into the next one + sw.writeCData(text.substring(start, ix + 2)); + start = ix + 2; + ix = text.indexOf("]]>", start); + } while (ix >= 0); + sw.writeCData(text.substring(start)); + } + + public static void writeCData(XMLStreamWriter2 sw, char[] buffer, int offset, int len) + throws XMLStreamException + { + // Common case has no "]]>" in range, so avoid allocating a String for it + final int end = offset + len; + for (int i = offset; i < end - 2; ++i) { + if (buffer[i] == ']' && buffer[i + 1] == ']' && buffer[i + 2] == '>') { + writeCData(sw, new String(buffer, offset, len)); + return; + } + } + sw.writeCData(buffer, offset, len); + } + private static String _message(Throwable t1, Throwable t2) { String msg = t1.getMessage(); if (msg == null) { diff --git a/src/test/java/tools/jackson/dataformat/xml/ser/CDataEndMarkerSerTest.java b/src/test/java/tools/jackson/dataformat/xml/ser/CDataEndMarkerSerTest.java new file mode 100644 index 00000000..865db3a4 --- /dev/null +++ b/src/test/java/tools/jackson/dataformat/xml/ser/CDataEndMarkerSerTest.java @@ -0,0 +1,49 @@ +package tools.jackson.dataformat.xml.ser; + +import org.junit.jupiter.api.Test; + +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.xml.XmlTestUtil; +import tools.jackson.dataformat.xml.annotation.JacksonXmlCData; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +// Values written as CDATA must survive serialization even when they contain +// the "]]>" sequence that closes a CDATA section. +public class CDataEndMarkerSerTest extends XmlTestUtil +{ + static class CDataBean + { + @JacksonXmlCData + public String value; + + public CDataBean() { } + public CDataBean(String v) { value = v; } + } + + private final XmlMapper MAPPER = newMapper(); + + @Test + public void testCDataWithEndMarkerRoundTrip() throws Exception + { + _roundTrip("a]]>b"); + _roundTrip("]]>"); + _roundTrip("]]>]]>"); + _roundTrip("trailing]]>"); + _roundTrip("tail"); + } + + @Test + public void testCDataWithoutEndMarkerUnchanged() throws Exception + { + String xml = MAPPER.writeValueAsString(new CDataBean("plain")); + assertEquals("", xml); + } + + private void _roundTrip(String value) throws Exception + { + String xml = MAPPER.writeValueAsString(new CDataBean(value)); + CDataBean result = MAPPER.readValue(xml, CDataBean.class); + assertEquals(value, result.value, "round-trip failed for XML: " + xml); + } +}