Home | History | Annotate | Download | only in net
      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 
     33     private UriCodec() {}
     34 
     35     /**
     36      * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f').
     37      */
     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 
     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 
     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      */
     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 
     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 
    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