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 17 package libcore.xml; 18 19 import com.google.mockwebserver.MockResponse; 20 import com.google.mockwebserver.MockWebServer; 21 import java.io.ByteArrayInputStream; 22 import java.io.IOException; 23 import java.io.InputStream; 24 import java.io.Reader; 25 import java.io.StringReader; 26 import java.util.ArrayList; 27 import java.util.Arrays; 28 import java.util.HashMap; 29 import java.util.List; 30 import java.util.Map; 31 import junit.framework.Assert; 32 import junit.framework.TestCase; 33 import org.apache.harmony.xml.ExpatReader; 34 import org.xml.sax.Attributes; 35 import org.xml.sax.ContentHandler; 36 import org.xml.sax.InputSource; 37 import org.xml.sax.Locator; 38 import org.xml.sax.SAXException; 39 import org.xml.sax.SAXParseException; 40 import org.xml.sax.XMLReader; 41 import org.xml.sax.ext.DefaultHandler2; 42 import org.xml.sax.helpers.DefaultHandler; 43 44 public 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"); 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"); 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 } 650 651 /** 652 * This tests what happens when UTF-16 input (little and big endian) that has an odd number of 653 * bytes (and hence is invalid UTF-16) is parsed by Expat. 654 * 655 * <p>Prior to the patch the files would cause the pointer into the byte buffer to jump past 656 * the end of the buffer and keep reading. Once it had jumped past it would continue reading 657 * from memory until it hit a check that caused it to stop or caused a SIGSEGV. If a SIGSEGV 658 * was not thrown that lead to spurious and misleading errors being reported. 659 * 660 * <p>The initial jump was caused because it was not checking to make sure that there were 661 * enough bytes to read a whole UTF-16 character. It kept reading because most of the buffer 662 * range checks used == and != rather than >= and <. The patch fixes the initial jump and then 663 * uses inequalities in the range check to fail fast in the event of another overflow bug. 664 */ 665 private void checkBug28698301(String name) throws IOException, SAXException { 666 InputStream is = getClass().getResourceAsStream(name); 667 try { 668 parse(is, Encoding.UTF_16, new TestHandler()); 669 } catch (SAXParseException exception) { 670 String message = exception.getMessage(); 671 if (!message.contains("no element found")) { 672 fail("Expected 'no element found' exception, found: " + message); 673 } 674 } 675 } 676 677 /** 678 * Parses the given xml string and fires events on the given SAX handler. 679 */ 680 private static void parse(String xml, ContentHandler contentHandler) 681 throws SAXException { 682 try { 683 XMLReader reader = new ExpatReader(); 684 reader.setContentHandler(contentHandler); 685 reader.parse(new InputSource(new StringReader(xml))); 686 } catch (IOException e) { 687 throw new AssertionError(e); 688 } 689 } 690 691 /** 692 * Parses xml from the given reader and fires events on the given SAX 693 * handler. 694 */ 695 private static void parse(Reader in, ContentHandler contentHandler) 696 throws IOException, SAXException { 697 XMLReader reader = new ExpatReader(); 698 reader.setContentHandler(contentHandler); 699 reader.parse(new InputSource(in)); 700 } 701 702 /** 703 * Parses xml from the given input stream and fires events on the given SAX 704 * handler. 705 */ 706 private static void parse(InputStream in, Encoding encoding, 707 ContentHandler contentHandler) throws IOException, SAXException { 708 try { 709 XMLReader reader = new ExpatReader(); 710 reader.setContentHandler(contentHandler); 711 InputSource source = new InputSource(in); 712 source.setEncoding(encoding.expatName); 713 reader.parse(source); 714 } catch (IOException e) { 715 throw new AssertionError(e); 716 } 717 } 718 719 /** 720 * Supported character encodings. 721 */ 722 private enum Encoding { 723 724 US_ASCII("US-ASCII"), 725 UTF_8("UTF-8"), 726 UTF_16("UTF-16"), 727 ISO_8859_1("ISO-8859-1"); 728 729 final String expatName; 730 731 Encoding(String expatName) { 732 this.expatName = expatName; 733 } 734 } 735 } 736