Merge "Move UriCodec to be near its one user"
am: 55d8b54ac3
Change-Id: I847df7436d4ef6d1bac9513e7b3579abecc6f87d
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java
index 0fb84b7..b7f5cdf 100644
--- a/core/java/android/net/Uri.java
+++ b/core/java/android/net/Uri.java
@@ -24,8 +24,6 @@
import android.os.StrictMode;
import android.util.Log;
-import libcore.net.UriCodec;
-
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@@ -1959,7 +1957,8 @@
if (s == null) {
return null;
}
- return UriCodec.decode(s, false, StandardCharsets.UTF_8, false);
+ return UriCodec.decode(
+ s, false /* convertPlus */, StandardCharsets.UTF_8, false /* throwOnFailure */);
}
/**
diff --git a/core/java/android/net/UriCodec.java b/core/java/android/net/UriCodec.java
new file mode 100644
index 0000000..e1470e0
--- /dev/null
+++ b/core/java/android/net/UriCodec.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 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 android.net;
+
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+
+/**
+ * Decodes “application/x-www-form-urlencoded” content.
+ *
+ * @hide
+ */
+public final class UriCodec {
+
+ private UriCodec() {}
+
+ /**
+ * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f').
+ */
+ private static int hexCharToValue(char c) {
+ if ('0' <= c && c <= '9') {
+ return c - '0';
+ }
+ if ('a' <= c && c <= 'f') {
+ return 10 + c - 'a';
+ }
+ if ('A' <= c && c <= 'F') {
+ return 10 + c - 'A';
+ }
+ return -1;
+ }
+
+ private static URISyntaxException unexpectedCharacterException(
+ String uri, String name, char unexpected, int index) {
+ String nameString = (name == null) ? "" : " in [" + name + "]";
+ return new URISyntaxException(
+ uri, "Unexpected character" + nameString + ": " + unexpected, index);
+ }
+
+ private static char getNextCharacter(String uri, int index, int end, String name)
+ throws URISyntaxException {
+ if (index >= end) {
+ String nameString = (name == null) ? "" : " in [" + name + "]";
+ throw new URISyntaxException(
+ uri, "Unexpected end of string" + nameString, index);
+ }
+ return uri.charAt(index);
+ }
+
+ /**
+ * Decode a string according to the rules of this decoder.
+ *
+ * - if {@code convertPlus == true} all ‘+’ chars in the decoded output are converted to ‘ ‘
+ * (white space)
+ * - if {@code throwOnFailure == true}, an {@link IllegalArgumentException} is thrown for
+ * invalid inputs. Else, U+FFFd is emitted to the output in place of invalid input octets.
+ */
+ public static String decode(
+ String s, boolean convertPlus, Charset charset, boolean throwOnFailure) {
+ StringBuilder builder = new StringBuilder(s.length());
+ appendDecoded(builder, s, convertPlus, charset, throwOnFailure);
+ return builder.toString();
+ }
+
+ /**
+ * Character to be output when there's an error decoding an input.
+ */
+ private static final char INVALID_INPUT_CHARACTER = '\ufffd';
+
+ private static void appendDecoded(
+ StringBuilder builder,
+ String s,
+ boolean convertPlus,
+ Charset charset,
+ boolean throwOnFailure) {
+ CharsetDecoder decoder = charset.newDecoder()
+ .onMalformedInput(CodingErrorAction.REPLACE)
+ .replaceWith("\ufffd")
+ .onUnmappableCharacter(CodingErrorAction.REPORT);
+ // Holds the bytes corresponding to the escaped chars being read (empty if the last char
+ // wasn't a escaped char).
+ ByteBuffer byteBuffer = ByteBuffer.allocate(s.length());
+ int i = 0;
+ while (i < s.length()) {
+ char c = s.charAt(i);
+ i++;
+ switch (c) {
+ case '+':
+ flushDecodingByteAccumulator(
+ builder, decoder, byteBuffer, throwOnFailure);
+ builder.append(convertPlus ? ' ' : '+');
+ break;
+ case '%':
+ // Expect two characters representing a number in hex.
+ byte hexValue = 0;
+ for (int j = 0; j < 2; j++) {
+ try {
+ c = getNextCharacter(s, i, s.length(), null /* name */);
+ } catch (URISyntaxException e) {
+ // Unexpected end of input.
+ if (throwOnFailure) {
+ throw new IllegalArgumentException(e);
+ } else {
+ flushDecodingByteAccumulator(
+ builder, decoder, byteBuffer, throwOnFailure);
+ builder.append(INVALID_INPUT_CHARACTER);
+ return;
+ }
+ }
+ i++;
+ int newDigit = hexCharToValue(c);
+ if (newDigit < 0) {
+ if (throwOnFailure) {
+ throw new IllegalArgumentException(
+ unexpectedCharacterException(s, null /* name */, c, i - 1));
+ } else {
+ flushDecodingByteAccumulator(
+ builder, decoder, byteBuffer, throwOnFailure);
+ builder.append(INVALID_INPUT_CHARACTER);
+ break;
+ }
+ }
+ hexValue = (byte) (hexValue * 0x10 + newDigit);
+ }
+ byteBuffer.put(hexValue);
+ break;
+ default:
+ flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure);
+ builder.append(c);
+ }
+ }
+ flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure);
+ }
+
+ private static void flushDecodingByteAccumulator(
+ StringBuilder builder,
+ CharsetDecoder decoder,
+ ByteBuffer byteBuffer,
+ boolean throwOnFailure) {
+ if (byteBuffer.position() == 0) {
+ return;
+ }
+ byteBuffer.flip();
+ try {
+ builder.append(decoder.decode(byteBuffer));
+ } catch (CharacterCodingException e) {
+ if (throwOnFailure) {
+ throw new IllegalArgumentException(e);
+ } else {
+ builder.append(INVALID_INPUT_CHARACTER);
+ }
+ } finally {
+ // Use the byte buffer to write again.
+ byteBuffer.flip();
+ byteBuffer.limit(byteBuffer.capacity());
+ }
+ }
+}
diff --git a/core/tests/coretests/src/android/net/UriCodecTest.java b/core/tests/coretests/src/android/net/UriCodecTest.java
new file mode 100644
index 0000000..7fe9402
--- /dev/null
+++ b/core/tests/coretests/src/android/net/UriCodecTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2015 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 android.net;
+
+import junit.framework.TestCase;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Tests for {@link UriCodec}
+ */
+public class UriCodecTest extends TestCase {
+
+ public void testDecode_emptyString_returnsEmptyString() {
+ assertEquals("", UriCodec.decode("",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ true /* throwOnFailure */));
+ }
+
+ public void testDecode_wrongHexDigit_fails() {
+ try {
+ // %p in the end.
+ UriCodec.decode("ab%2f$%C4%82%25%e0%a1%80%p",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ true /* throwOnFailure */);
+ fail("Expected URISyntaxException");
+ } catch (IllegalArgumentException expected) {
+ // Expected.
+ }
+ }
+
+ public void testDecode_secondHexDigitWrong_fails() {
+ try {
+ // %1p in the end.
+ UriCodec.decode("ab%2f$%c4%82%25%e0%a1%80%1p",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ true /* throwOnFailure */);
+ fail("Expected URISyntaxException");
+ } catch (IllegalArgumentException expected) {
+ // Expected.
+ }
+ }
+
+ public void testDecode_endsWithPercent_fails() {
+ try {
+ // % in the end.
+ UriCodec.decode("ab%2f$%c4%82%25%e0%a1%80%",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ true /* throwOnFailure */);
+ fail("Expected URISyntaxException");
+ } catch (IllegalArgumentException expected) {
+ // Expected.
+ }
+ }
+
+ public void testDecode_dontThrowException_appendsUnknownCharacter() {
+ assertEquals("ab/$\u0102%\u0840\ufffd",
+ UriCodec.decode("ab%2f$%c4%82%25%e0%a1%80%",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ false /* throwOnFailure */));
+ }
+
+ public void testDecode_convertPlus() {
+ assertEquals("ab/$\u0102% \u0840",
+ UriCodec.decode("ab%2f$%c4%82%25+%e0%a1%80",
+ true /* convertPlus */,
+ StandardCharsets.UTF_8,
+ false /* throwOnFailure */));
+ }
+
+ // Last character needs decoding (make sure we are flushing the buffer with chars to decode).
+ public void testDecode_lastCharacter() {
+ assertEquals("ab/$\u0102%\u0840",
+ UriCodec.decode("ab%2f$%c4%82%25%e0%a1%80",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ true /* throwOnFailure */));
+ }
+
+ // Check that a second row of encoded characters is decoded properly (internal buffers are
+ // reset properly).
+ public void testDecode_secondRowOfEncoded() {
+ assertEquals("ab/$\u0102%\u0840aa\u0840",
+ UriCodec.decode("ab%2f$%c4%82%25%e0%a1%80aa%e0%a1%80",
+ false /* convertPlus */,
+ StandardCharsets.UTF_8,
+ true /* throwOnFailure */));
+ }
+}