/* * 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()); } } }