TestSurrogateEscaping.java
package wstxtest.wstream;
import java.io.*;
import javax.xml.stream.*;
import org.codehaus.stax2.XMLStreamReader2;
import org.junit.jupiter.api.Test;
/**
* Verifies that a supplementary character (one requiring a surrogate pair)
* is escaped as a single character entity, not two, when the target encoding
* can not represent it natively and output goes through a {@code Writer}
* (i.e. {@code BufferingXmlWriter}).
*/
public class TestSurrogateEscaping
extends BaseWriterTest
{
// U+1D11E MUSICAL SYMBOL G CLEF, surrogate pair 0xD834 0xDD1E
private final static String ASTRAL = "����";
// Narrow encodings whose high char sits below the surrogate range, so a
// supplementary char must be escaped (and an unpaired half rejected)
private final static String[] NARROW_ENCODINGS = { "US-ASCII", "ISO-8859-1" };
@Test
public void testSupplementaryCharInTextAndAttr()
throws Exception
{
for (String enc : NARROW_ENCODINGS) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Writer w = new OutputStreamWriter(bos, enc);
XMLStreamWriter sw = getOutputFactory().createXMLStreamWriter(w);
sw.writeStartElement("root");
sw.writeAttribute("v", "a" + ASTRAL + "b");
sw.writeCharacters("c" + ASTRAL + "d");
sw.writeEndElement();
sw.close();
w.flush();
String xml = bos.toString(enc);
// Single, valid entity; never two surrogate-half entities
if (xml.indexOf("𝄞") < 0) {
fail("Expected '𝄞' in output for encoding "+enc+", got: "+xml);
}
if (xml.indexOf("�") >= 0 || xml.indexOf("�") >= 0) {
fail("Output for encoding "+enc+" emitted surrogate-half entities: "+xml);
}
verifyRoundtrip(xml, "a" + ASTRAL + "b", "c" + ASTRAL + "d");
}
}
@Test
public void testSupplementaryCharSplitAcrossCalls()
throws Exception
{
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Writer w = new OutputStreamWriter(bos, "US-ASCII");
XMLStreamWriter sw = getOutputFactory().createXMLStreamWriter(w);
sw.writeStartElement("root");
// Surrogate halves delivered in separate writeCharacters() calls
sw.writeCharacters("\uD834");
sw.writeCharacters("\uDD1E");
sw.writeEndElement();
sw.close();
w.flush();
String xml = bos.toString("US-ASCII");
if (xml.indexOf("𝄞") < 0) {
fail("Expected combined '𝄞' for split surrogate, got: "+xml);
}
verifyRoundtrip(xml, null, ASTRAL);
}
@Test
public void testUnpairedSurrogateRejected()
throws Exception
{
// lone low surrogate -- invalid regardless of narrow encoding
for (String enc : NARROW_ENCODINGS) {
XMLStreamWriter sw = startNarrowDoc(enc);
try {
sw.writeCharacters("\uDD1E");
sw.writeEndElement();
sw.close();
fail("Expected failure on unpaired surrogate ("+enc+")");
} catch (XMLStreamException expected) {
}
}
}
/**
* A high surrogate held for pairing at the end of a text segment must not
* leak into a following, unrelated write. Previously the held half was
* silently combined with the next escaped content (e.g. a different
* element's attribute), moving the character and corrupting output; it now
* must be rejected, matching the byte-backed writers.
*/
@Test
public void testPendingSurrogateDoesNotLeakIntoAttribute()
throws Exception
{
for (String enc : NARROW_ENCODINGS) {
XMLStreamWriter sw = startNarrowDoc(enc);
try {
sw.writeCharacters("a\uD834"); // trailing high surrogate held
sw.writeStartElement("child");
sw.writeAttribute("k", "\uDD1Ez"); // low half of an unrelated value
sw.writeEndElement();
sw.writeEndElement();
sw.close();
fail("Expected failure: pending surrogate leaked into following attribute ("+enc+")");
} catch (XMLStreamException expected) {
}
}
}
/**
* A high surrogate left pending by a text write must be rejected when the
* next operation ends the element rather than continuing character data.
*/
@Test
public void testPendingSurrogateRejectedAtEndElement()
throws Exception
{
for (String enc : NARROW_ENCODINGS) {
XMLStreamWriter sw = startNarrowDoc(enc);
try {
sw.writeCharacters("a\uD834");
sw.writeEndElement();
sw.close();
fail("Expected failure: pending surrogate at end of element ("+enc+")");
} catch (XMLStreamException expected) {
}
}
}
/**
* An attribute value is atomic, so a high surrogate as its final char can
* never be completed and must be rejected immediately (not held).
*/
@Test
public void testTrailingHighSurrogateInAttributeRejected()
throws Exception
{
for (String enc : NARROW_ENCODINGS) {
XMLStreamWriter sw = startNarrowDoc(enc);
try {
sw.writeAttribute("k", "x\uD834");
sw.writeEndElement();
sw.close();
fail("Expected failure: trailing high surrogate in attribute value ("+enc+")");
} catch (XMLStreamException expected) {
}
}
}
private XMLStreamWriter startNarrowDoc(String enc)
throws Exception
{
Writer w = new OutputStreamWriter(new ByteArrayOutputStream(), enc);
XMLStreamWriter sw = getOutputFactory().createXMLStreamWriter(w);
sw.writeStartElement("root");
return sw;
}
private void verifyRoundtrip(String xml, String expAttr, String expText)
throws XMLStreamException
{
XMLStreamReader2 sr = constructNsStreamReader(xml, true);
assertTokenType(START_ELEMENT, sr.next());
if (expAttr != null) {
assertEquals(expAttr, sr.getAttributeValue(0));
}
assertTokenType(CHARACTERS, sr.next());
assertEquals(expText, getAndVerifyText(sr));
assertTokenType(END_ELEMENT, sr.next());
sr.close();
}
}