blob: a065cb824b7ebcc497e03d91a8203a5bac1c4e7d [file] [log] [blame]
/*
* Copyright (C) 2007 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 com.google.mockwebserver.MockResponse;
import com.google.mockwebserver.MockWebServer;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import junit.framework.Assert;
import junit.framework.TestCase;
import org.apache.harmony.xml.ExpatReader;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.helpers.DefaultHandler;
public class ExpatSaxParserTest extends TestCase {
private static final String SNIPPET = "<dagny dad=\"bob\">hello</dagny>";
public void testGlobalReferenceTableOverflow() throws Exception {
// We used to use a JNI global reference per interned string.
// Framework apps have a limit of 2000 JNI global references per VM.
StringBuilder xml = new StringBuilder();
xml.append("<root>");
for (int i = 0; i < 4000; ++i) {
xml.append("<tag" + i + ">");
xml.append("</tag" + i + ">");
}
xml.append("</root>");
parse(xml.toString(), new DefaultHandler());
}
public void testExceptions() {
// From startElement().
ContentHandler contentHandler = new DefaultHandler() {
@Override
public void startElement(String uri, String localName,
String qName, Attributes attributes)
throws SAXException {
throw new SAXException();
}
};
try {
parse(SNIPPET, contentHandler);
fail();
} catch (SAXException checked) { /* expected */ }
// From endElement().
contentHandler = new DefaultHandler() {
@Override
public void endElement(String uri, String localName,
String qName)
throws SAXException {
throw new SAXException();
}
};
try {
parse(SNIPPET, contentHandler);
fail();
} catch (SAXException checked) { /* expected */ }
// From characters().
contentHandler = new DefaultHandler() {
@Override
public void characters(char ch[], int start, int length)
throws SAXException {
throw new SAXException();
}
};
try {
parse(SNIPPET, contentHandler);
fail();
} catch (SAXException checked) { /* expected */ }
}
public void testSax() {
try {
// Parse String.
TestHandler handler = new TestHandler();
parse(SNIPPET, handler);
validate(handler);
// Parse Reader.
handler = new TestHandler();
parse(new StringReader(SNIPPET), handler);
validate(handler);
// Parse InputStream.
handler = new TestHandler();
parse(new ByteArrayInputStream(SNIPPET.getBytes()),
Encoding.UTF_8, handler);
validate(handler);
} catch (SAXException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
static void validate(TestHandler handler) {
assertEquals("dagny", handler.startElementName);
assertEquals("dagny", handler.endElementName);
assertEquals("hello", handler.text.toString());
}
static class TestHandler extends DefaultHandler {
String startElementName;
String endElementName;
StringBuilder text = new StringBuilder();
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
assertNull(this.startElementName);
this.startElementName = localName;
// Validate attributes.
assertEquals(1, attributes.getLength());
assertEquals("", attributes.getURI(0));
assertEquals("dad", attributes.getLocalName(0));
assertEquals("bob", attributes.getValue(0));
assertEquals(0, attributes.getIndex("", "dad"));
assertEquals("bob", attributes.getValue("", "dad"));
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
assertNull(this.endElementName);
this.endElementName = localName;
}
@Override
public void characters(char ch[], int start, int length)
throws SAXException {
this.text.append(ch, start, length);
}
}
static final String XML =
"<one xmlns='ns:default' xmlns:n1='ns:1' a='b'>\n"
+ " <n1:two c='d' n1:e='f' xmlns:n2='ns:2'>text</n1:two>\n"
+ "</one>";
public void testNamespaces() {
try {
NamespaceHandler handler = new NamespaceHandler();
parse(XML, handler);
handler.validate();
} catch (SAXException e) {
throw new RuntimeException(e);
}
}
static class NamespaceHandler implements ContentHandler {
Locator locator;
boolean documentStarted;
boolean documentEnded;
Map<String, String> prefixMappings = new HashMap<String, String>();
boolean oneStarted;
boolean twoStarted;
boolean oneEnded;
boolean twoEnded;
public void validate() {
assertTrue(documentEnded);
}
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
public void startDocument() throws SAXException {
documentStarted = true;
assertNotNull(locator);
assertEquals(0, prefixMappings.size());
assertFalse(documentEnded);
}
public void endDocument() throws SAXException {
assertTrue(documentStarted);
assertTrue(oneEnded);
assertTrue(twoEnded);
assertEquals(0, prefixMappings.size());
documentEnded = true;
}
public void startPrefixMapping(String prefix, String uri)
throws SAXException {
prefixMappings.put(prefix, uri);
}
public void endPrefixMapping(String prefix) throws SAXException {
assertNotNull(prefixMappings.remove(prefix));
}
public void startElement(String uri, String localName, String qName,
Attributes atts) throws SAXException {
if (localName == "one") {
assertEquals(2, prefixMappings.size());
assertEquals(1, locator.getLineNumber());
assertFalse(oneStarted);
assertFalse(twoStarted);
assertFalse(oneEnded);
assertFalse(twoEnded);
oneStarted = true;
assertSame("ns:default", uri);
assertEquals("one", qName);
// Check atts.
assertEquals(1, atts.getLength());
assertSame("", atts.getURI(0));
assertSame("a", atts.getLocalName(0));
assertEquals("b", atts.getValue(0));
assertEquals(0, atts.getIndex("", "a"));
assertEquals("b", atts.getValue("", "a"));
return;
}
if (localName == "two") {
assertEquals(3, prefixMappings.size());
assertTrue(oneStarted);
assertFalse(twoStarted);
assertFalse(oneEnded);
assertFalse(twoEnded);
twoStarted = true;
assertSame("ns:1", uri);
Assert.assertEquals("n1:two", qName);
// Check atts.
assertEquals(2, atts.getLength());
assertSame("", atts.getURI(0));
assertSame("c", atts.getLocalName(0));
assertEquals("d", atts.getValue(0));
assertEquals(0, atts.getIndex("", "c"));
assertEquals("d", atts.getValue("", "c"));
assertSame("ns:1", atts.getURI(1));
assertSame("e", atts.getLocalName(1));
assertEquals("f", atts.getValue(1));
assertEquals(1, atts.getIndex("ns:1", "e"));
assertEquals("f", atts.getValue("ns:1", "e"));
// We shouldn't find these.
assertEquals(-1, atts.getIndex("ns:default", "e"));
assertEquals(null, atts.getValue("ns:default", "e"));
return;
}
fail();
}
public void endElement(String uri, String localName, String qName)
throws SAXException {
if (localName == "one") {
assertEquals(3, locator.getLineNumber());
assertTrue(oneStarted);
assertTrue(twoStarted);
assertTrue(twoEnded);
assertFalse(oneEnded);
oneEnded = true;
assertSame("ns:default", uri);
assertEquals("one", qName);
return;
}
if (localName == "two") {
assertTrue(oneStarted);
assertTrue(twoStarted);
assertFalse(twoEnded);
assertFalse(oneEnded);
twoEnded = true;
assertSame("ns:1", uri);
assertEquals("n1:two", qName);
return;
}
fail();
}
public void characters(char ch[], int start, int length)
throws SAXException {
String s = new String(ch, start, length).trim();
if (!s.equals("")) {
assertTrue(oneStarted);
assertTrue(twoStarted);
assertFalse(oneEnded);
assertFalse(twoEnded);
assertEquals("text", s);
}
}
public void ignorableWhitespace(char ch[], int start, int length)
throws SAXException {
fail();
}
public void processingInstruction(String target, String data)
throws SAXException {
fail();
}
public void skippedEntity(String name) throws SAXException {
fail();
}
}
private TestDtdHandler runDtdTest(String s) throws Exception {
Reader in = new StringReader(s);
ExpatReader reader = new ExpatReader();
TestDtdHandler handler = new TestDtdHandler();
reader.setContentHandler(handler);
reader.setDTDHandler(handler);
reader.setLexicalHandler(handler);
reader.parse(new InputSource(in));
return handler;
}
public void testDtdDoctype() throws Exception {
TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee'><a></a>");
assertEquals("foo", handler.name);
assertEquals("bar", handler.publicId);
assertEquals("tee", handler.systemId);
assertTrue(handler.ended);
}
public void testDtdUnparsedEntity_system() throws Exception {
TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!ENTITY ent SYSTEM 'blah' NDATA poop> ]><a></a>");
assertEquals("ent", handler.ueName);
assertEquals(null, handler.uePublicId);
assertEquals("blah", handler.ueSystemId);
assertEquals("poop", handler.ueNotationName);
}
public void testDtdUnparsedEntity_public() throws Exception {
TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!ENTITY ent PUBLIC 'a' 'b' NDATA poop> ]><a></a>");
assertEquals("ent", handler.ueName);
assertEquals("a", handler.uePublicId);
assertEquals("b", handler.ueSystemId);
assertEquals("poop", handler.ueNotationName);
}
public void testDtdNotation_system() throws Exception {
TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!NOTATION sn SYSTEM 'nf2'> ]><a></a>");
assertEquals("sn", handler.ndName);
assertEquals(null, handler.ndPublicId);
assertEquals("nf2", handler.ndSystemId);
}
public void testDtdNotation_public() throws Exception {
TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!NOTATION pn PUBLIC 'nf1'> ]><a></a>");
assertEquals("pn", handler.ndName);
assertEquals("nf1", handler.ndPublicId);
assertEquals(null, handler.ndSystemId);
}
static class TestDtdHandler extends DefaultHandler2 {
String name;
String publicId;
String systemId;
String ndName;
String ndPublicId;
String ndSystemId;
String ueName;
String uePublicId;
String ueSystemId;
String ueNotationName;
boolean ended;
Locator locator;
@Override
public void startDTD(String name, String publicId, String systemId) {
this.name = name;
this.publicId = publicId;
this.systemId = systemId;
}
@Override
public void endDTD() {
ended = true;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@Override
public void notationDecl(String name, String publicId, String systemId) {
this.ndName = name;
this.ndPublicId = publicId;
this.ndSystemId = systemId;
}
@Override
public void unparsedEntityDecl(String entityName, String publicId, String systemId, String notationName) {
this.ueName = entityName;
this.uePublicId = publicId;
this.ueSystemId = systemId;
this.ueNotationName = notationName;
}
}
public void testCdata() throws Exception {
Reader in = new StringReader(
"<a><![CDATA[<b></b>]]> <![CDATA[<c></c>]]></a>");
ExpatReader reader = new ExpatReader();
TestCdataHandler handler = new TestCdataHandler();
reader.setContentHandler(handler);
reader.setLexicalHandler(handler);
reader.parse(new InputSource(in));
assertEquals(2, handler.startCdata);
assertEquals(2, handler.endCdata);
assertEquals("<b></b> <c></c>", handler.buffer.toString());
}
static class TestCdataHandler extends DefaultHandler2 {
int startCdata, endCdata;
StringBuffer buffer = new StringBuffer();
@Override
public void characters(char ch[], int start, int length) {
buffer.append(ch, start, length);
}
@Override
public void startCDATA() throws SAXException {
startCdata++;
}
@Override
public void endCDATA() throws SAXException {
endCdata++;
}
}
public void testProcessingInstructions() throws IOException, SAXException {
Reader in = new StringReader(
"<?bob lee?><a></a>");
ExpatReader reader = new ExpatReader();
TestProcessingInstrutionHandler handler
= new TestProcessingInstrutionHandler();
reader.setContentHandler(handler);
reader.parse(new InputSource(in));
assertEquals("bob", handler.target);
assertEquals("lee", handler.data);
}
static class TestProcessingInstrutionHandler extends DefaultHandler2 {
String target;
String data;
@Override
public void processingInstruction(String target, String data) {
this.target = target;
this.data = data;
}
}
public void testExternalEntity() throws IOException, SAXException {
class Handler extends DefaultHandler {
List<String> elementNames = new ArrayList<String>();
StringBuilder text = new StringBuilder();
public InputSource resolveEntity(String publicId, String systemId)
throws IOException, SAXException {
if (publicId.equals("publicA") && systemId.equals("systemA")) {
return new InputSource(new StringReader("<a/>"));
} else if (publicId.equals("publicB")
&& systemId.equals("systemB")) {
/*
* Explicitly set the encoding here or else the parser will
* try to use the parent parser's encoding which is utf-16.
*/
InputSource inputSource = new InputSource(
new ByteArrayInputStream("bob".getBytes("utf-8")));
inputSource.setEncoding("utf-8");
return inputSource;
}
throw new AssertionError();
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attributes) throws SAXException {
elementNames.add(localName);
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
elementNames.add("/" + localName);
}
@Override
public void characters(char ch[], int start, int length)
throws SAXException {
text.append(ch, start, length);
}
}
Reader in = new StringReader("<?xml version=\"1.0\"?>\n"
+ "<!DOCTYPE foo [\n"
+ " <!ENTITY a PUBLIC 'publicA' 'systemA'>\n"
+ " <!ENTITY b PUBLIC 'publicB' 'systemB'>\n"
+ "]>\n"
+ "<foo>\n"
+ " &a;<b>&b;</b></foo>");
ExpatReader reader = new ExpatReader();
Handler handler = new Handler();
reader.setContentHandler(handler);
reader.setEntityResolver(handler);
reader.parse(new InputSource(in));
assertEquals(Arrays.asList("foo", "a", "/a", "b", "/b", "/foo"),
handler.elementNames);
assertEquals("bob", handler.text.toString().trim());
}
public void testExternalEntityDownload() throws IOException, SAXException {
final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("<bar></bar>"));
server.play();
class Handler extends DefaultHandler {
final List<String> elementNames = new ArrayList<String>();
@Override public InputSource resolveEntity(String publicId, String systemId)
throws IOException {
// The parser should have resolved the systemId.
assertEquals(server.getUrl("/systemBar").toString(), systemId);
return new InputSource(systemId);
}
@Override public void startElement(String uri, String localName, String qName,
Attributes attributes) {
elementNames.add(localName);
}
@Override public void endElement(String uri, String localName, String qName) {
elementNames.add("/" + localName);
}
}
// 'systemBar', the external entity, is relative to 'systemFoo':
Reader in = new StringReader("<?xml version=\"1.0\"?>\n"
+ "<!DOCTYPE foo [\n"
+ " <!ENTITY bar SYSTEM 'systemBar'>\n"
+ "]>\n"
+ "<foo>&bar;</foo>");
ExpatReader reader = new ExpatReader();
Handler handler = new Handler();
reader.setContentHandler(handler);
reader.setEntityResolver(handler);
InputSource source = new InputSource(in);
source.setSystemId(server.getUrl("/systemFoo").toString());
reader.parse(source);
assertEquals(Arrays.asList("foo", "bar", "/bar", "/foo"), handler.elementNames);
server.shutdown();
}
/**
* A little endian UTF-16 file with an odd number of bytes.
*/
public void testBug28698301_1() throws Exception {
checkBug28698301("bug28698301-1.xml");
}
/**
* A little endian UTF-16 file with an even number of bytes that didn't exhibit the problem
* reported in the bug.
*/
public void testBug28698301_2() throws Exception {
checkBug28698301("bug28698301-2.xml");
}
/**
* A big endian UTF-16 file with an odd number of bytes.
*/
public void testBug28698301_3() throws Exception {
checkBug28698301("bug28698301-3.xml");
}
/**
* This tests what happens when UTF-16 input (little and big endian) that has an odd number of
* bytes (and hence is invalid UTF-16) is parsed by Expat.
*
* <p>Prior to the patch the files would cause the pointer into the byte buffer to jump past
* the end of the buffer and keep reading. Once it had jumped past it would continue reading
* from memory until it hit a check that caused it to stop or caused a SIGSEGV. If a SIGSEGV
* was not thrown that lead to spurious and misleading errors being reported.
*
* <p>The initial jump was caused because it was not checking to make sure that there were
* enough bytes to read a whole UTF-16 character. It kept reading because most of the buffer
* range checks used == and != rather than >= and <. The patch fixes the initial jump and then
* uses inequalities in the range check to fail fast in the event of another overflow bug.
*/
private void checkBug28698301(String name) throws IOException, SAXException {
InputStream is = getClass().getResourceAsStream(name);
try {
parse(is, Encoding.UTF_16, new TestHandler());
} catch (SAXParseException exception) {
String message = exception.getMessage();
if (!message.contains("no element found")) {
fail("Expected 'no element found' exception, found: " + message);
}
}
}
/**
* Parses the given xml string and fires events on the given SAX handler.
*/
private static void parse(String xml, ContentHandler contentHandler)
throws SAXException {
try {
XMLReader reader = new ExpatReader();
reader.setContentHandler(contentHandler);
reader.parse(new InputSource(new StringReader(xml)));
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* Parses xml from the given reader and fires events on the given SAX
* handler.
*/
private static void parse(Reader in, ContentHandler contentHandler)
throws IOException, SAXException {
XMLReader reader = new ExpatReader();
reader.setContentHandler(contentHandler);
reader.parse(new InputSource(in));
}
/**
* Parses xml from the given input stream and fires events on the given SAX
* handler.
*/
private static void parse(InputStream in, Encoding encoding,
ContentHandler contentHandler) throws IOException, SAXException {
try {
XMLReader reader = new ExpatReader();
reader.setContentHandler(contentHandler);
InputSource source = new InputSource(in);
source.setEncoding(encoding.expatName);
reader.parse(source);
} catch (IOException e) {
throw new AssertionError(e);
}
}
/**
* Supported character encodings.
*/
private enum Encoding {
US_ASCII("US-ASCII"),
UTF_8("UTF-8"),
UTF_16("UTF-16"),
ISO_8859_1("ISO-8859-1");
final String expatName;
Encoding(String expatName) {
this.expatName = expatName;
}
}
}