TestCDataEndInTextSplit.java

package wstxtest.stream;

import java.io.StringWriter;

import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;

import org.junit.jupiter.api.Test;
import org.codehaus.stax2.XMLStreamReader2;

import com.ctc.wstx.api.WstxInputProperties;

/**
 * Test that the well-formedness check forbidding a literal {@code "]]>"} in
 * character content (XML 1.0/1.1 #2.4) still fires when the sequence is split
 * across the internal input-buffer boundary. Previously the look-back used to
 * spot {@code "]]>"} only inspected the current buffer, so a sequence whose
 * {@code "]]"} ended one buffer and whose {@code '>'} started the next was
 * silently accepted.
 *<p>
 * The complementary case must keep working: {@code "]]"} produced by a general
 * entity followed by a literal {@code '>'} is legitimately quoted (the literal
 * string never appears in the source) and must parse cleanly at every alignment.
 */
public class TestCDataEndInTextSplit extends BaseStreamTest
{
    // small enough that the pad sweep below straddles the boundary at every offset
    private final static int BUF_LEN = 11;

    // Illegal literal "]]>" in element content, swept across all buffer alignments.
    // Includes longer bracket runs ("]]]>", "]]]]>") whose trailing "]]>" must be
    // caught even when two or more brackets end one buffer and "]>" opens the next.
    @Test
    public void testSplitBracketEndContent() throws Exception {
        String[] tails = { "]]>", "]]]>", "]]]]>", "x]]]>" };
        for (String tail : tails) {
            for (int pad = 0; pad <= 3 * BUF_LEN + 8; ++pad) {
                String xml = "<root>" + pad(pad) + tail + "</root>";
                assertRejected("content tail="+tail+" pad="+pad, factory(false, false), xml);
                assertRejected("content-coalescing tail="+tail+" pad="+pad, factory(true, false), xml);
            }
        }
    }

    // Same, through the getText(Writer) sink (readAndWriteText)
    @Test
    public void testSplitBracketEndGetTextWriter() throws Exception {
        for (int pad = 0; pad <= 3 * BUF_LEN + 8; ++pad) {
            // leading 'z' keeps content a single CHARACTERS event
            String xml = "<root>z" + pad(pad) + "]]></root>";
            XMLStreamReader2 sr = (XMLStreamReader2) constructStreamReader(factory(true, false), xml);
            boolean rejected = false;
            try {
                assertTokenType(START_ELEMENT, sr.next());
                sr.next();
                sr.getText(new StringWriter(), false);
                while (sr.next() != END_ELEMENT) { }
            } catch (Exception e) {
                if (!isBracketError(e)) {
                    throw e;
                }
                rejected = true;
            }
            if (!rejected) {
                fail("getText(Writer) pad="+pad+": expected split ']]>' to be rejected");
            }
            sr.close();
        }
    }

    // Legal: a general entity expanding to "]]" then a literal '>' is quoted and
    // must NOT be flagged, regardless of where the boundary falls.
    @Test
    public void testEntityQuotedBracketsAllowed() throws Exception {
        final String dtd = "<!DOCTYPE root [<!ENTITY bb \"]]\">]>";
        for (int pad = 0; pad <= 3 * BUF_LEN + 8; ++pad) {
            String p = pad(pad);
            String xml = dtd + "<root>" + p + "&bb;></root>";
            XMLStreamReader2 sr = (XMLStreamReader2) constructStreamReader(factory(false, true), xml);
            int t;
            while ((t = sr.next()) != START_ELEMENT) {
                ; // skip DTD / prolog
            }
            StringBuilder actual = new StringBuilder();
            while ((t = sr.next()) != END_ELEMENT) {
                if (t == CHARACTERS || t == ENTITY_REFERENCE) {
                    actual.append(sr.getText());
                }
            }
            assertEquals("pad="+pad, p + "]]>", actual.toString());
            sr.close();
        }
    }

    // Legal: single ']' before '>' (and a trailing "]]" not followed by '>') are
    // fine, including across the boundary.
    @Test
    public void testLegalBracketsAllowed() throws Exception {
        String[] bodies = { "]>", "]]", "x]]x", "]]]" };
        for (String body : bodies) {
            for (int pad = 0; pad <= 3 * BUF_LEN + 8; ++pad) {
                String content = pad(pad) + body;
                String xml = "<root>" + content + "</root>";
                XMLStreamReader2 sr = (XMLStreamReader2) constructStreamReader(factory(false, false), xml);
                assertTokenType(START_ELEMENT, sr.next());
                StringBuilder actual = new StringBuilder();
                int t;
                while ((t = sr.next()) != END_ELEMENT) {
                    if (t == CHARACTERS || t == ENTITY_REFERENCE) {
                        actual.append(sr.getText());
                    }
                }
                assertEquals("body="+body+",pad="+pad, content, actual.toString());
                sr.close();
            }
        }
    }

    /*
    ///////////////////////////////////////////////////////////////////////
    // Helper methods
    ///////////////////////////////////////////////////////////////////////
     */

    private void assertRejected(String desc, XMLInputFactory f, String xml) throws Exception {
        XMLStreamReader2 sr = (XMLStreamReader2) constructStreamReader(f, xml);
        try {
            int t;
            while ((t = sr.next()) != END_ELEMENT) {
                if (t == CHARACTERS || t == ENTITY_REFERENCE) {
                    sr.getText();
                }
            }
            fail(desc + ": expected split ']]>' in content to be rejected");
        } catch (Exception e) {
            if (!isBracketError(e)) {
                throw e;
            }
        } finally {
            sr.close();
        }
    }

    // getText() may surface the deferred error lazily (as a RuntimeException), so
    // accept either form as long as it is the "]]>" well-formedness error.
    private static boolean isBracketError(Throwable e) {
        for (Throwable t = e; t != null; t = t.getCause()) {
            String msg = t.getMessage();
            if (msg != null && msg.indexOf("]]>") >= 0) {
                return true;
            }
            if (t.getCause() == t) {
                break;
            }
        }
        return false;
    }

    private static String pad(int len) {
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; ++i) {
            sb.append('x');
        }
        return sb.toString();
    }

    private XMLInputFactory factory(boolean coalescing, boolean supportDTD) throws XMLStreamException {
        XMLInputFactory f = getInputFactory();
        setCoalescing(f, coalescing);
        setReplaceEntities(f, true);
        setValidating(f, false);
        setSupportDTD(f, supportDTD);
        f.setProperty(WstxInputProperties.P_INPUT_BUFFER_LENGTH, Integer.valueOf(BUF_LEN));
        return f;
    }
}