001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.apache.commons.codec.net; 019 020import java.io.ByteArrayOutputStream; 021import java.io.UnsupportedEncodingException; 022import java.net.URLDecoder; 023import java.net.URLEncoder; 024import java.util.BitSet; 025 026import org.apache.commons.codec.BinaryDecoder; 027import org.apache.commons.codec.BinaryEncoder; 028import org.apache.commons.codec.CharEncoding; 029import org.apache.commons.codec.DecoderException; 030import org.apache.commons.codec.EncoderException; 031import org.apache.commons.codec.StringDecoder; 032import org.apache.commons.codec.StringEncoder; 033import org.apache.commons.codec.binary.StringUtils; 034 035/** 036 * Implements the 'www-form-urlencoded' encoding scheme, also misleadingly known as URL encoding. 037 * <p> 038 * This codec is meant to be a replacement for standard Java classes {@link URLEncoder} and 039 * {@link URLDecoder} on older Java platforms, as these classes in Java versions below 040 * 1.4 rely on the platform's default charset encoding. 041 * </p> 042 * <p> 043 * This class is thread-safe as of 1.11 044 * </p> 045 * 046 * @see <a href="https://www.w3.org/TR/html4/interact/forms.html#h-17.13.4.1">Chapter 17.13.4 Form content types</a> 047 * of the <a href="https://www.w3.org/TR/html4/">HTML 4.01 Specification</a> 048 * 049 * @since 1.2 050 */ 051public class URLCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder { 052 053 /** 054 * Release 1.5 made this field final. 055 */ 056 protected static final byte ESCAPE_CHAR = '%'; 057 058 private static final byte PLUS_CHAR = '+'; 059 060 /** 061 * BitSet of www-form-url safe characters. 062 * This is a copy of the internal BitSet which is now used for the conversion. 063 * Changes to this field are ignored. 064 * 065 * @deprecated 1.11 Will be removed in 2.0 (CODEC-230) 066 */ 067 @Deprecated 068 protected static final BitSet WWW_FORM_URL; 069 070 private static final BitSet WWW_FORM_URL_SAFE = new BitSet(256); 071 072 // Static initializer for www_form_url 073 static { 074 // alpha characters 075 for (int i = 'a'; i <= 'z'; i++) { 076 WWW_FORM_URL_SAFE.set(i); 077 } 078 for (int i = 'A'; i <= 'Z'; i++) { 079 WWW_FORM_URL_SAFE.set(i); 080 } 081 // numeric characters 082 for (int i = '0'; i <= '9'; i++) { 083 WWW_FORM_URL_SAFE.set(i); 084 } 085 // special chars 086 WWW_FORM_URL_SAFE.set('-'); 087 WWW_FORM_URL_SAFE.set('_'); 088 WWW_FORM_URL_SAFE.set('.'); 089 WWW_FORM_URL_SAFE.set('*'); 090 // blank to be replaced with + 091 WWW_FORM_URL_SAFE.set(' '); 092 093 // Create a copy in case anyone (ab)uses it 094 WWW_FORM_URL = (BitSet) WWW_FORM_URL_SAFE.clone(); 095 } 096 097 /** 098 * Decodes an array of bytes using the safe set supplied to {@link #encodeUrl(BitSet, byte[])}. 099 * <p> 100 * A percent sign marked safe is copied literally; otherwise it starts a two-digit hexadecimal escape. A plus sign marked safe is copied literally. 101 * Otherwise, a plus sign becomes a space only if space is marked safe. All other bytes are copied unchanged. A {@code null} bitset selects the default 102 * {@code www-form-urlencoded} safe set, giving the same behavior as {@link #decodeUrl(byte[])}. 103 * </p> 104 * <p> 105 * Not every safe set permits a round trip. If both space and plus are marked safe, the encoder maps both to plus and this method preserves that plus. If 106 * percent is marked safe, literal percent signs cannot be distinguished from generated escapes, so this method preserves all percent signs, including 107 * generated escapes. Use a safe set that excludes percent and does not mark both space and plus safe when a round trip is required. 108 * </p> 109 * 110 * @param urlsafe bitset of characters deemed URL safe during encoding, or {@code null} to use the default safe set. 111 * @param bytes array of encoded bytes, or {@code null}. 112 * @return array of decoded bytes, or {@code null} if the input is {@code null}. 113 * @throws DecoderException if percent is not marked safe and an escape is incomplete or contains invalid hexadecimal digits. 114 * @since 1.23.0 115 */ 116 public static final byte[] decodeUrl(BitSet urlsafe, final byte[] bytes) throws DecoderException { 117 if (bytes == null) { 118 return null; 119 } 120 if (urlsafe == null) { 121 urlsafe = WWW_FORM_URL_SAFE; 122 } 123 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 124 for (int i = 0; i < bytes.length; i++) { 125 final int b = bytes[i]; 126 if (b == PLUS_CHAR && !urlsafe.get(PLUS_CHAR) && urlsafe.get(' ')) { 127 buffer.write(' '); 128 } else if (b == ESCAPE_CHAR && !urlsafe.get(ESCAPE_CHAR)) { 129 try { 130 final int u = Utils.digit16(bytes[++i]); 131 final int l = Utils.digit16(bytes[++i]); 132 buffer.write((char) ((u << 4) + l)); 133 } catch (final ArrayIndexOutOfBoundsException e) { 134 throw new DecoderException("Invalid URL encoding: ", e); 135 } 136 } else { 137 buffer.write(b); 138 } 139 } 140 return buffer.toByteArray(); 141 } 142 143 /** 144 * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted 145 * back to their original representation. 146 * 147 * <p> 148 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape. 149 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a 150 * {@link DecoderException}, depending on which characters were marked safe. 151 * </p> 152 * 153 * @param bytes 154 * array of URL safe characters. 155 * @return array of original bytes. 156 * @throws DecoderException 157 * Thrown if URL decoding is unsuccessful. 158 */ 159 public static final byte[] decodeUrl(final byte[] bytes) throws DecoderException { 160 return decodeUrl(null, bytes); 161 } 162 163 /** 164 * Encodes an array of bytes using the given set of URL safe characters. 165 * <p> 166 * Unsafe characters are percent-escaped. Characters marked safe are copied unchanged, except that a space marked safe is converted to {@code +}. A 167 * {@code null} bitset selects the default {@code www-form-urlencoded} safe set, which escapes both {@code %} and {@code +}. 168 * </p> 169 * <p> 170 * A custom bitset can produce output that {@link #decodeUrl(byte[])} and the {@code decode} methods cannot decode back to the original input. These 171 * decoders always convert {@code +} to a space and interpret {@code %} as the start of a hexadecimal escape, regardless of the bitset used for encoding. If 172 * the custom bitset marks either character safe, decoding can change the original data or throw {@link DecoderException}. Callers using a custom bitset 173 * can use {@link #decodeUrl(BitSet, byte[])} with the same bitset, subject to its documented limitations for ambiguous safe sets. 174 * </p> 175 * 176 * @param urlsafe bitset of characters deemed URL safe, or {@code null} to use the default {@code www-form-urlencoded} safe set. 177 * @param bytes array of bytes to convert to URL safe characters. 178 * @return array of bytes containing URL safe characters. 179 */ 180 public static final byte[] encodeUrl(BitSet urlsafe, final byte[] bytes) { 181 if (bytes == null) { 182 return null; 183 } 184 if (urlsafe == null) { 185 urlsafe = WWW_FORM_URL_SAFE; 186 } 187 188 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 189 for (final byte c : bytes) { 190 int b = c; 191 if (b < 0) { 192 b = 256 + b; 193 } 194 if (urlsafe.get(b)) { 195 if (b == ' ') { 196 b = PLUS_CHAR; 197 } 198 buffer.write(b); 199 } else { 200 buffer.write(ESCAPE_CHAR); 201 final char hex1 = Utils.hexChar(b >> 4); 202 final char hex2 = Utils.hexChar(b); 203 buffer.write(hex1); 204 buffer.write(hex2); 205 } 206 } 207 return buffer.toByteArray(); 208 } 209 210 /** 211 * The default charset used for string decoding and encoding. 212 * 213 * @deprecated TODO: This field will be changed to a private final Charset in 2.0. (CODEC-126) 214 */ 215 @Deprecated 216 protected volatile String charset; // added volatile: see CODEC-232 217 218 /** 219 * Default constructor. 220 */ 221 public URLCodec() { 222 this(CharEncoding.UTF_8); 223 } 224 225 /** 226 * Constructs a new instance for the selection of a default charset. 227 * 228 * @param charset The default string charset to use. 229 */ 230 public URLCodec(final String charset) { 231 this.charset = charset; 232 } 233 234 /** 235 * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted 236 * back to their original representation. 237 * 238 * <p> 239 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape. 240 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a 241 * {@link DecoderException}, depending on which characters were marked safe. 242 * </p> 243 * 244 * @param bytes 245 * array of URL safe characters. 246 * @return array of original bytes. 247 * @throws DecoderException 248 * Thrown if URL decoding is unsuccessful. 249 */ 250 @Override 251 public byte[] decode(final byte[] bytes) throws DecoderException { 252 return decodeUrl(bytes); 253 } 254 255 /** 256 * Decodes a URL safe object into its original form. Escaped characters are converted back to their original 257 * representation. 258 * 259 * <p> 260 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape. 261 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a 262 * {@link DecoderException}, depending on which characters were marked safe. 263 * </p> 264 * 265 * @param obj 266 * URL safe object to convert into its original form. 267 * @return original object. 268 * @throws DecoderException 269 * Thrown if the argument is not a {@code String} or {@code byte[]}. Thrown if a failure 270 * condition is encountered during the decode process. 271 */ 272 @Override 273 public Object decode(final Object obj) throws DecoderException { 274 if (obj == null) { 275 return null; 276 } 277 if (obj instanceof byte[]) { 278 return decode((byte[]) obj); 279 } 280 if (obj instanceof String) { 281 return decode((String) obj); 282 } 283 throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be URL decoded"); 284 } 285 286 /** 287 * Decodes a URL safe string into its original form using the default string charset. Escaped characters are 288 * converted back to their original representation. 289 * 290 * <p> 291 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape. 292 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a 293 * {@link DecoderException}, depending on which characters were marked safe. 294 * </p> 295 * 296 * @param str 297 * URL safe string to convert into its original form. 298 * @return original string. 299 * @throws DecoderException 300 * Thrown if URL decoding is unsuccessful. 301 * @see #getDefaultCharset() 302 */ 303 @Override 304 public String decode(final String str) throws DecoderException { 305 if (str == null) { 306 return null; 307 } 308 try { 309 return decode(str, getDefaultCharset()); 310 } catch (final UnsupportedEncodingException e) { 311 throw new DecoderException(e.getMessage(), e); 312 } 313 } 314 315 /** 316 * Decodes a URL safe string into its original form using the specified encoding. Escaped characters are converted 317 * back to their original representation. 318 * 319 * <p> 320 * Decoding always follows {@code www-form-urlencoded} rules: {@code +} becomes a space and {@code %} starts a hexadecimal escape. 321 * Output from {@link #encodeUrl(BitSet, byte[])} with a custom safe set may therefore not decode back to the original input and may cause a 322 * {@link DecoderException}, depending on which characters were marked safe. 323 * </p> 324 * 325 * @param str 326 * URL safe string to convert into its original form. 327 * @param charsetName 328 * the original string charset. 329 * @return original string. 330 * @throws DecoderException 331 * Thrown if URL decoding is unsuccessful. 332 * @throws UnsupportedEncodingException 333 * Thrown if charset is not supported. 334 */ 335 public String decode(final String str, final String charsetName) 336 throws DecoderException, UnsupportedEncodingException { 337 if (str == null) { 338 return null; 339 } 340 return new String(decode(StringUtils.getBytesUsAscii(str)), charsetName); 341 } 342 343 /** 344 * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped. 345 * 346 * @param bytes 347 * array of bytes to convert to URL safe characters. 348 * @return array of bytes containing URL safe characters. 349 */ 350 @Override 351 public byte[] encode(final byte[] bytes) { 352 return encodeUrl(WWW_FORM_URL_SAFE, bytes); 353 } 354 355 /** 356 * Encodes an object into its URL safe form. Unsafe characters are escaped. 357 * 358 * @param obj 359 * string to convert to a URL safe form. 360 * @return URL safe object. 361 * @throws EncoderException 362 * Thrown if URL encoding is not applicable to objects of this type or if encoding is unsuccessful. 363 */ 364 @Override 365 public Object encode(final Object obj) throws EncoderException { 366 if (obj == null) { 367 return null; 368 } 369 if (obj instanceof byte[]) { 370 return encode((byte[]) obj); 371 } 372 if (obj instanceof String) { 373 return encode((String) obj); 374 } 375 throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be URL encoded"); 376 } 377 378 /** 379 * Encodes a string into its URL safe form using the default string charset. Unsafe characters are escaped. 380 * 381 * @param str 382 * string to convert to a URL safe form. 383 * @return URL safe string. 384 * @throws EncoderException 385 * Thrown if URL encoding is unsuccessful. 386 * @see #getDefaultCharset() 387 */ 388 @Override 389 public String encode(final String str) throws EncoderException { 390 if (str == null) { 391 return null; 392 } 393 try { 394 return encode(str, getDefaultCharset()); 395 } catch (final UnsupportedEncodingException e) { 396 throw new EncoderException(e.getMessage(), e); 397 } 398 } 399 400 /** 401 * Encodes a string into its URL safe form using the specified string charset. Unsafe characters are escaped. 402 * 403 * @param str 404 * string to convert to a URL safe form. 405 * @param charsetName 406 * the charset for str. 407 * @return URL safe string. 408 * @throws UnsupportedEncodingException 409 * Thrown if charset is not supported. 410 */ 411 public String encode(final String str, final String charsetName) throws UnsupportedEncodingException { 412 if (str == null) { 413 return null; 414 } 415 return StringUtils.newStringUsAscii(encode(str.getBytes(charsetName))); 416 } 417 418 /** 419 * Gets the default charset used for string decoding and encoding. 420 * 421 * @return The default string charset. 422 */ 423 public String getDefaultCharset() { 424 return this.charset; 425 } 426 427 /** 428 * Gets the {@code String} encoding used for decoding and encoding. 429 * 430 * @return The encoding. 431 * @deprecated Use {@link #getDefaultCharset()}, will be removed in 2.0. 432 */ 433 @Deprecated 434 public String getEncoding() { 435 return this.charset; 436 } 437 438}