diff options
| -rw-r--r-- | core/java/android/net/Uri.java | 5 | ||||
| -rw-r--r-- | core/java/android/net/UriCodec.java | 176 | ||||
| -rw-r--r-- | core/tests/coretests/src/android/net/UriCodecTest.java | 108 |
3 files changed, 286 insertions, 3 deletions
diff --git a/core/java/android/net/Uri.java b/core/java/android/net/Uri.java index 0fb84b723634..b7f5cdfabc46 100644 --- a/core/java/android/net/Uri.java +++ b/core/java/android/net/Uri.java @@ -24,8 +24,6 @@ import android.os.Parcelable; 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 @@ public abstract class Uri implements Parcelable, Comparable<Uri> { 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 000000000000..e1470e0a958f --- /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 000000000000..7fe9402c6ef0 --- /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 */)); + } +} |