1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package libcore.xml;
18
19import com.google.mockwebserver.MockResponse;
20import com.google.mockwebserver.MockWebServer;
21import java.io.ByteArrayInputStream;
22import java.io.IOException;
23import java.io.InputStream;
24import java.io.Reader;
25import java.io.StringReader;
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.HashMap;
29import java.util.List;
30import java.util.Map;
31import junit.framework.Assert;
32import junit.framework.TestCase;
33import org.apache.harmony.xml.ExpatReader;
34import org.xml.sax.Attributes;
35import org.xml.sax.ContentHandler;
36import org.xml.sax.InputSource;
37import org.xml.sax.Locator;
38import org.xml.sax.SAXException;
39import org.xml.sax.SAXParseException;
40import org.xml.sax.XMLReader;
41import org.xml.sax.ext.DefaultHandler2;
42import org.xml.sax.helpers.DefaultHandler;
43
44public class ExpatSaxParserTest extends TestCase {
45
46    private static final String SNIPPET = "<dagny dad=\"bob\">hello</dagny>";
47
48    public void testGlobalReferenceTableOverflow() throws Exception {
49        // We used to use a JNI global reference per interned string.
50        // Framework apps have a limit of 2000 JNI global references per VM.
51        StringBuilder xml = new StringBuilder();
52        xml.append("<root>");
53        for (int i = 0; i < 4000; ++i) {
54            xml.append("<tag" + i + ">");
55            xml.append("</tag" + i + ">");
56        }
57        xml.append("</root>");
58        parse(xml.toString(), new DefaultHandler());
59    }
60
61    public void testExceptions() {
62        // From startElement().
63        ContentHandler contentHandler = new DefaultHandler() {
64            @Override
65            public void startElement(String uri, String localName,
66                    String qName, Attributes attributes)
67                    throws SAXException {
68                throw new SAXException();
69            }
70        };
71        try {
72            parse(SNIPPET, contentHandler);
73            fail();
74        } catch (SAXException checked) { /* expected */ }
75
76        // From endElement().
77        contentHandler = new DefaultHandler() {
78            @Override
79            public void endElement(String uri, String localName,
80                    String qName)
81                    throws SAXException {
82                throw new SAXException();
83            }
84        };
85        try {
86            parse(SNIPPET, contentHandler);
87            fail();
88        } catch (SAXException checked) { /* expected */ }
89
90        // From characters().
91        contentHandler = new DefaultHandler() {
92            @Override
93            public void characters(char ch[], int start, int length)
94                    throws SAXException {
95                throw new SAXException();
96            }
97        };
98        try {
99            parse(SNIPPET, contentHandler);
100            fail();
101        } catch (SAXException checked) { /* expected */ }
102    }
103
104    public void testSax() {
105        try {
106            // Parse String.
107            TestHandler handler = new TestHandler();
108            parse(SNIPPET, handler);
109            validate(handler);
110
111            // Parse Reader.
112            handler = new TestHandler();
113            parse(new StringReader(SNIPPET), handler);
114            validate(handler);
115
116            // Parse InputStream.
117            handler = new TestHandler();
118            parse(new ByteArrayInputStream(SNIPPET.getBytes()),
119                    Encoding.UTF_8, handler);
120            validate(handler);
121        } catch (SAXException e) {
122            throw new RuntimeException(e);
123        } catch (IOException e) {
124            throw new RuntimeException(e);
125        }
126    }
127
128    static void validate(TestHandler handler) {
129        assertEquals("dagny", handler.startElementName);
130        assertEquals("dagny", handler.endElementName);
131        assertEquals("hello", handler.text.toString());
132    }
133
134    static class TestHandler extends DefaultHandler {
135
136        String startElementName;
137        String endElementName;
138        StringBuilder text = new StringBuilder();
139
140        @Override
141        public void startElement(String uri, String localName, String qName,
142                Attributes attributes) throws SAXException {
143
144            assertNull(this.startElementName);
145            this.startElementName = localName;
146
147            // Validate attributes.
148            assertEquals(1, attributes.getLength());
149            assertEquals("", attributes.getURI(0));
150            assertEquals("dad", attributes.getLocalName(0));
151            assertEquals("bob", attributes.getValue(0));
152            assertEquals(0, attributes.getIndex("", "dad"));
153            assertEquals("bob", attributes.getValue("", "dad"));
154        }
155
156        @Override
157        public void endElement(String uri, String localName, String qName)
158                throws SAXException {
159            assertNull(this.endElementName);
160            this.endElementName = localName;
161        }
162
163        @Override
164        public void characters(char ch[], int start, int length)
165                throws SAXException {
166            this.text.append(ch, start, length);
167        }
168    }
169
170    static final String XML =
171        "<one xmlns='ns:default' xmlns:n1='ns:1' a='b'>\n"
172              + "  <n1:two c='d' n1:e='f' xmlns:n2='ns:2'>text</n1:two>\n"
173              + "</one>";
174
175    public void testNamespaces() {
176        try {
177            NamespaceHandler handler = new NamespaceHandler();
178            parse(XML, handler);
179            handler.validate();
180        } catch (SAXException e) {
181            throw new RuntimeException(e);
182        }
183    }
184
185    static class NamespaceHandler implements ContentHandler {
186
187        Locator locator;
188        boolean documentStarted;
189        boolean documentEnded;
190        Map<String, String> prefixMappings = new HashMap<String, String>();
191
192        boolean oneStarted;
193        boolean twoStarted;
194        boolean oneEnded;
195        boolean twoEnded;
196
197        public void validate() {
198            assertTrue(documentEnded);
199        }
200
201        public void setDocumentLocator(Locator locator) {
202            this.locator = locator;
203        }
204
205        public void startDocument() throws SAXException {
206            documentStarted = true;
207            assertNotNull(locator);
208            assertEquals(0, prefixMappings.size());
209            assertFalse(documentEnded);
210        }
211
212        public void endDocument() throws SAXException {
213            assertTrue(documentStarted);
214            assertTrue(oneEnded);
215            assertTrue(twoEnded);
216            assertEquals(0, prefixMappings.size());
217            documentEnded = true;
218        }
219
220        public void startPrefixMapping(String prefix, String uri)
221                throws SAXException {
222            prefixMappings.put(prefix, uri);
223        }
224
225        public void endPrefixMapping(String prefix) throws SAXException {
226            assertNotNull(prefixMappings.remove(prefix));
227        }
228
229        public void startElement(String uri, String localName, String qName,
230                Attributes atts) throws SAXException {
231
232            if (localName == "one") {
233                assertEquals(2, prefixMappings.size());
234
235                assertEquals(1, locator.getLineNumber());
236
237                assertFalse(oneStarted);
238                assertFalse(twoStarted);
239                assertFalse(oneEnded);
240                assertFalse(twoEnded);
241
242                oneStarted = true;
243
244                assertSame("ns:default", uri);
245                assertEquals("one", qName);
246
247                // Check atts.
248                assertEquals(1, atts.getLength());
249
250                assertSame("", atts.getURI(0));
251                assertSame("a", atts.getLocalName(0));
252                assertEquals("b", atts.getValue(0));
253                assertEquals(0, atts.getIndex("", "a"));
254                assertEquals("b", atts.getValue("", "a"));
255
256                return;
257            }
258
259            if (localName == "two") {
260                assertEquals(3, prefixMappings.size());
261
262                assertTrue(oneStarted);
263                assertFalse(twoStarted);
264                assertFalse(oneEnded);
265                assertFalse(twoEnded);
266
267                twoStarted = true;
268
269                assertSame("ns:1", uri);
270                Assert.assertEquals("n1:two", qName);
271
272                // Check atts.
273                assertEquals(2, atts.getLength());
274
275                assertSame("", atts.getURI(0));
276                assertSame("c", atts.getLocalName(0));
277                assertEquals("d", atts.getValue(0));
278                assertEquals(0, atts.getIndex("", "c"));
279                assertEquals("d", atts.getValue("", "c"));
280
281                assertSame("ns:1", atts.getURI(1));
282                assertSame("e", atts.getLocalName(1));
283                assertEquals("f", atts.getValue(1));
284                assertEquals(1, atts.getIndex("ns:1", "e"));
285                assertEquals("f", atts.getValue("ns:1", "e"));
286
287                // We shouldn't find these.
288                assertEquals(-1, atts.getIndex("ns:default", "e"));
289                assertEquals(null, atts.getValue("ns:default", "e"));
290
291                return;
292            }
293
294            fail();
295         }
296
297        public void endElement(String uri, String localName, String qName)
298                throws SAXException {
299            if (localName == "one") {
300                assertEquals(3, locator.getLineNumber());
301
302                assertTrue(oneStarted);
303                assertTrue(twoStarted);
304                assertTrue(twoEnded);
305                assertFalse(oneEnded);
306
307                oneEnded = true;
308
309                assertSame("ns:default", uri);
310                assertEquals("one", qName);
311
312                return;
313            }
314
315            if (localName == "two") {
316                assertTrue(oneStarted);
317                assertTrue(twoStarted);
318                assertFalse(twoEnded);
319                assertFalse(oneEnded);
320
321                twoEnded = true;
322
323                assertSame("ns:1", uri);
324                assertEquals("n1:two", qName);
325
326                return;
327            }
328
329            fail();
330        }
331
332        public void characters(char ch[], int start, int length)
333                throws SAXException {
334            String s = new String(ch, start, length).trim();
335
336            if (!s.equals("")) {
337                assertTrue(oneStarted);
338                assertTrue(twoStarted);
339                assertFalse(oneEnded);
340                assertFalse(twoEnded);
341                assertEquals("text", s);
342            }
343        }
344
345        public void ignorableWhitespace(char ch[], int start, int length)
346                throws SAXException {
347            fail();
348        }
349
350        public void processingInstruction(String target, String data)
351                throws SAXException {
352            fail();
353        }
354
355        public void skippedEntity(String name) throws SAXException {
356            fail();
357        }
358    }
359
360    private TestDtdHandler runDtdTest(String s) throws Exception {
361        Reader in = new StringReader(s);
362        ExpatReader reader = new ExpatReader();
363        TestDtdHandler handler = new TestDtdHandler();
364        reader.setContentHandler(handler);
365        reader.setDTDHandler(handler);
366        reader.setLexicalHandler(handler);
367        reader.parse(new InputSource(in));
368        return handler;
369    }
370
371    public void testDtdDoctype() throws Exception {
372        TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee'><a></a>");
373        assertEquals("foo", handler.name);
374        assertEquals("bar", handler.publicId);
375        assertEquals("tee", handler.systemId);
376        assertTrue(handler.ended);
377    }
378
379    public void testDtdUnparsedEntity_system() throws Exception {
380        TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!ENTITY ent SYSTEM 'blah' NDATA poop> ]><a></a>");
381        assertEquals("ent", handler.ueName);
382        assertEquals(null, handler.uePublicId);
383        assertEquals("blah", handler.ueSystemId);
384        assertEquals("poop", handler.ueNotationName);
385    }
386
387    public void testDtdUnparsedEntity_public() throws Exception {
388        TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!ENTITY ent PUBLIC 'a' 'b' NDATA poop> ]><a></a>");
389        assertEquals("ent", handler.ueName);
390        assertEquals("a", handler.uePublicId);
391        assertEquals("b", handler.ueSystemId);
392        assertEquals("poop", handler.ueNotationName);
393    }
394
395    public void testDtdNotation_system() throws Exception {
396        TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!NOTATION sn SYSTEM 'nf2'> ]><a></a>");
397        assertEquals("sn", handler.ndName);
398        assertEquals(null, handler.ndPublicId);
399        assertEquals("nf2", handler.ndSystemId);
400    }
401
402    public void testDtdNotation_public() throws Exception {
403        TestDtdHandler handler = runDtdTest("<?xml version=\"1.0\"?><!DOCTYPE foo PUBLIC 'bar' 'tee' [ <!NOTATION pn PUBLIC 'nf1'> ]><a></a>");
404        assertEquals("pn", handler.ndName);
405        assertEquals("nf1", handler.ndPublicId);
406        assertEquals(null, handler.ndSystemId);
407    }
408
409    static class TestDtdHandler extends DefaultHandler2 {
410
411        String name;
412        String publicId;
413        String systemId;
414        String ndName;
415        String ndPublicId;
416        String ndSystemId;
417        String ueName;
418        String uePublicId;
419        String ueSystemId;
420        String ueNotationName;
421
422        boolean ended;
423
424        Locator locator;
425
426        @Override
427        public void startDTD(String name, String publicId, String systemId) {
428            this.name = name;
429            this.publicId = publicId;
430            this.systemId = systemId;
431        }
432
433        @Override
434        public void endDTD() {
435            ended = true;
436        }
437
438        @Override
439        public void setDocumentLocator(Locator locator) {
440            this.locator = locator;
441        }
442
443        @Override
444        public void notationDecl(String name, String publicId, String systemId) {
445            this.ndName = name;
446            this.ndPublicId = publicId;
447            this.ndSystemId = systemId;
448        }
449
450        @Override
451        public void unparsedEntityDecl(String entityName, String publicId, String systemId, String notationName) {
452            this.ueName = entityName;
453            this.uePublicId = publicId;
454            this.ueSystemId = systemId;
455            this.ueNotationName = notationName;
456        }
457    }
458
459    public void testCdata() throws Exception {
460        Reader in = new StringReader(
461            "<a><![CDATA[<b></b>]]> <![CDATA[<c></c>]]></a>");
462
463        ExpatReader reader = new ExpatReader();
464        TestCdataHandler handler = new TestCdataHandler();
465        reader.setContentHandler(handler);
466        reader.setLexicalHandler(handler);
467
468        reader.parse(new InputSource(in));
469
470        assertEquals(2, handler.startCdata);
471        assertEquals(2, handler.endCdata);
472        assertEquals("<b></b> <c></c>", handler.buffer.toString());
473    }
474
475    static class TestCdataHandler extends DefaultHandler2 {
476
477        int startCdata, endCdata;
478        StringBuffer buffer = new StringBuffer();
479
480        @Override
481        public void characters(char ch[], int start, int length) {
482            buffer.append(ch, start, length);
483        }
484
485        @Override
486        public void startCDATA() throws SAXException {
487            startCdata++;
488        }
489
490        @Override
491        public void endCDATA() throws SAXException {
492            endCdata++;
493        }
494    }
495
496    public void testProcessingInstructions() throws IOException, SAXException {
497        Reader in = new StringReader(
498            "<?bob lee?><a></a>");
499
500        ExpatReader reader = new ExpatReader();
501        TestProcessingInstrutionHandler handler
502                = new TestProcessingInstrutionHandler();
503        reader.setContentHandler(handler);
504
505        reader.parse(new InputSource(in));
506
507        assertEquals("bob", handler.target);
508        assertEquals("lee", handler.data);
509    }
510
511    static class TestProcessingInstrutionHandler extends DefaultHandler2 {
512
513        String target;
514        String data;
515
516        @Override
517        public void processingInstruction(String target, String data) {
518            this.target = target;
519            this.data = data;
520        }
521    }
522
523    public void testExternalEntity() throws IOException, SAXException {
524        class Handler extends DefaultHandler {
525
526            List<String> elementNames = new ArrayList<String>();
527            StringBuilder text = new StringBuilder();
528
529            public InputSource resolveEntity(String publicId, String systemId)
530                    throws IOException, SAXException {
531                if (publicId.equals("publicA") && systemId.equals("systemA")) {
532                    return new InputSource(new StringReader("<a/>"));
533                } else if (publicId.equals("publicB")
534                        && systemId.equals("systemB")) {
535                    /*
536                     * Explicitly set the encoding here or else the parser will
537                     * try to use the parent parser's encoding which is utf-16.
538                     */
539                    InputSource inputSource = new InputSource(
540                            new ByteArrayInputStream("bob".getBytes("utf-8")));
541                    inputSource.setEncoding("utf-8");
542                    return inputSource;
543                }
544
545                throw new AssertionError();
546            }
547
548            @Override
549            public void startElement(String uri, String localName, String qName,
550                    Attributes attributes) throws SAXException {
551                elementNames.add(localName);
552            }
553
554            @Override
555            public void endElement(String uri, String localName, String qName)
556                    throws SAXException {
557                elementNames.add("/" + localName);
558            }
559
560            @Override
561            public void characters(char ch[], int start, int length)
562                    throws SAXException {
563                text.append(ch, start, length);
564            }
565        }
566
567        Reader in = new StringReader("<?xml version=\"1.0\"?>\n"
568            + "<!DOCTYPE foo [\n"
569            + "  <!ENTITY a PUBLIC 'publicA' 'systemA'>\n"
570            + "  <!ENTITY b PUBLIC 'publicB' 'systemB'>\n"
571            + "]>\n"
572            + "<foo>\n"
573            + "  &a;<b>&b;</b></foo>");
574
575        ExpatReader reader = new ExpatReader();
576        Handler handler = new Handler();
577        reader.setContentHandler(handler);
578        reader.setEntityResolver(handler);
579
580        reader.parse(new InputSource(in));
581
582        assertEquals(Arrays.asList("foo", "a", "/a", "b", "/b", "/foo"),
583                handler.elementNames);
584        assertEquals("bob", handler.text.toString().trim());
585    }
586
587    public void testExternalEntityDownload() throws IOException, SAXException {
588        final MockWebServer server = new MockWebServer();
589        server.enqueue(new MockResponse().setBody("<bar></bar>"));
590        server.play();
591
592        class Handler extends DefaultHandler {
593            final List<String> elementNames = new ArrayList<String>();
594
595            @Override public InputSource resolveEntity(String publicId, String systemId)
596                    throws IOException {
597                // The parser should have resolved the systemId.
598                assertEquals(server.getUrl("/systemBar").toString(), systemId);
599                return new InputSource(systemId);
600            }
601
602            @Override public void startElement(String uri, String localName, String qName,
603                    Attributes attributes) {
604                elementNames.add(localName);
605            }
606
607            @Override public void endElement(String uri, String localName, String qName) {
608                elementNames.add("/" + localName);
609            }
610        }
611
612        // 'systemBar', the external entity, is relative to 'systemFoo':
613        Reader in = new StringReader("<?xml version=\"1.0\"?>\n"
614            + "<!DOCTYPE foo [\n"
615            + "  <!ENTITY bar SYSTEM 'systemBar'>\n"
616            + "]>\n"
617            + "<foo>&bar;</foo>");
618        ExpatReader reader = new ExpatReader();
619        Handler handler = new Handler();
620        reader.setContentHandler(handler);
621        reader.setEntityResolver(handler);
622        InputSource source = new InputSource(in);
623        source.setSystemId(server.getUrl("/systemFoo").toString());
624        reader.parse(source);
625        assertEquals(Arrays.asList("foo", "bar", "/bar", "/foo"), handler.elementNames);
626        server.shutdown();
627    }
628
629    /**
630     * A little endian UTF-16 file with an odd number of bytes.
631     */
632    public void testBug28698301_1() throws Exception {
633        checkBug28698301("bug28698301-1.xml", "At line 19, column 18: no element found");
634    }
635
636    /**
637     * A little endian UTF-16 file with an even number of bytes that didn't exhibit the problem
638     * reported in the bug.
639     */
640    public void testBug28698301_2() throws Exception {
641        checkBug28698301("bug28698301-2.xml", "At line 3, column 18: no element found");
642    }
643
644    /**
645     * A big endian UTF-16 file with an odd number of bytes.
646     */
647    public void testBug28698301_3() throws Exception {
648        checkBug28698301("bug28698301-3.xml",
649            "At line 97, column 21: not well-formed (invalid token)");
650    }
651
652    /**
653     * This tests what happens when UTF-16 input (little and big endian) that has an odd number of
654     * bytes (and hence is invalid UTF-16) is parsed by Expat.
655     *
656     * <p>Prior to the patch the files would cause the pointer into the byte buffer to jump past
657     * the end of the buffer and keep reading. Once it had jumped past it would continue reading
658     * from memory until it hit a check that caused it to stop or caused a SIGSEGV. If a SIGSEGV
659     * was not thrown that lead to spurious and misleading errors being reported.
660     *
661     * <p>The initial jump was caused because it was not checking to make sure that there were
662     * enough bytes to read a whole UTF-16 character. It kept reading because most of the buffer
663     * range checks used == and != rather than >= and <. The patch fixes the initial jump and then
664     * uses inequalities in the range check to fail fast in the event of another overflow bug.
665     */
666    private void checkBug28698301(String name, String expectedMessage)
667        throws IOException, SAXException {
668        InputStream is = getClass().getResourceAsStream(name);
669        try {
670            parse(is, Encoding.UTF_16, new TestHandler());
671        } catch (SAXParseException exception) {
672            String message = exception.getMessage();
673            if (!message.equals(expectedMessage)) {
674                fail("Expected '" + expectedMessage + "' exception, found: '" + message + "'");
675            }
676        }
677    }
678
679    /**
680     * Parses the given xml string and fires events on the given SAX handler.
681     */
682    private static void parse(String xml, ContentHandler contentHandler)
683            throws SAXException {
684        try {
685            XMLReader reader = new ExpatReader();
686            reader.setContentHandler(contentHandler);
687            reader.parse(new InputSource(new StringReader(xml)));
688        } catch (IOException e) {
689            throw new AssertionError(e);
690        }
691    }
692
693    /**
694     * Parses xml from the given reader and fires events on the given SAX
695     * handler.
696     */
697    private static void parse(Reader in, ContentHandler contentHandler)
698            throws IOException, SAXException {
699        XMLReader reader = new ExpatReader();
700        reader.setContentHandler(contentHandler);
701        reader.parse(new InputSource(in));
702    }
703
704    /**
705     * Parses xml from the given input stream and fires events on the given SAX
706     * handler.
707     */
708    private static void parse(InputStream in, Encoding encoding,
709            ContentHandler contentHandler) throws IOException, SAXException {
710        try {
711            XMLReader reader = new ExpatReader();
712            reader.setContentHandler(contentHandler);
713            InputSource source = new InputSource(in);
714            source.setEncoding(encoding.expatName);
715            reader.parse(source);
716        } catch (IOException e) {
717            throw new AssertionError(e);
718        }
719    }
720
721    /**
722     * Supported character encodings.
723     */
724    private enum Encoding {
725
726        US_ASCII("US-ASCII"),
727        UTF_8("UTF-8"),
728        UTF_16("UTF-16"),
729        ISO_8859_1("ISO-8859-1");
730
731        final String expatName;
732
733        Encoding(String expatName) {
734            this.expatName = expatName;
735        }
736    }
737}
738