1/* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.exchange.utility; 19 20import java.io.ByteArrayOutputStream; 21import java.net.URISyntaxException; 22import java.nio.charset.Charset; 23 24class Misc { 25 public static final Charset UTF_8 = Charset.forName("UTF-8"); 26 27 private static final char[] UPPER_CASE_DIGITS = { 28 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 29 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 30 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 31 'U', 'V', 'W', 'X', 'Y', 'Z' 32 }; 33 34 public static String byteToHexString(byte b) { 35 StringBuilder sb = new StringBuilder(); 36 sb.append(UPPER_CASE_DIGITS[(b >> 4) & 0xf]); 37 sb.append(UPPER_CASE_DIGITS[b & 0xf]); 38 return sb.toString(); 39 } 40} 41 42// Note: The UriCodec class is copied verbatim from libcore.net 43 44/** 45 * Encodes and decodes {@code application/x-www-form-urlencoded} content. 46 * Subclasses define exactly which characters are legal. 47 * 48 * <p>By default, UTF-8 is used to encode escaped characters. A single input 49 * character like "\u0080" may be encoded to multiple octets like %C2%80. 50 */ 51public abstract class UriCodec { 52 53 /** 54 * Returns true if {@code c} does not need to be escaped. 55 */ 56 protected abstract boolean isRetained(char c); 57 58 /** 59 * Throws if {@code s} is invalid according to this encoder. 60 */ 61 public final String validate(String uri, int start, int end, String name) 62 throws URISyntaxException { 63 for (int i = start; i < end; ) { 64 char ch = uri.charAt(i); 65 if ((ch >= 'a' && ch <= 'z') 66 || (ch >= 'A' && ch <= 'Z') 67 || (ch >= '0' && ch <= '9') 68 || isRetained(ch)) { 69 i++; 70 } else if (ch == '%') { 71 if (i + 2 >= end) { 72 throw new URISyntaxException(uri, "Incomplete % sequence in " + name, i); 73 } 74 int d1 = hexToInt(uri.charAt(i + 1)); 75 int d2 = hexToInt(uri.charAt(i + 2)); 76 if (d1 == -1 || d2 == -1) { 77 throw new URISyntaxException(uri, "Invalid % sequence: " 78 + uri.substring(i, i + 3) + " in " + name, i); 79 } 80 i += 3; 81 } else { 82 throw new URISyntaxException(uri, "Illegal character in " + name, i); 83 } 84 } 85 return uri.substring(start, end); 86 } 87 88 /** 89 * Throws if {@code s} contains characters that are not letters, digits or 90 * in {@code legal}. 91 */ 92 public static void validateSimple(String s, String legal) 93 throws URISyntaxException { 94 for (int i = 0; i < s.length(); i++) { 95 char ch = s.charAt(i); 96 if (!((ch >= 'a' && ch <= 'z') 97 || (ch >= 'A' && ch <= 'Z') 98 || (ch >= '0' && ch <= '9') 99 || legal.indexOf(ch) > -1)) { 100 throw new URISyntaxException(s, "Illegal character", i); 101 } 102 } 103 } 104 105 /** 106 * Encodes {@code s} and appends the result to {@code builder}. 107 * 108 * @param isPartiallyEncoded true to fix input that has already been 109 * partially or fully encoded. For example, input of "hello%20world" is 110 * unchanged with isPartiallyEncoded=true but would be double-escaped to 111 * "hello%2520world" otherwise. 112 */ 113 private void appendEncoded(StringBuilder builder, String s, Charset charset, 114 boolean isPartiallyEncoded) { 115 if (s == null) { 116 throw new NullPointerException(); 117 } 118 119 int escapeStart = -1; 120 for (int i = 0; i < s.length(); i++) { 121 char c = s.charAt(i); 122 if ((c >= 'a' && c <= 'z') 123 || (c >= 'A' && c <= 'Z') 124 || (c >= '0' && c <= '9') 125 || isRetained(c) 126 || (c == '%' && isPartiallyEncoded)) { 127 if (escapeStart != -1) { 128 appendHex(builder, s.substring(escapeStart, i), charset); 129 escapeStart = -1; 130 } 131 if (c == '%' && isPartiallyEncoded) { 132 // this is an encoded 3-character sequence like "%20" 133 builder.append(s, i, i + 3); 134 i += 2; 135 } else if (c == ' ') { 136 builder.append('+'); 137 } else { 138 builder.append(c); 139 } 140 } else if (escapeStart == -1) { 141 escapeStart = i; 142 } 143 } 144 if (escapeStart != -1) { 145 appendHex(builder, s.substring(escapeStart, s.length()), charset); 146 } 147 } 148 149 public final String encode(String s, Charset charset) { 150 // Guess a bit larger for encoded form 151 StringBuilder builder = new StringBuilder(s.length() + 16); 152 appendEncoded(builder, s, charset, false); 153 return builder.toString(); 154 } 155 156 public final void appendEncoded(StringBuilder builder, String s) { 157 appendEncoded(builder, s, Misc.UTF_8, false); 158 } 159 160 public final void appendPartiallyEncoded(StringBuilder builder, String s) { 161 appendEncoded(builder, s, Misc.UTF_8, true); 162 } 163 164 /** 165 * @param convertPlus true to convert '+' to ' '. 166 */ 167 public static String decode(String s, boolean convertPlus, Charset charset) { 168 if (s.indexOf('%') == -1 && (!convertPlus || s.indexOf('+') == -1)) { 169 return s; 170 } 171 172 StringBuilder result = new StringBuilder(s.length()); 173 ByteArrayOutputStream out = new ByteArrayOutputStream(); 174 for (int i = 0; i < s.length();) { 175 char c = s.charAt(i); 176 if (c == '%') { 177 do { 178 if (i + 2 >= s.length()) { 179 throw new IllegalArgumentException("Incomplete % sequence at: " + i); 180 } 181 int d1 = hexToInt(s.charAt(i + 1)); 182 int d2 = hexToInt(s.charAt(i + 2)); 183 if (d1 == -1 || d2 == -1) { 184 throw new IllegalArgumentException("Invalid % sequence " + 185 s.substring(i, i + 3) + " at " + i); 186 } 187 out.write((byte) ((d1 << 4) + d2)); 188 i += 3; 189 } while (i < s.length() && s.charAt(i) == '%'); 190 result.append(new String(out.toByteArray(), charset)); 191 out.reset(); 192 } else { 193 if (convertPlus && c == '+') { 194 c = ' '; 195 } 196 result.append(c); 197 i++; 198 } 199 } 200 return result.toString(); 201 } 202 203 /** 204 * Like {@link Character#digit}, but without support for non-ASCII 205 * characters. 206 */ 207 private static int hexToInt(char c) { 208 if ('0' <= c && c <= '9') { 209 return c - '0'; 210 } else if ('a' <= c && c <= 'f') { 211 return 10 + (c - 'a'); 212 } else if ('A' <= c && c <= 'F') { 213 return 10 + (c - 'A'); 214 } else { 215 return -1; 216 } 217 } 218 219 public static String decode(String s) { 220 return decode(s, false, Misc.UTF_8); 221 } 222 223 private static void appendHex(StringBuilder builder, String s, Charset charset) { 224 for (byte b : s.getBytes(charset)) { 225 appendHex(builder, b); 226 } 227 } 228 229 private static void appendHex(StringBuilder sb, byte b) { 230 sb.append('%'); 231 sb.append(Misc.byteToHexString(b)); 232 } 233} 234