diff options
9 files changed, 400 insertions, 62 deletions
diff --git a/expectations/knownfailures.txt b/expectations/knownfailures.txt index e1d54bc..622d401 100644 --- a/expectations/knownfailures.txt +++ b/expectations/knownfailures.txt @@ -1456,6 +1456,7 @@ "com.squareup.okhttp.internal.spdy.SpdyConnectionTest", "com.squareup.okhttp.internal.http.HttpOverHttp20Draft09Test", "com.squareup.okhttp.internal.http.HttpOverSpdy3Test", + "com.squareup.okhttp.internal.http.ResponseCacheAdapterTest", "com.squareup.okhttp.internal.http.URLConnectionTest#npnSetsProtocolHeader_SPDY_3", "com.squareup.okhttp.internal.http.URLConnectionTest#npnSetsProtocolHeader_HTTP_2", "com.squareup.okhttp.internal.http.URLConnectionTest#zeroLengthPost_SPDY_3", diff --git a/luni/src/main/java/java/util/Locale.java b/luni/src/main/java/java/util/Locale.java index e509a6e..a6368e8 100644 --- a/luni/src/main/java/java/util/Locale.java +++ b/luni/src/main/java/java/util/Locale.java @@ -95,7 +95,7 @@ import libcore.icu.ICU; * <td><a href="http://site.icu-project.org/download/51">ICU 51</a></td> * <td><a href="http://cldr.unicode.org/index/downloads/cldr-23">CLDR 23</a></td> * <td><a href="http://www.unicode.org/versions/Unicode6.2.0/">Unicode 6.2</a></td></tr> - * <tr><td>Android 4.? (STOPSHIP)</td> + * <tr><td>Android 5.0 (Lollipop)</td> * <td><a href="http://site.icu-project.org/download/53">ICU 53</a></td> * <td><a href="http://cldr.unicode.org/index/downloads/cldr-25">CLDR 25</a></td> * <td><a href="http://www.unicode.org/versions/Unicode6.3.0/">Unicode 6.3</a></td></tr> diff --git a/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java b/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java index 33584d8..cfd4089 100644 --- a/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java +++ b/luni/src/main/java/org/apache/harmony/security/utils/JarUtils.java @@ -31,6 +31,7 @@ import java.security.NoSuchAlgorithmException; import java.security.Principal; import java.security.Signature; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; @@ -82,8 +83,10 @@ public class JarUtils { CertificateFactory cf = CertificateFactory.getInstance("X.509"); int i = 0; for (org.apache.harmony.security.x509.Certificate encCert : encCerts) { - final InputStream is = new ByteArrayInputStream(encCert.getEncoded()); - certs[i++] = (X509Certificate) cf.generateCertificate(is); + final byte[] encoded = encCert.getEncoded(); + final InputStream is = new ByteArrayInputStream(encoded); + certs[i++] = new VerbatimX509Certificate((X509Certificate) cf.generateCertificate(is), + encoded); } List<SignerInfo> sigInfos = signedData.getSignerInfos(); @@ -263,4 +266,22 @@ public class JarUtils { return null; } + /** + * For legacy reasons we need to return exactly the original encoded + * certificate bytes, instead of letting the underlying implementation have + * a shot at re-encoding the data. + */ + private static class VerbatimX509Certificate extends WrappedX509Certificate { + private byte[] encodedVerbatim; + + public VerbatimX509Certificate(X509Certificate wrapped, byte[] encodedVerbatim) { + super(wrapped); + this.encodedVerbatim = encodedVerbatim; + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return encodedVerbatim; + } + } } diff --git a/luni/src/main/java/org/apache/harmony/security/utils/WrappedX509Certificate.java b/luni/src/main/java/org/apache/harmony/security/utils/WrappedX509Certificate.java new file mode 100644 index 0000000..2b09309 --- /dev/null +++ b/luni/src/main/java/org/apache/harmony/security/utils/WrappedX509Certificate.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2014 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 org.apache.harmony.security.utils; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Date; +import java.util.Set; + +public class WrappedX509Certificate extends X509Certificate { + private final X509Certificate wrapped; + + public WrappedX509Certificate(X509Certificate wrapped) { + this.wrapped = wrapped; + } + + @Override + public Set<String> getCriticalExtensionOIDs() { + return wrapped.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return wrapped.getExtensionValue(oid); + } + + @Override + public Set<String> getNonCriticalExtensionOIDs() { + return wrapped.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return wrapped.hasUnsupportedCriticalExtension(); + } + + @Override + public void checkValidity() throws CertificateExpiredException, + CertificateNotYetValidException { + wrapped.checkValidity(); + } + + @Override + public void checkValidity(Date date) throws CertificateExpiredException, + CertificateNotYetValidException { + wrapped.checkValidity(date); + } + + @Override + public int getVersion() { + return wrapped.getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return wrapped.getSerialNumber(); + } + + @Override + public Principal getIssuerDN() { + return wrapped.getIssuerDN(); + } + + @Override + public Principal getSubjectDN() { + return wrapped.getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return wrapped.getNotBefore(); + } + + @Override + public Date getNotAfter() { + return wrapped.getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return wrapped.getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return wrapped.getSignature(); + } + + @Override + public String getSigAlgName() { + return wrapped.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return wrapped.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return wrapped.getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return wrapped.getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return wrapped.getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return wrapped.getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return wrapped.getBasicConstraints(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return wrapped.getEncoded(); + } + + @Override + public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, + InvalidKeyException, NoSuchProviderException, SignatureException { + wrapped.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) throws CertificateException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, + SignatureException { + verify(key, sigProvider); + } + + @Override + public String toString() { + return wrapped.toString(); + } + + @Override + public PublicKey getPublicKey() { + return wrapped.getPublicKey(); + } +} diff --git a/luni/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java b/luni/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java index d1079c8..040a012 100644 --- a/luni/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java +++ b/luni/src/main/java/org/apache/harmony/xml/parsers/DocumentBuilderImpl.java @@ -416,11 +416,13 @@ class DocumentBuilderImpl extends DocumentBuilder { private String resolveCharacterReference(String value, int base) { try { - int ch = Integer.parseInt(value, base); - if (ch < 0 || ch > Character.MAX_VALUE) { - return null; + int codePoint = Integer.parseInt(value, base); + if (Character.isBmpCodePoint(codePoint)) { + return String.valueOf((char) codePoint); + } else { + char[] surrogatePair = Character.toChars(codePoint); + return new String(surrogatePair); } - return String.valueOf((char) ch); } catch (NumberFormatException ex) { return null; } diff --git a/luni/src/test/java/libcore/icu/DateIntervalFormatTest.java b/luni/src/test/java/libcore/icu/DateIntervalFormatTest.java index bac8138..1c1296b 100644 --- a/luni/src/test/java/libcore/icu/DateIntervalFormatTest.java +++ b/luni/src/test/java/libcore/icu/DateIntervalFormatTest.java @@ -408,4 +408,16 @@ public class DateIntervalFormatTest extends junit.framework.TestCase { assertEquals("یکشنبه د ۱۹۸۰ د فبروري ۱۰", formatDateRange(new Locale("ps"), utc, thisYear, thisYear, flags)); assertEquals("วันอาทิตย์ 10 กุมภาพันธ์ 1980", formatDateRange(new Locale("th"), utc, thisYear, thisYear, flags)); } + + // http://b/13234532 + public void test13234532() throws Exception { + Locale l = Locale.US; + TimeZone utc = TimeZone.getTimeZone("UTC"); + + int flags = FORMAT_SHOW_TIME | FORMAT_ABBREV_ALL | FORMAT_12HOUR; + + assertEquals("10 – 11 AM", formatDateRange(l, utc, 10*HOUR, 11*HOUR, flags)); + assertEquals("11 AM – 1 PM", formatDateRange(l, utc, 11*HOUR, 13*HOUR, flags)); + assertEquals("2 – 3 PM", formatDateRange(l, utc, 14*HOUR, 15*HOUR, flags)); + } } diff --git a/luni/src/test/java/libcore/xml/KxmlSerializerTest.java b/luni/src/test/java/libcore/xml/KxmlSerializerTest.java index 6a75a9b..5f68a99 100644 --- a/luni/src/test/java/libcore/xml/KxmlSerializerTest.java +++ b/luni/src/test/java/libcore/xml/KxmlSerializerTest.java @@ -22,7 +22,9 @@ import java.io.StringWriter; import junit.framework.TestCase; import org.kxml2.io.KXmlSerializer; import org.w3c.dom.Document; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.w3c.dom.Text; import org.xmlpull.v1.XmlSerializer; import static tests.support.Support_Xml.domOf; @@ -87,12 +89,67 @@ public final class KxmlSerializerTest extends TestCase { return serializer; } + public String fromCodePoint(int codePoint) { + if (codePoint > Character.MAX_VALUE) { + return new String(Character.toChars(codePoint)); + } + return Character.toString((char) codePoint); + } + + // http://b/17960630 + public void testSpeakNoEvilMonkeys() throws Exception { + StringWriter stringWriter = new StringWriter(); + XmlSerializer serializer = new KXmlSerializer(); + serializer.setOutput(stringWriter); + serializer.startDocument("UTF-8", null); + serializer.startTag(NAMESPACE, "tag"); + serializer.attribute(NAMESPACE, "attr", "a\ud83d\ude4ab"); + serializer.text("c\ud83d\ude4ad"); + serializer.cdsect("e\ud83d\ude4af"); + serializer.endTag(NAMESPACE, "tag"); + serializer.endDocument(); + assertXmlEquals("<tag attr=\"a🙊b\">" + + "c🙊d" + + "<![CDATA[e]]>🙊<![CDATA[f]]>" + + "</tag>", stringWriter.toString()); + + // Check we can parse what we just output. + Document doc = domOf(stringWriter.toString()); + Node root = doc.getDocumentElement(); + assertEquals("a\ud83d\ude4ab", root.getAttributes().getNamedItem("attr").getNodeValue()); + Text text = (Text) root.getFirstChild(); + assertEquals("c\ud83d\ude4ade\ud83d\ude4af", text.getNodeValue()); + } + + public void testBadSurrogates() throws Exception { + StringWriter stringWriter = new StringWriter(); + XmlSerializer serializer = new KXmlSerializer(); + serializer.setOutput(stringWriter); + serializer.startDocument("UTF-8", null); + serializer.startTag(NAMESPACE, "tag"); + try { + serializer.attribute(NAMESPACE, "attr", "a\ud83d\u0040b"); + } catch (IllegalArgumentException expected) { + } + try { + serializer.text("c\ud83d\u0040d"); + } catch (IllegalArgumentException expected) { + } + try { + serializer.cdsect("e\ud83d\u0040f"); + } catch (IllegalArgumentException expected) { + } + } + + // Cover all the BMP code points plus a few that require us to use surrogates. + private static int MAX_TEST_CODE_POINT = 0x10008; + 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)) { + for (int c = 0; c <= MAX_TEST_CODE_POINT; ++c) { + final String s = fromCodePoint(c); + if (isValidXmlCodePoint(c)) { serializer.text("a" + s + "b"); } else { try { @@ -108,9 +165,9 @@ public final class KxmlSerializerTest extends TestCase { 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)) { + for (int c = 0; c <= MAX_TEST_CODE_POINT; ++c) { + final String s = fromCodePoint(c); + if (isValidXmlCodePoint(c)) { serializer.attribute(NAMESPACE, "a", "a" + s + "b"); } else { try { @@ -126,9 +183,9 @@ public final class KxmlSerializerTest extends TestCase { 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)) { + for (int c = 0; c <= MAX_TEST_CODE_POINT; ++c) { + final String s = fromCodePoint(c); + if (isValidXmlCodePoint(c)) { serializer.cdsect("a" + s + "b"); } else { try { diff --git a/support/src/test/java/libcore/java/security/TestKeyStore.java b/support/src/test/java/libcore/java/security/TestKeyStore.java index 203c028..bd64360 100644 --- a/support/src/test/java/libcore/java/security/TestKeyStore.java +++ b/support/src/test/java/libcore/java/security/TestKeyStore.java @@ -47,6 +47,7 @@ import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; +import java.security.UnrecoverableEntryException; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateFactory; @@ -260,6 +261,7 @@ public final class TestKeyStore extends Assert { private X500Principal subject; private int keyUsage; private boolean ca; + private PrivateKeyEntry privateEntry; private PrivateKeyEntry signer; private Certificate rootCa; private final List<KeyPurposeId> extendedKeyUsages = new ArrayList<KeyPurposeId>(); @@ -314,6 +316,12 @@ public final class TestKeyStore extends Assert { return this; } + /** a private key entry to use for the generation of the certificate */ + public Builder privateEntry(PrivateKeyEntry privateEntry) { + this.privateEntry = privateEntry; + return this; + } + /** a private key entry to be used for signing, otherwise self-sign */ public Builder signer(PrivateKeyEntry signer) { this.signer = signer; @@ -368,21 +376,32 @@ public final class TestKeyStore extends Assert { } } + /* + * This is not implemented for other key types because the logic + * would be long to write and it's not needed currently. + */ + if (privateEntry != null + && (keyAlgorithms.length != 1 || !"RSA".equals(keyAlgorithms[0]))) { + throw new IllegalStateException( + "Only reusing an existing key is implemented for RSA"); + } + KeyStore keyStore = createKeyStore(); for (String keyAlgorithm : keyAlgorithms) { String publicAlias = aliasPrefix + "-public-" + keyAlgorithm; String privateAlias = aliasPrefix + "-private-" + keyAlgorithm; if ((keyAlgorithm.equals("EC_RSA") || keyAlgorithm.equals("DH_RSA")) && signer == null && rootCa == null) { - createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, - privateKey(keyStore, keyPassword, "RSA", "RSA")); + createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, null, + privateKey(keyStore, keyPassword, "RSA", "RSA")); continue; } else if (keyAlgorithm.equals("DH_DSA") && signer == null && rootCa == null) { - createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, + createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, null, privateKey(keyStore, keyPassword, "DSA", "DSA")); continue; } - createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, signer); + createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, privateEntry, + signer); } if (rootCa != null) { keyStore.setCertificateEntry(aliasPrefix @@ -416,6 +435,7 @@ public final class TestKeyStore extends Assert { String keyAlgorithm, String publicAlias, String privateAlias, + PrivateKeyEntry privateEntry, PrivateKeyEntry signer) throws Exception { PrivateKey caKey; X509Certificate caCert; @@ -430,41 +450,50 @@ public final class TestKeyStore extends Assert { caCertChain = (X509Certificate[])signer.getCertificateChain(); } - PrivateKey privateKey; + final PrivateKey privateKey; + final PublicKey publicKey; X509Certificate x509c; if (publicAlias == null && privateAlias == null) { // don't want anything apparently privateKey = null; + publicKey = null; x509c = null; } else { - // 1.) we make the keys - int keySize; - if (keyAlgorithm.equals("RSA")) { - // 512 breaks SSL_RSA_EXPORT_* on RI and TLS_ECDHE_RSA_WITH_RC4_128_SHA for us - keySize = 1024; - } else if (keyAlgorithm.equals("DH_RSA")) { - keySize = 512; - keyAlgorithm = "DH"; - } else if (keyAlgorithm.equals("DSA")) { - keySize = 512; - } else if (keyAlgorithm.equals("DH_DSA")) { - keySize = 512; - keyAlgorithm = "DH"; - } else if (keyAlgorithm.equals("EC")) { - keySize = 256; - } else if (keyAlgorithm.equals("EC_RSA")) { - keySize = 256; - keyAlgorithm = "EC"; - } else { - throw new IllegalArgumentException("Unknown key algorithm " + keyAlgorithm); - } + if (privateEntry == null) { + // 1a.) we make the keys + int keySize; + if (keyAlgorithm.equals("RSA")) { + // 512 breaks SSL_RSA_EXPORT_* on RI and + // TLS_ECDHE_RSA_WITH_RC4_128_SHA for us + keySize = 1024; + } else if (keyAlgorithm.equals("DH_RSA")) { + keySize = 512; + keyAlgorithm = "DH"; + } else if (keyAlgorithm.equals("DSA")) { + keySize = 512; + } else if (keyAlgorithm.equals("DH_DSA")) { + keySize = 512; + keyAlgorithm = "DH"; + } else if (keyAlgorithm.equals("EC")) { + keySize = 256; + } else if (keyAlgorithm.equals("EC_RSA")) { + keySize = 256; + keyAlgorithm = "EC"; + } else { + throw new IllegalArgumentException("Unknown key algorithm " + keyAlgorithm); + } - KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlgorithm); - kpg.initialize(keySize, new SecureRandom()); + KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyAlgorithm); + kpg.initialize(keySize, new SecureRandom()); - KeyPair kp = kpg.generateKeyPair(); - privateKey = kp.getPrivate(); - PublicKey publicKey = kp.getPublic(); + KeyPair kp = kpg.generateKeyPair(); + privateKey = kp.getPrivate(); + publicKey = kp.getPublic(); + } else { + // 1b.) we use the previous keys + privateKey = privateEntry.getPrivateKey(); + publicKey = privateEntry.getCertificate().getPublicKey(); + } // 2.) use keys to make certificate X500Principal issuer = ((caCert != null) @@ -820,6 +849,24 @@ public final class TestKeyStore extends Assert { } /** + * Return an {@code X509Certificate that matches the given {@code alias}. + */ + public KeyStore.Entry getEntryByAlias(String alias) { + return entryByAlias(keyStore, alias); + } + + /** + * Finds an entry in the keystore by the given alias. + */ + public static KeyStore.Entry entryByAlias(KeyStore keyStore, String alias) { + try { + return keyStore.getEntry(alias, null); + } catch (NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) { + throw new RuntimeException(e); + } + } + + /** * Create a client key store that only contains self-signed certificates but no private keys */ public static KeyStore createClient(KeyStore caKeyStore) { diff --git a/xml/src/main/java/org/kxml2/io/KXmlSerializer.java b/xml/src/main/java/org/kxml2/io/KXmlSerializer.java index 8fa2756..bfdeece 100644 --- a/xml/src/main/java/org/kxml2/io/KXmlSerializer.java +++ b/xml/src/main/java/org/kxml2/io/KXmlSerializer.java @@ -125,14 +125,18 @@ public class KXmlSerializer implements XmlSerializer { // 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); + boolean allowedInXml = (c >= 0x20 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xfffd); + if (allowedInXml) { + if (unicode || c < 127) { + writer.write(c); + } else { + writer.write("&#" + ((int) c) + ";"); + } + } else if (Character.isHighSurrogate(c) && i < s.length() - 1) { + writeSurrogate(c, s.charAt(i + 1)); + ++i; } else { - writer.write("&#" + ((int) c) + ";"); + reportInvalidCharacter(c); } // END android-changed } @@ -141,7 +145,7 @@ public class KXmlSerializer implements XmlSerializer { // BEGIN android-added private static void reportInvalidCharacter(char ch) { - throw new IllegalArgumentException("Illegal character (" + Integer.toHexString((int) ch) + ")"); + throw new IllegalArgumentException("Illegal character (U+" + Integer.toHexString((int) ch) + ")"); } // END android-added @@ -548,22 +552,41 @@ public class KXmlSerializer implements XmlSerializer { // 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) || + writer.write("<![CDATA["); + for (int i = 0; i < data.length(); ++i) { + char ch = data.charAt(i); + boolean allowedInCdata = (ch >= 0x20 && ch <= 0xd7ff) || (ch == '\t' || ch == '\n' || ch == '\r') || (ch >= 0xe000 && ch <= 0xfffd); - if (!valid) { + if (allowedInCdata) { + writer.write(ch); + } else if (Character.isHighSurrogate(ch) && i < data.length() - 1) { + // Character entities aren't valid in CDATA, so break out for this. + writer.write("]]>"); + writeSurrogate(ch, data.charAt(++i)); + writer.write("<![CDATA["); + } else { reportInvalidCharacter(ch); } } - writer.write("<![CDATA["); - writer.write(chars, 0, chars.length); writer.write("]]>"); // END android-changed } + // BEGIN android-added + private void writeSurrogate(char high, char low) throws IOException { + if (!Character.isLowSurrogate(low)) { + throw new IllegalArgumentException("Bad surrogate pair (U+" + Integer.toHexString((int) high) + + " U+" + Integer.toHexString((int) low) + ")"); + } + // Java-style surrogate pairs aren't allowed in XML. We could use the > 3-byte encodings, but that + // seems likely to upset anything expecting modified UTF-8 rather than "real" UTF-8. It seems more + // conservative in a Java environment to use an entity reference instead. + int codePoint = Character.toCodePoint(high, low); + writer.write("&#" + codePoint + ";"); + } + // END android-added + public void comment(String comment) throws IOException { check(false); writer.write("<!--"); |