Fix KXmlSerializer so it won't generate invalid XML.

We were allowing arbitrary characters to be output (which, surprisingly,
XML does not), and we weren't correctly escaping CDATA sections that
contained "]]>".

Pull out some of my test helpers from DocumentBuilderTest into Support_Xml,
because they're more generally useful when writing tests involving XML.

Also correct a bunch of spelling mistakes in XmlSerializer's javadoc, since
I happened to be reading through.
diff --git a/libcore/luni/src/test/java/javax/xml/parsers/DocumentBuilderTest.java b/libcore/luni/src/test/java/javax/xml/parsers/DocumentBuilderTest.java
index 6d3c463..b1aea21 100644
--- a/libcore/luni/src/test/java/javax/xml/parsers/DocumentBuilderTest.java
+++ b/libcore/luni/src/test/java/javax/xml/parsers/DocumentBuilderTest.java
@@ -16,33 +16,12 @@
 
 package javax.xml.parsers;
 
-import java.io.ByteArrayInputStream;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
 import junit.framework.Test;
 import junit.framework.TestSuite;
 
+import static tests.support.Support_Xml.*;
+
 public class DocumentBuilderTest extends junit.framework.TestCase {
-    private static Document domOf(String xml) throws Exception {
-        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
-        dbf.setCoalescing(true);
-        dbf.setExpandEntityReferences(true);
-        
-        ByteArrayInputStream stream = new ByteArrayInputStream(xml.getBytes());
-        DocumentBuilder builder = dbf.newDocumentBuilder();
-        
-        return builder.parse(stream);
-    }
-    
-    private static String firstChildTextOf(Document doc) throws Exception {
-        NodeList children = doc.getFirstChild().getChildNodes();
-        assertEquals(1, children.getLength());
-        return children.item(0).getNodeValue();
-    }
-    
     // http://code.google.com/p/android/issues/detail?id=2607
     public void test_characterReferences() throws Exception {
         assertEquals("aAb", firstChildTextOf(domOf("<p>a&#65;b</p>")));
@@ -58,14 +37,6 @@
         assertEquals("a\"b", firstChildTextOf(domOf("<p>a&quot;b</p>")));
     }
     
-    private static Element firstElementOf(Document doc) throws Exception {
-        return (Element) doc.getFirstChild();
-    }
-    
-    private static String attrOf(Element e) throws Exception {
-        return e.getAttribute("attr");
-    }
-    
     // http://code.google.com/p/android/issues/detail?id=2487
     public void test_cdata_attributes() throws Exception {
         assertEquals("hello & world", attrOf(firstElementOf(domOf("<?xml version=\"1.0\"?><root attr=\"hello &amp; world\" />"))));
diff --git a/libcore/support/src/test/java/tests/support/Support_Xml.java b/libcore/support/src/test/java/tests/support/Support_Xml.java
new file mode 100644
index 0000000..03ed4a1
--- /dev/null
+++ b/libcore/support/src/test/java/tests/support/Support_Xml.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2009 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 tests.support;
+
+import java.io.ByteArrayInputStream;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import static junit.framework.Assert.assertEquals;
+
+public class Support_Xml {
+    public static Document domOf(String xml) throws Exception {
+        // DocumentBuilderTest assumes we're using DocumentBuilder to do this parsing!
+        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+        dbf.setCoalescing(true);
+        dbf.setExpandEntityReferences(true);
+        
+        ByteArrayInputStream stream = new ByteArrayInputStream(xml.getBytes());
+        DocumentBuilder builder = dbf.newDocumentBuilder();
+        
+        return builder.parse(stream);
+    }
+    
+    public static String firstChildTextOf(Document doc) throws Exception {
+        NodeList children = doc.getFirstChild().getChildNodes();
+        assertEquals(1, children.getLength());
+        return children.item(0).getNodeValue();
+    }
+    
+    public static Element firstElementOf(Document doc) throws Exception {
+        return (Element) doc.getFirstChild();
+    }
+    
+    public static String attrOf(Element e) throws Exception {
+        return e.getAttribute("attr");
+    }
+}
diff --git a/libcore/xml/src/main/java/org/kxml2/io/KXmlSerializer.java b/libcore/xml/src/main/java/org/kxml2/io/KXmlSerializer.java
index d63ed04..1f13357 100644
--- a/libcore/xml/src/main/java/org/kxml2/io/KXmlSerializer.java
+++ b/libcore/xml/src/main/java/org/kxml2/io/KXmlSerializer.java
@@ -124,18 +124,33 @@
                         break;
                     }
                 default :
-                    //if(c < ' ')
-                    //    throw new IllegalArgumentException("Illegal control code:"+((int) c));
-
-                    if (c >= ' ' && c !='@' && (c < 127 || unicode))
+                    // BEGIN android-changed: refuse to output invalid characters
+                    // See http://www.w3.org/TR/REC-xml/#charsets for definition.
+                    // No other Java XML writer we know of does this, but no Java
+                    // XML reader we know of is able to parse the bad output we'd
+                    // otherwise generate.
+                    // Note: tab, newline, and carriage return have already been
+                    // handled above.
+                    boolean valid = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd);
+                    if (!valid) {
+                        reportInvalidCharacter(c);
+                    }
+                    if (unicode || c < 127) {
                         writer.write(c);
-                    else
+                    } else {
                         writer.write("&#" + ((int) c) + ";");
-
+                    }
+                    // END android-changed
             }
         }
     }
 
+    // BEGIN android-added
+    private static void reportInvalidCharacter(char ch) {
+        throw new IllegalArgumentException("Illegal character (" + Integer.toHexString((int) ch) + ")");
+    }
+    // END android-added
+
     /*
         private final void writeIndent() throws IOException {
             writer.write("\r\n");
@@ -540,9 +555,23 @@
 
     public void cdsect(String data) throws IOException {
         check(false);
+        // BEGIN android-changed: ]]> is not allowed within a CDATA,
+        // so break and start a new one when necessary.
+        data = data.replace("]]>", "]]]]><![CDATA[>");
+        char[] chars = data.toCharArray();
+        // We also aren't allowed any invalid characters.
+        for (char ch : chars) {
+            boolean valid = (ch >= 0x20 && ch <= 0xd7ff) ||
+                    (ch == '\t' || ch == '\n' || ch == '\r') ||
+                    (ch >= 0xe000 && ch <= 0xfffd);
+            if (!valid) {
+                reportInvalidCharacter(ch);
+            }
+        }
         writer.write("<![CDATA[");
-        writer.write(data);
+        writer.write(chars, 0, chars.length);
         writer.write("]]>");
+        // END android-changed
     }
 
     public void comment(String comment) throws IOException {
diff --git a/libcore/xml/src/main/java/org/xmlpull/v1/XmlSerializer.java b/libcore/xml/src/main/java/org/xmlpull/v1/XmlSerializer.java
index 8e85e2f..09503ba 100644
--- a/libcore/xml/src/main/java/org/xmlpull/v1/XmlSerializer.java
+++ b/libcore/xml/src/main/java/org/xmlpull/v1/XmlSerializer.java
@@ -5,8 +5,8 @@
 import java.io.Writer;
 
 /**
- * Define an interface to serialziation of XML Infoset.
- * This interface abstracts away if serialized XML is XML 1.0 comaptible text or
+ * Define an interface to serialization of XML Infoset.
+ * This interface abstracts away if serialized XML is XML 1.0 compatible text or
  * other formats of XML 1.0 serializations (such as binary XML for example with WBXML).
  *
  * <p><b>PLEASE NOTE:</b> This interface will be part of XmlPull 1.2 API.
@@ -27,7 +27,7 @@
  * <p><b>NOTE:</b> writing  CDSECT, ENTITY_REF, IGNORABLE_WHITESPACE,
  *  PROCESSING_INSTRUCTION, COMMENT, and DOCDECL in some implementations
  * may not be supported (for example when serializing to WBXML).
- * In such case IllegalStateException will be thrown and it is recommened
+ * In such case IllegalStateException will be thrown and it is recommended
  * to use an optional feature to signal that implementation is not
  * supporting this kind of output.
  */
@@ -40,7 +40,7 @@
      * <a href="http://www.xmlpull.org/v1/doc/features.html">
      * http://www.xmlpull.org/v1/doc/features.html</a>.
      *
-     * If feature is not recocgnized or can not be set
+     * If feature is not recognized or can not be set
      * then IllegalStateException MUST be thrown.
      *
      * @exception IllegalStateException If the feature is not supported or can not be set
@@ -63,12 +63,12 @@
     
     /**
      * Set the value of a property.
-     * (the property name is recommened to be URI for uniqueness).
+     * (the property name is recommended to be URI for uniqueness).
      * Some well known optional properties are defined in
      * <a href="http://www.xmlpull.org/v1/doc/properties.html">
      * http://www.xmlpull.org/v1/doc/properties.html</a>.
      *
-     * If property is not recocgnized or can not be set
+     * If property is not recognized or can not be set
      * then IllegalStateException MUST be thrown.
      *
      * @exception IllegalStateException if the property is not supported or can not be set
@@ -144,7 +144,7 @@
      * If there is no prefix bound to this namespace return null
      * but if generatePrefix is false then return generated prefix.
      *
-     * <p><b>NOTE:</b> if the prefix is empty string "" and defualt namespace is bound
+     * <p><b>NOTE:</b> if the prefix is empty string "" and default namespace is bound
      * to this prefix then empty string ("") is returned.
      *
      * <p><b>NOTE:</b> prefixes "xml" and "xmlns" are already bound
@@ -176,7 +176,7 @@
     /**
      * Returns the namespace URI of the current element as set by startTag().
      *
-     * <p><b>NOTE:</b> that measn in particaulr that: <ul>
+     * <p><b>NOTE:</b> that means in particular that: <ul>
      * <li>if there was startTag("", ...) then getNamespace() returns ""
      * <li>if there was startTag(null, ...) then getNamespace() returns null
      * </ul>
@@ -201,7 +201,7 @@
      * The explicit prefixes for namespaces can be established by calling setPrefix()
      * immediately before this method.
      * If namespace is null no namespace prefix is printed but just name.
-     * If namespace is empty string then serialzier will make sure that
+     * If namespace is empty string then serializer will make sure that
      * default empty namespace is declared (in XML 1.0 xmlns='')
      * or throw IllegalStateException if default namespace is already bound
      * to non-empty string.
@@ -224,7 +224,7 @@
      * <p><b>Background:</b> in kXML endTag had no arguments, and non matching tags were
      *  very difficult to find...
      * If namespace is null no namespace prefix is printed but just name.
-     * If namespace is empty string then serialzier will make sure that
+     * If namespace is empty string then serializer will make sure that
      * default empty namespace is declared (in XML 1.0 xmlns='').
      */
     XmlSerializer endTag (String namespace, String name)
@@ -315,7 +315,7 @@
      * before flush() is called on underlying output stream.
      *
      * <p><b>NOTE:</b> if there is need to close start tag
-     * (so no more attribute() calls are allowed) but without flushinging output
+     * (so no more attribute() calls are allowed) but without flushing output
      * call method text() with empty string (text("")).
      *
      */
@@ -323,4 +323,3 @@
         throws IOException;
     
 }
-
diff --git a/libcore/xml/src/test/java/org/kxml2/io/KXmlSerializerTest.java b/libcore/xml/src/test/java/org/kxml2/io/KXmlSerializerTest.java
index 2d5ddf7..01c7393 100644
--- a/libcore/xml/src/test/java/org/kxml2/io/KXmlSerializerTest.java
+++ b/libcore/xml/src/test/java/org/kxml2/io/KXmlSerializerTest.java
@@ -20,29 +20,100 @@
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.StringWriter;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xmlpull.v1.XmlSerializer;
+
+import static tests.support.Support_Xml.*;
 
 public class KXmlSerializerTest extends TestCase {
-
-    /** the namespace */
-    final String ns = null;
-
-    public void testEmittingNullCharacterThrows() throws IOException {
+    private static final String NAMESPACE = null;
+    
+    private static boolean isValidXmlCodePoint(int c) {
+        // http://www.w3.org/TR/REC-xml/#charsets
+        return (c >= 0x20 && c <= 0xd7ff) || (c == 0x9) || (c == 0xa) || (c == 0xd) ||
+                (c >= 0xe000 && c <= 0xfffd) || (c >= 0x10000 && c <= 0x10ffff);
+    }
+    
+    private static XmlSerializer newSerializer() throws IOException {
         ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-        KXmlSerializer serializer = new KXmlSerializer();
+        XmlSerializer serializer = new KXmlSerializer();
         serializer.setOutput(bytesOut, "UTF-8");
         serializer.startDocument("UTF-8", null);
-
-        serializer.startTag(ns, "foo");
-        try {
-            serializer.text("bar\0baz");
-            fail();
-        } catch (IllegalArgumentException expected) {
+        return serializer;
+    }
+    
+    public void testInvalidCharactersInText() throws IOException {
+        XmlSerializer serializer = newSerializer();
+        serializer.startTag(NAMESPACE, "root");
+        for (int ch = 0; ch <= 0xffff; ++ch) {
+            final String s = Character.toString((char) ch);
+            if (isValidXmlCodePoint(ch)) {
+                serializer.text("a" + s + "b");
+            } else {
+                try {
+                    serializer.text("a" + s + "b");
+                    fail(s);
+                } catch (IllegalArgumentException expected) {
+                }
+            }
         }
-
-        serializer.startTag(ns, "bar");
-        try {
-            serializer.attribute(ns, "baz", "qu\0ux");
-        } catch (IllegalArgumentException expected) {
+        serializer.endTag(NAMESPACE, "root");
+    }
+    
+    public void testInvalidCharactersInAttributeValues() throws IOException {
+        XmlSerializer serializer = newSerializer();
+        serializer.startTag(NAMESPACE, "root");
+        for (int ch = 0; ch <= 0xffff; ++ch) {
+            final String s = Character.toString((char) ch);
+            if (isValidXmlCodePoint(ch)) {
+                serializer.attribute(NAMESPACE, "a", "a" + s + "b");
+            } else {
+                try {
+                    serializer.attribute(NAMESPACE, "a", "a" + s + "b");
+                    fail(s);
+                } catch (IllegalArgumentException expected) {
+                }
+            }
         }
+        serializer.endTag(NAMESPACE, "root");
+    }
+    
+    public void testInvalidCharactersInCdataSections() throws IOException {
+        XmlSerializer serializer = newSerializer();
+        serializer.startTag(NAMESPACE, "root");
+        for (int ch = 0; ch <= 0xffff; ++ch) {
+            final String s = Character.toString((char) ch);
+            if (isValidXmlCodePoint(ch)) {
+                serializer.cdsect("a" + s + "b");
+            } else {
+                try {
+                    serializer.cdsect("a" + s + "b");
+                    fail(s);
+                } catch (IllegalArgumentException expected) {
+                }
+            }
+        }
+        serializer.endTag(NAMESPACE, "root");
+    }
+    
+    public void testCdataWithTerminatorInside() throws Exception {
+        StringWriter writer = new StringWriter();
+        XmlSerializer serializer = new KXmlSerializer();
+        serializer.setOutput(writer);
+        serializer.startDocument("UTF-8", null);
+        serializer.startTag(NAMESPACE, "p");
+        serializer.cdsect("a]]>b");
+        serializer.endTag(NAMESPACE, "p");
+        serializer.endDocument();
+        // Adjacent CDATA sections aren't merged, so let's stick them together ourselves...
+        Document doc = domOf(writer.toString());
+        NodeList children = doc.getFirstChild().getChildNodes();
+        String text = "";
+        for (int i = 0; i < children.getLength(); ++i) {
+            text += children.item(i).getNodeValue();
+        }
+        assertEquals("a]]>b", text);
     }
 }