/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package libcore.xml; import java.io.StringReader; import java.util.Arrays; import junit.framework.TestCase; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; /** * Test doctype handling in pull parsers. */ public abstract class PullParserDtdTest extends TestCase { private static final int READ_BUFFER_SIZE = 8192; /** * Android's Expat pull parser permits parameter entities to be declared, * but it doesn't permit such entities to be used. */ public void testDeclaringParameterEntities() throws Exception { String xml = "<!DOCTYPE foo [" + " <!ENTITY % a \"android\">" + "]><foo></foo>"; XmlPullParser parser = newPullParser(xml); while (parser.next() != XmlPullParser.END_DOCUMENT) { } } public void testUsingParameterEntitiesInDtds() throws Exception { assertParseFailure("<!DOCTYPE foo [" + " <!ENTITY % a \"android\">" + " <!ENTITY b \"%a;\">" + "]><foo></foo>"); } public void testUsingParameterInDocuments() throws Exception { assertParseFailure("<!DOCTYPE foo [" + " <!ENTITY % a \"android\">" + "]><foo>&a;</foo>"); } public void testGeneralAndParameterEntityWithTheSameName() throws Exception { String xml = "<!DOCTYPE foo [" + " <!ENTITY a \"aaa\">" + " <!ENTITY % a \"bbb\">" + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("aaa", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testInternalEntities() throws Exception { String xml = "<!DOCTYPE foo [" + " <!ENTITY a \"android\">" + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("android", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testExternalDtdIsSilentlyIgnored() throws Exception { String xml = "<!DOCTYPE foo SYSTEM \"http://127.0.0.1:1/no-such-file.dtd\"><foo></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testExternalAndInternalDtd() throws Exception { String xml = "<!DOCTYPE foo SYSTEM \"http://127.0.0.1:1/no-such-file.dtd\" [" + " <!ENTITY a \"android\">" + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("android", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testInternalEntitiesAreParsed() throws Exception { String xml = "<!DOCTYPE foo [" + " <!ENTITY a \"&#65;\">" // & expands to '&', A expands to 'A' + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("A", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testFoolishlyRecursiveInternalEntities() throws Exception { String xml = "<!DOCTYPE foo [" + " <!ENTITY a \"&#38;#38;#38;\">" // expand & to '&' only twice + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("&#38;", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } /** * Test that the output of {@code &} is parsed, but {@code &} isn't. * http://www.w3.org/TR/2008/REC-xml-20081126/#sec-entexpand */ public void testExpansionOfEntityAndCharacterReferences() throws Exception { String xml = "<!DOCTYPE foo [" + "<!ENTITY example \"<p>An ampersand (&#38;) may be escaped\n" + "numerically (&#38;#38;) or with a general entity\n" + "(&amp;).</p>\" >" + "]><foo>&example;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("p", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("An ampersand (&) may be escaped\n" + "numerically (&) or with a general entity\n" + "(&).", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals("p", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testGeneralEntitiesAreStoredUnresolved() throws Exception { String xml = "<!DOCTYPE foo [" + "<!ENTITY b \"&a;\" >" + "<!ENTITY a \"android\" >" + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("android", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testStructuredEntityAndNextToken() throws Exception { String xml = "<!DOCTYPE foo [<!ENTITY bb \"<bar>baz<!--quux--></bar>\">]><foo>a&bb;c</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.DOCDECL, parser.nextToken()); assertEquals(XmlPullParser.START_TAG, parser.nextToken()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.nextToken()); assertEquals("a", parser.getText()); assertEquals(XmlPullParser.ENTITY_REF, parser.nextToken()); assertEquals("bb", parser.getName()); assertEquals("", parser.getText()); assertEquals(XmlPullParser.START_TAG, parser.nextToken()); assertEquals("bar", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.nextToken()); assertEquals("baz", parser.getText()); assertEquals(XmlPullParser.COMMENT, parser.nextToken()); assertEquals("quux", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.nextToken()); assertEquals("bar", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.nextToken()); assertEquals("c", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } /** * Android's Expat replaces external entities with the empty string. */ public void testUsingExternalEntities() throws Exception { String xml = "<!DOCTYPE foo [" + " <!ENTITY a SYSTEM \"http://localhost:1/no-such-file.xml\">" + "]><foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); // &a; is dropped! assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } /** * Android's ExpatPullParser replaces missing entities with the empty string * when an external DTD is declared. */ public void testExternalDtdAndMissingEntity() throws Exception { String xml = "<!DOCTYPE foo SYSTEM \"http://127.0.0.1:1/no-such-file.dtd\">" + "<foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testExternalIdIsCaseSensitive() throws Exception { // The spec requires 'SYSTEM' in upper case assertParseFailure("<!DOCTYPE foo [" + " <!ENTITY a system \"http://localhost:1/no-such-file.xml\">" + "]><foo/>"); } /** * Use a DTD to specify that {@code <foo>} only contains {@code <bar>} tags. * Validating parsers react to this by dropping whitespace between the two * tags. */ public void testDtdDoesNotInformIgnorableWhitespace() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ELEMENT foo (bar)*>\n" + " <!ELEMENT bar ANY>\n" + "]>" + "<foo> \n <bar></bar> \t </foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals(" \n ", parser.getText()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("bar", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals("bar", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals(" \t ", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testEmptyDoesNotInformIgnorableWhitespace() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ELEMENT foo EMPTY>\n" + "]>" + "<foo> \n </foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals(" \n ", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } /** * Test that the parser doesn't expand the entity attributes. */ public void testAttributeOfTypeEntity() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ENTITY a \"android\">" + " <!ELEMENT foo ANY>\n" + " <!ATTLIST foo\n" + " bar ENTITY #IMPLIED>" + "]>" + "<foo bar=\"a\"></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals("a", parser.getAttributeValue(null, "bar")); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testTagStructureNotValidated() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ELEMENT foo (bar)*>\n" + " <!ELEMENT bar ANY>\n" + " <!ELEMENT baz ANY>\n" + "]>" + "<foo><bar/><bar/><baz/></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("bar", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("bar", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("baz", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testAttributeDefaultValues() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ATTLIST bar\n" + " baz (a|b|c) \"c\">" + "]>" + "<foo>" + "<bar/>" + "<bar baz=\"a\"/>" + "</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("bar", parser.getName()); assertEquals("c", parser.getAttributeValue(null, "baz")); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("bar", parser.getName()); assertEquals("a", parser.getAttributeValue(null, "baz")); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testAttributeDefaultValueEntitiesExpanded() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ENTITY g \"ghi\">" + " <!ELEMENT foo ANY>\n" + " <!ATTLIST foo\n" + " bar CDATA \"abc & def &g; jk\">" + "]>" + "<foo></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals("abc & def ghi jk", parser.getAttributeValue(null, "bar")); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testAttributeDefaultValuesAndNamespaces() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ATTLIST foo\n" + " bar:a CDATA \"android\">" + "]>" + "<foo xmlns:bar='http://bar'></foo>"; XmlPullParser parser = newPullParser(xml); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); // In Expat, namespaces don't apply to default attributes int index = indexOfAttributeWithName(parser, "bar:a"); assertEquals("", parser.getAttributeNamespace(index)); assertEquals("bar:a", parser.getAttributeName(index)); assertEquals("android", parser.getAttributeValue(index)); assertEquals("CDATA", parser.getAttributeType(index)); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } private int indexOfAttributeWithName(XmlPullParser parser, String name) { for (int i = 0; i < parser.getAttributeCount(); i++) { if (parser.getAttributeName(i).equals(name)) { return i; } } return -1; } public void testAttributeEntitiesExpandedEagerly() throws Exception { assertParseFailure("<!DOCTYPE foo [\n" + " <!ELEMENT foo ANY>\n" + " <!ATTLIST foo\n" + " bar CDATA \"abc & def &g; jk\">" + " <!ENTITY g \"ghi\">" + "]>" + "<foo></foo>"); } public void testRequiredAttributesOmitted() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ELEMENT foo ANY>\n" + " <!ATTLIST foo\n" + " bar (a|b|c) #REQUIRED>" + "]>" + "<foo></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(null, parser.getAttributeValue(null, "bar")); assertEquals(XmlPullParser.END_TAG, parser.next()); } public void testFixedAttributesWithConflictingValues() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!ELEMENT foo ANY>\n" + " <!ATTLIST foo\n" + " bar (a|b|c) #FIXED \"c\">" + "]>" + "<foo bar=\"a\"></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals("a", parser.getAttributeValue(null, "bar")); assertEquals(XmlPullParser.END_TAG, parser.next()); } public void testParsingNotations() throws Exception { String xml = "<!DOCTYPE foo [\n" + " <!NOTATION type-a PUBLIC \"application/a\"> \n" + " <!NOTATION type-b PUBLIC \"image/b\">\n" + " <!NOTATION type-c PUBLIC \"-//W3C//DTD SVG 1.1//EN\"\n" + " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\"> \n" + " <!ENTITY file SYSTEM \"d.xml\">\n" + " <!ENTITY fileWithNdata SYSTEM \"e.bin\" NDATA type-b>\n" + "]>" + "<foo type=\"type-a\"/>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); } public void testCommentsInDoctype() throws Exception { String xml = "<!DOCTYPE foo [" + " <!-- ' -->" + "]><foo>android</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("android", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testDoctypeNameOnly() throws Exception { String xml = "<!DOCTYPE foo><foo></foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals("foo", parser.getName()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testVeryLongEntities() throws Exception { String a = repeat('a', READ_BUFFER_SIZE + 1); String b = repeat('b', READ_BUFFER_SIZE + 1); String c = repeat('c', READ_BUFFER_SIZE + 1); String xml = "<!DOCTYPE foo [\n" + " <!ENTITY " + a + " \"d &" + b + "; e\">" + " <!ENTITY " + b + " \"f " + c + " g\">" + "]>" + "<foo>h &" + a + "; i</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("h d f " + c + " g e i", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testManuallyRegisteredEntitiesWithDoctypeParsing() throws Exception { String xml = "<foo>&a;</foo>"; XmlPullParser parser = newPullParser(xml); try { parser.defineEntityReplacementText("a", "android"); fail(); } catch (UnsupportedOperationException expected) { } catch (IllegalStateException expected) { } } public void testDoctypeWithNextToken() throws Exception { String xml = "<!DOCTYPE foo [<!ENTITY bb \"bar baz\">]><foo>a&bb;c</foo>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.DOCDECL, parser.nextToken()); assertEquals(" foo [<!ENTITY bb \"bar baz\">]", parser.getText()); assertNull(parser.getName()); assertEquals(XmlPullParser.START_TAG, parser.nextToken()); assertEquals(XmlPullParser.TEXT, parser.next()); assertEquals("abar bazc", parser.getText()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testDoctypeSpansBuffers() throws Exception { char[] doctypeChars = new char[READ_BUFFER_SIZE + 1]; Arrays.fill(doctypeChars, 'x'); String doctypeBody = " foo [<!--" + new String(doctypeChars) + "-->]"; String xml = "<!DOCTYPE" + doctypeBody + "><foo/>"; XmlPullParser parser = newPullParser(xml); assertEquals(XmlPullParser.DOCDECL, parser.nextToken()); assertEquals(doctypeBody, parser.getText()); assertEquals(XmlPullParser.START_TAG, parser.next()); assertEquals(XmlPullParser.END_TAG, parser.next()); assertEquals(XmlPullParser.END_DOCUMENT, parser.next()); } public void testDoctypeInDocumentElement() throws Exception { assertParseFailure("<foo><!DOCTYPE foo></foo>"); } public void testDoctypeAfterDocumentElement() throws Exception { assertParseFailure("<foo/><!DOCTYPE foo>"); } private void assertParseFailure(String xml) throws Exception { XmlPullParser parser = newPullParser(); parser.setInput(new StringReader(xml)); try { while (parser.next() != XmlPullParser.END_DOCUMENT) { } fail(); } catch (XmlPullParserException expected) { } } private String repeat(char c, int length) { char[] chars = new char[length]; Arrays.fill(chars, c); return new String(chars); } private XmlPullParser newPullParser(String xml) throws XmlPullParserException { XmlPullParser result = newPullParser(); result.setInput(new StringReader(xml)); return result; } /** * Creates a new pull parser. */ abstract XmlPullParser newPullParser() throws XmlPullParserException; }