1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.net; 18 19 import java.net.URISyntaxException; 20 import java.nio.ByteBuffer; 21 import java.nio.charset.CharacterCodingException; 22 import java.nio.charset.Charset; 23 import java.nio.charset.CharsetDecoder; 24 import java.nio.charset.CodingErrorAction; 25 26 /** 27 * Decodes “application/x-www-form-urlencoded” content. 28 * 29 * @hide 30 */ 31 public final class UriCodec { 32 UriCodec()33 private UriCodec() {} 34 35 /** 36 * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f'). 37 */ hexCharToValue(char c)38 private static int hexCharToValue(char c) { 39 if ('0' <= c && c <= '9') { 40 return c - '0'; 41 } 42 if ('a' <= c && c <= 'f') { 43 return 10 + c - 'a'; 44 } 45 if ('A' <= c && c <= 'F') { 46 return 10 + c - 'A'; 47 } 48 return -1; 49 } 50 unexpectedCharacterException( String uri, String name, char unexpected, int index)51 private static URISyntaxException unexpectedCharacterException( 52 String uri, String name, char unexpected, int index) { 53 String nameString = (name == null) ? "" : " in [" + name + "]"; 54 return new URISyntaxException( 55 uri, "Unexpected character" + nameString + ": " + unexpected, index); 56 } 57 getNextCharacter(String uri, int index, int end, String name)58 private static char getNextCharacter(String uri, int index, int end, String name) 59 throws URISyntaxException { 60 if (index >= end) { 61 String nameString = (name == null) ? "" : " in [" + name + "]"; 62 throw new URISyntaxException( 63 uri, "Unexpected end of string" + nameString, index); 64 } 65 return uri.charAt(index); 66 } 67 68 /** 69 * Decode a string according to the rules of this decoder. 70 * 71 * - if {@code convertPlus == true} all ‘+’ chars in the decoded output are converted to ‘ ‘ 72 * (white space) 73 * - if {@code throwOnFailure == true}, an {@link IllegalArgumentException} is thrown for 74 * invalid inputs. Else, U+FFFd is emitted to the output in place of invalid input octets. 75 */ decode( String s, boolean convertPlus, Charset charset, boolean throwOnFailure)76 public static String decode( 77 String s, boolean convertPlus, Charset charset, boolean throwOnFailure) { 78 StringBuilder builder = new StringBuilder(s.length()); 79 appendDecoded(builder, s, convertPlus, charset, throwOnFailure); 80 return builder.toString(); 81 } 82 83 /** 84 * Character to be output when there's an error decoding an input. 85 */ 86 private static final char INVALID_INPUT_CHARACTER = '\ufffd'; 87 appendDecoded( StringBuilder builder, String s, boolean convertPlus, Charset charset, boolean throwOnFailure)88 private static void appendDecoded( 89 StringBuilder builder, 90 String s, 91 boolean convertPlus, 92 Charset charset, 93 boolean throwOnFailure) { 94 CharsetDecoder decoder = charset.newDecoder() 95 .onMalformedInput(CodingErrorAction.REPLACE) 96 .replaceWith("\ufffd") 97 .onUnmappableCharacter(CodingErrorAction.REPORT); 98 // Holds the bytes corresponding to the escaped chars being read (empty if the last char 99 // wasn't a escaped char). 100 ByteBuffer byteBuffer = ByteBuffer.allocate(s.length()); 101 int i = 0; 102 while (i < s.length()) { 103 char c = s.charAt(i); 104 i++; 105 switch (c) { 106 case '+': 107 flushDecodingByteAccumulator( 108 builder, decoder, byteBuffer, throwOnFailure); 109 builder.append(convertPlus ? ' ' : '+'); 110 break; 111 case '%': 112 // Expect two characters representing a number in hex. 113 byte hexValue = 0; 114 for (int j = 0; j < 2; j++) { 115 try { 116 c = getNextCharacter(s, i, s.length(), null /* name */); 117 } catch (URISyntaxException e) { 118 // Unexpected end of input. 119 if (throwOnFailure) { 120 throw new IllegalArgumentException(e); 121 } else { 122 flushDecodingByteAccumulator( 123 builder, decoder, byteBuffer, throwOnFailure); 124 builder.append(INVALID_INPUT_CHARACTER); 125 return; 126 } 127 } 128 i++; 129 int newDigit = hexCharToValue(c); 130 if (newDigit < 0) { 131 if (throwOnFailure) { 132 throw new IllegalArgumentException( 133 unexpectedCharacterException(s, null /* name */, c, i - 1)); 134 } else { 135 flushDecodingByteAccumulator( 136 builder, decoder, byteBuffer, throwOnFailure); 137 builder.append(INVALID_INPUT_CHARACTER); 138 break; 139 } 140 } 141 hexValue = (byte) (hexValue * 0x10 + newDigit); 142 } 143 byteBuffer.put(hexValue); 144 break; 145 default: 146 flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure); 147 builder.append(c); 148 } 149 } 150 flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure); 151 } 152 flushDecodingByteAccumulator( StringBuilder builder, CharsetDecoder decoder, ByteBuffer byteBuffer, boolean throwOnFailure)153 private static void flushDecodingByteAccumulator( 154 StringBuilder builder, 155 CharsetDecoder decoder, 156 ByteBuffer byteBuffer, 157 boolean throwOnFailure) { 158 if (byteBuffer.position() == 0) { 159 return; 160 } 161 byteBuffer.flip(); 162 try { 163 builder.append(decoder.decode(byteBuffer)); 164 } catch (CharacterCodingException e) { 165 if (throwOnFailure) { 166 throw new IllegalArgumentException(e); 167 } else { 168 builder.append(INVALID_INPUT_CHARACTER); 169 } 170 } finally { 171 // Use the byte buffer to write again. 172 byteBuffer.flip(); 173 byteBuffer.limit(byteBuffer.capacity()); 174 } 175 } 176 } 177