/* * Copyright 2012 The Netty Project * * The Netty Project licenses this file to you 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 io.netty.handler.codec.http; import io.netty.util.CharsetUtil; import java.net.URI; import java.net.URLDecoder; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static io.netty.util.internal.ObjectUtil.*; import static io.netty.util.internal.StringUtil.*; /** * Splits an HTTP query string into a path string and key-value parameter pairs. * This decoder is for one time use only. Create a new instance for each URI: *
* {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("/hello?recipient=world&x=1;y=2"); * assert decoder.path().equals("/hello"); * assert decoder.parameters().get("recipient").get(0).equals("world"); * assert decoder.parameters().get("x").get(0).equals("1"); * assert decoder.parameters().get("y").get(0).equals("2"); ** * This decoder can also decode the content of an HTTP POST request whose * content type is application/x-www-form-urlencoded: *
* {@link QueryStringDecoder} decoder = new {@link QueryStringDecoder}("recipient=world&x=1;y=2", false); * ... ** *
* This is equivalent to calling {@link #decodeComponent(String, Charset)} * with the UTF-8 charset (recommended to comply with RFC 3986, Section 2). * @param s The string to decode (can be empty). * @return The decoded string, or {@code s} if there's nothing to decode. * If the string to decode is {@code null}, returns an empty string. * @throws IllegalArgumentException if the string contains a malformed * escape sequence. */ public static String decodeComponent(final String s) { return decodeComponent(s, HttpConstants.DEFAULT_CHARSET); } /** * Decodes a bit of an URL encoded by a browser. *
* The string is expected to be encoded as per RFC 3986, Section 2. * This is the encoding used by JavaScript functions {@code encodeURI} * and {@code encodeURIComponent}, but not {@code escape}. For example * in this encoding, é (in Unicode {@code U+00E9} or in UTF-8 * {@code 0xC3 0xA9}) is encoded as {@code %C3%A9} or {@code %c3%a9}. *
* This is essentially equivalent to calling * {@link URLDecoder#decode(String, String)} * except that it's over 2x faster and generates less garbage for the GC. * Actually this function doesn't allocate any memory if there's nothing * to decode, the argument itself is returned. * @param s The string to decode (can be empty). * @param charset The charset to use to decode the string (should really * be {@link CharsetUtil#UTF_8}. * @return The decoded string, or {@code s} if there's nothing to decode. * If the string to decode is {@code null}, returns an empty string. * @throws IllegalArgumentException if the string contains a malformed * escape sequence. */ public static String decodeComponent(final String s, final Charset charset) { if (s == null) { return EMPTY_STRING; } return decodeComponent(s, 0, s.length(), charset, false); } private static String decodeComponent(String s, int from, int toExcluded, Charset charset, boolean isPath) { int len = toExcluded - from; if (len <= 0) { return EMPTY_STRING; } int firstEscaped = -1; for (int i = from; i < toExcluded; i++) { char c = s.charAt(i); if (c == '%' || c == '+' && !isPath) { firstEscaped = i; break; } } if (firstEscaped == -1) { return s.substring(from, toExcluded); } CharsetDecoder decoder = CharsetUtil.decoder(charset); // Each encoded byte takes 3 characters (e.g. "%20") int decodedCapacity = (toExcluded - firstEscaped) / 3; ByteBuffer byteBuf = ByteBuffer.allocate(decodedCapacity); CharBuffer charBuf = CharBuffer.allocate(decodedCapacity); StringBuilder strBuf = new StringBuilder(len); strBuf.append(s, from, firstEscaped); for (int i = firstEscaped; i < toExcluded; i++) { char c = s.charAt(i); if (c != '%') { strBuf.append(c != '+' || isPath? c : SPACE); continue; } byteBuf.clear(); do { if (i + 3 > toExcluded) { throw new IllegalArgumentException("unterminated escape sequence at index " + i + " of: " + s); } byteBuf.put(decodeHexByte(s, i + 1)); i += 3; } while (i < toExcluded && s.charAt(i) == '%'); i--; byteBuf.flip(); charBuf.clear(); CoderResult result = decoder.reset().decode(byteBuf, charBuf, true); try { if (!result.isUnderflow()) { result.throwException(); } result = decoder.flush(charBuf); if (!result.isUnderflow()) { result.throwException(); } } catch (CharacterCodingException ex) { throw new IllegalStateException(ex); } strBuf.append(charBuf.flip()); } return strBuf.toString(); } private static int findPathEndIndex(String uri) { int len = uri.length(); for (int i = 0; i < len; i++) { char c = uri.charAt(i); if (c == '?' || c == '#') { return i; } } return len; } }