TestXml11RestrictedCharsViaReader.java

package stax2.stream;

import java.io.ByteArrayInputStream;
import java.io.StringReader;

import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;

import org.codehaus.stax2.XMLInputFactory2;
import org.codehaus.stax2.validation.XMLValidationSchema;
import org.codehaus.stax2.validation.XMLValidationSchemaFactory;
import org.junit.jupiter.api.Test;

import stax2.BaseStax2Test;

/**
 * Tests for [woodstox-core]: XML 1.1 "restricted characters" of the C1 control
 * range (U+007F–U+009F, except U+0085 NEL) must be rejected when they
 * appear literally (i.e. not via character references), regardless of whether
 * the document is supplied as an {@code InputStream} or as a {@code Reader}.
 *<p>
 * Before the fix, the check lived only in Woodstox's own byte-decoding Readers,
 * so {@code Reader}-based (or exotic-encoding) input silently accepted these
 * restricted control characters, while the same bytes were rejected when read
 * from an {@code InputStream}. These tests pin the now-consistent behavior;
 * they construct the reader from a {@link java.io.StringReader} (the path that
 * used to bypass the check) and assert rejection.
 */
public class TestXml11RestrictedCharsViaReader extends BaseStax2Test
{
    private final static String XML11_DECL = "<?xml version=\"1.1\"?>";

    // // // Restricted C1 controls: must be rejected when read via a Reader

    @Test
    public void testRestrictedC1InContentViaReader() throws Exception
    {
        // sample across the restricted range: 0x7F (DEL), 0x84, 0x86, 0x90, 0x9F
        for (int c : new int[] { 0x7F, 0x84, 0x86, 0x90, 0x9F }) {
            String doc = XML11_DECL + "<root>A" + ((char) c) + "B</root>";
            assertRejectedViaReader(doc, c);
        }
    }

    @Test
    public void testRestrictedC1InAttributeViaReader() throws Exception
    {
        String doc = XML11_DECL + "<root attr=\"x" + ((char) 0x90) + "y\">z</root>";
        assertRejectedViaReader(doc, 0x90);
    }

    @Test
    public void testRestrictedC1InCDataViaReader() throws Exception
    {
        String doc = XML11_DECL + "<root><![CDATA[A" + ((char) 0x93) + "B]]></root>";
        assertRejectedViaReader(doc, 0x93);
    }

    @Test
    public void testRestrictedC1AcrossBufferBoundaryViaReader() throws Exception
    {
        // Push the restricted char well past the first buffer fill so that the
        // readMore() refill path is exercised, not just the initial readInto().
        StringBuilder sb = new StringBuilder(XML11_DECL).append("<root>");
        for (int i = 0; i < 50000; ++i) {
            sb.append('x');
        }
        sb.append((char) 0x90).append("</root>");
        assertRejectedViaReader(sb.toString(), 0x90);
    }

    // // // Things that must STILL be accepted (no over-rejection)

    @Test
    public void testAllowedCharsViaReaderXml11() throws Exception
    {
        // U+0085 (NEL) and U+00A0 (NBSP) are NOT restricted; plain content too.
        for (int c : new int[] { 0x09, 0x41, 0x85, 0xA0, 0x100 }) {
            String doc = XML11_DECL + "<root>A" + ((char) c) + "B</root>";
            assertAcceptedViaReader(doc);
        }
    }

    @Test
    public void testRestrictedC1AsCharRefAllowedViaReader() throws Exception
    {
        // Restricted chars ARE legal when expressed as character references.
        String doc = XML11_DECL + "<root>A&#x90;B</root>";
        assertAcceptedViaReader(doc);
    }

    @Test
    public void testRestrictedC1AllowedInXml10ViaReader() throws Exception
    {
        // In XML 1.0 the C1 range is ordinary content and must not be rejected.
        String doc = "<?xml version=\"1.0\"?><root>A" + ((char) 0x90) + "B</root>";
        assertAcceptedViaReader(doc);
    }

    // // // Cross-check: InputStream path already behaved this way and is unchanged

    @Test
    public void testRestrictedC1ViaInputStreamStillRejected() throws Exception
    {
        String doc = XML11_DECL + "<root>A" + ((char) 0x90) + "B</root>";
        XMLInputFactory2 f = getInputFactory();
        XMLStreamReader sr = (XMLStreamReader) f.createXMLStreamReader(
                new ByteArrayInputStream(doc.getBytes("UTF-8")));
        try {
            streamThrough(sr);
            fail("Expected XML 1.1 restricted char 0x90 to be rejected for InputStream input");
        } catch (XMLStreamException e) {
            // expected
        }
    }

    // // // Standalone DTD: Reader and InputStream must behave the same

    @Test
    public void testRestrictedC1InStandaloneDTDViaReader() throws Exception
    {
        // Text declaration of an external subset requires 'encoding'
        final String dtd = "<?xml version=\"1.1\" encoding=\"UTF-8\"?>"
                + "<!ENTITY e 'A" + ((char) 0x90) + "B'>";
        XMLValidationSchemaFactory f = XMLValidationSchemaFactory.newInstance(XMLValidationSchema.SCHEMA_ID_DTD);
        try {
            f.createSchema(new StringReader(dtd));
            fail("Expected XML 1.1 restricted char 0x90 in standalone DTD to be rejected when read via a Reader");
        } catch (XMLStreamException e) {
            verifyException(e, "Invalid character 0x90");
        }
        // and same for InputStream, for parity
        try {
            f.createSchema(new ByteArrayInputStream(dtd.getBytes("UTF-8")));
            fail("Expected XML 1.1 restricted char 0x90 in standalone DTD to be rejected when read via an InputStream");
        } catch (XMLStreamException e) {
            verifyException(e, "Invalid character 0x90");
        }
    }

    // // // Helpers

    private void assertRejectedViaReader(String doc, int expectedChar) throws Exception
    {
        XMLInputFactory2 f = getInputFactory();
        XMLStreamReader sr = constructStreamReader(f, doc);
        try {
            streamThrough(sr);
            fail("Expected XML 1.1 restricted char 0x" + Integer.toHexString(expectedChar)
                    + " to be rejected when read via a Reader");
        } catch (XMLStreamException e) {
            // expected
        }
    }

    private void assertAcceptedViaReader(String doc) throws Exception
    {
        XMLInputFactory2 f = getInputFactory();
        XMLStreamReader sr = constructStreamReader(f, doc);
        // Should consume the whole document without throwing:
        streamThrough(sr);
        sr.close();
    }
}