/*
 * Copyright (c) 2010, 2017 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.http.util;

import java.io.CharConversionException;
import java.io.UnsupportedEncodingException;
import org.glassfish.grizzly.Buffer;

Author:Alexey Stashok
/** * * @author Alexey Stashok */
public class URLDecoder { public static void decode(final DataChunk dataChunk) throws CharConversionException { decode(dataChunk, true); }
URLDecode the DataChunk
/** * URLDecode the {@link DataChunk} */
public static void decode(final DataChunk dataChunk, final boolean allowEncodedSlash) throws CharConversionException { decodeAscii(dataChunk, dataChunk, allowEncodedSlash); }
URLDecode the DataChunk
/** * URLDecode the {@link DataChunk} */
public static void decode(final DataChunk srcDataChunk, final DataChunk dstDataChunk, final boolean allowEncodedSlash) throws CharConversionException { decodeAscii(srcDataChunk, dstDataChunk, allowEncodedSlash); }
URLDecode the DataChunk
/** * URLDecode the {@link DataChunk} */
public static void decode(final DataChunk srcDataChunk, final DataChunk dstDataChunk, final boolean allowEncodedSlash, final String enc) throws CharConversionException { switch (srcDataChunk.getType()) { case Bytes: { final ByteChunk srcByteChunk = srcDataChunk.getByteChunk(); final ByteChunk dstByteChunk = dstDataChunk.getByteChunk(); // If it's the same buffer - we don't want to call allocate, // because it resets the ByteChunk length if (dstByteChunk != srcByteChunk) { dstByteChunk.allocate(srcByteChunk.getLength(), -1); } decode(srcByteChunk, dstByteChunk, allowEncodedSlash); return; } case Buffer: { final BufferChunk srcBufferChunk = srcDataChunk.getBufferChunk(); // If it's the same buffer - we don't want to call allocate, // because it resets the ByteChunk length if (dstDataChunk != srcDataChunk) { final ByteChunk dstByteChunk = dstDataChunk.getByteChunk(); dstByteChunk.allocate(srcBufferChunk.getLength(), -1); decode(srcBufferChunk, dstByteChunk, allowEncodedSlash); } else { decode(srcBufferChunk, srcBufferChunk, allowEncodedSlash); } return; } case String: { dstDataChunk.setString( decode(srcDataChunk.toString(), allowEncodedSlash, enc)); return; } case Chars: { final CharChunk srcCharChunk = srcDataChunk.getCharChunk(); final CharChunk dstCharChunk = dstDataChunk.getCharChunk(); // If it's the same buffer - we don't want to call allocate, // because it resets the ByteChunk length if (dstDataChunk != srcDataChunk) { dstCharChunk.ensureCapacity(srcCharChunk.getLength()); decode(srcCharChunk, dstCharChunk, allowEncodedSlash, enc); } else { decode(srcCharChunk, srcCharChunk, allowEncodedSlash, enc); } dstDataChunk.setChars(dstCharChunk.getChars(), dstCharChunk.getStart(), dstCharChunk.getEnd()); return; } default: throw new NullPointerException(); } }
URLDecode the DataChunk
/** * URLDecode the {@link DataChunk} */
public static void decodeAscii(final DataChunk srcDataChunk, final DataChunk dstDataChunk, final boolean allowEncodedSlash) throws CharConversionException { switch (srcDataChunk.getType()) { case Bytes: { final ByteChunk srcByteChunk = srcDataChunk.getByteChunk(); final ByteChunk dstByteChunk = dstDataChunk.getByteChunk(); // If it's the same buffer - we don't want to call allocate, // because it resets the ByteChunk length if (dstByteChunk != srcByteChunk) { dstByteChunk.allocate(srcByteChunk.getLength(), -1); } decode(srcByteChunk, dstByteChunk, allowEncodedSlash); return; } case Buffer: { final BufferChunk srcBufferChunk = srcDataChunk.getBufferChunk(); // If it's the same buffer - we don't want to call allocate, // because it resets the ByteChunk length if (dstDataChunk != srcDataChunk) { final ByteChunk dstByteChunk = dstDataChunk.getByteChunk(); dstByteChunk.allocate(srcBufferChunk.getLength(), -1); decode(srcBufferChunk, dstByteChunk, allowEncodedSlash); } else { decode(srcBufferChunk, srcBufferChunk, allowEncodedSlash); } return; } case String: { dstDataChunk.setString( decodeAscii(srcDataChunk.toString(), allowEncodedSlash)); return; } case Chars: { final CharChunk srcCharChunk = srcDataChunk.getCharChunk(); final CharChunk dstCharChunk = dstDataChunk.getCharChunk(); // If it's the same buffer - we don't want to call allocate, // because it resets the ByteChunk length if (dstDataChunk != srcDataChunk) { dstCharChunk.ensureCapacity(srcCharChunk.getLength()); decodeAscii(srcCharChunk, dstCharChunk, allowEncodedSlash); } else { decodeAscii(srcCharChunk, srcCharChunk, allowEncodedSlash); } dstDataChunk.setChars(dstCharChunk.getChars(), dstCharChunk.getStart(), dstCharChunk.getEnd()); return; } default: throw new NullPointerException(); } }
URLDecode the ByteChunk
/** * URLDecode the {@link ByteChunk} */
public static void decode(final ByteChunk byteChunk, final boolean allowEncodedSlash) throws CharConversionException { decode(byteChunk, byteChunk, allowEncodedSlash); }
URLDecode the ByteChunk
/** * URLDecode the {@link ByteChunk} */
public static void decode(final ByteChunk srcByteChunk, final ByteChunk dstByteChunk, final boolean allowEncodedSlash) throws CharConversionException { final byte[] srcBuffer = srcByteChunk.getBuffer(); final int srcStart = srcByteChunk.getStart(); final int srcEnd = srcByteChunk.getEnd(); final byte[] dstBuffer = dstByteChunk.getBuffer(); int idx = dstByteChunk.getStart(); for (int j = srcStart; j < srcEnd; j++, idx++) { final byte b = srcBuffer[j]; if (b == '+') { dstBuffer[idx] = (byte) ' '; } else if (b != '%') { dstBuffer[idx] = b; } else { // read next 2 digits if (j + 2 >= srcEnd) { throw new IllegalStateException("Unexpected termination"); } byte b1 = srcBuffer[j + 1]; byte b2 = srcBuffer[j + 2]; if (!HexUtils.isHexDigit(b1) || !HexUtils.isHexDigit(b2)) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - %" + (char) b1 + "" + (char) b2); } j += 2; final int res = x2c(b1, b2); if (!allowEncodedSlash && (res == '/')) { throw new CharConversionException("Encoded slashes are not allowed"); } dstBuffer[idx] = (byte) res; } } dstByteChunk.setEnd(idx); }
URLDecode the BufferChunk
/** * URLDecode the {@link BufferChunk} */
public static void decode(final BufferChunk srcBufferChunk, final ByteChunk dstByteChunk, final boolean allowEncodedSlash) throws CharConversionException { final Buffer srcBuffer = srcBufferChunk.getBuffer(); final int srcStart = srcBufferChunk.getStart(); final int srcEnd = srcBufferChunk.getEnd(); final byte[] dstBuffer = dstByteChunk.getBuffer(); int idx = dstByteChunk.getStart(); for (int j = srcStart; j < srcEnd; j++, idx++) { final byte b = srcBuffer.get(j); if (b == '+') { dstBuffer[idx] = (byte) ' '; } else if (b != '%') { dstBuffer[idx] = b; } else { // read next 2 digits if (j + 2 >= srcEnd) { throw new IllegalStateException("Unexpected termination"); } byte b1 = srcBuffer.get(j + 1); byte b2 = srcBuffer.get(j + 2); if (!HexUtils.isHexDigit(b1) || !HexUtils.isHexDigit(b2)) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - %" + (char) b1 + "" + (char) b2); } j += 2; final int res = x2c(b1, b2); if (!allowEncodedSlash && (res == '/')) { throw new CharConversionException("Encoded slashes are not allowed"); } dstBuffer[idx] = (byte) res; } } dstByteChunk.setEnd(idx); }
URLDecode the ByteChunk
/** * URLDecode the {@link ByteChunk} */
public static void decode(final ByteChunk srcByteChunk, final BufferChunk dstBufferChunk, final boolean allowEncodedSlash) throws CharConversionException { final byte[] srcBuffer = srcByteChunk.getBuffer(); final int srcStart = srcByteChunk.getStart(); final int srcEnd = srcByteChunk.getEnd(); final Buffer dstBuffer = dstBufferChunk.getBuffer(); int idx = dstBufferChunk.getStart(); for (int j = srcStart; j < srcEnd; j++, idx++) { final byte b = srcBuffer[j]; if (b == '+') { dstBuffer.put(idx , (byte) ' '); } else if (b != '%') { dstBuffer.put(idx, b); } else { // read next 2 digits if (j + 2 >= srcEnd) { throw new IllegalStateException("Unexpected termination"); } byte b1 = srcBuffer[j + 1]; byte b2 = srcBuffer[j + 2]; if (!HexUtils.isHexDigit(b1) || !HexUtils.isHexDigit(b2)) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - %" + (char) b1 + "" + (char) b2); } j += 2; final int res = x2c(b1, b2); if (!allowEncodedSlash && (res == '/')) { throw new CharConversionException("Encoded slashes are not allowed"); } dstBuffer.put(idx, (byte) res); } } dstBufferChunk.setEnd(idx); }
URLDecode the BufferChunk
/** * URLDecode the {@link BufferChunk} */
public static void decode(final BufferChunk bufferChunk, final boolean allowEncodedSlash) throws CharConversionException { decode(bufferChunk, bufferChunk, allowEncodedSlash); }
URLDecode the BufferChunk
/** * URLDecode the {@link BufferChunk} */
public static void decode(final BufferChunk srcBufferChunk, final BufferChunk dstBufferChunk, final boolean allowEncodedSlash) throws CharConversionException { final Buffer srcBuffer = srcBufferChunk.getBuffer(); final int srcStart = srcBufferChunk.getStart(); final int srcEnd = srcBufferChunk.getEnd(); final Buffer dstBuffer = dstBufferChunk.getBuffer(); int idx = dstBufferChunk.getStart(); for (int j = srcStart; j < srcEnd; j++, idx++) { final byte b = srcBuffer.get(j); if (b == '+') { dstBuffer.put(idx , (byte) ' '); } else if (b != '%') { dstBuffer.put(idx, b); } else { // read next 2 digits if (j + 2 >= srcEnd) { throw new IllegalStateException("Unexpected termination"); } byte b1 = srcBuffer.get(j + 1); byte b2 = srcBuffer.get(j + 2); if (!HexUtils.isHexDigit(b1) || !HexUtils.isHexDigit(b2)) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - %" + (char) b1 + "" + (char) b2); } j += 2; final int res = x2c(b1, b2); if (!allowEncodedSlash && (res == '/')) { throw new CharConversionException("Encoded slashes are not allowed"); } dstBuffer.put(idx, (byte) res); } } dstBufferChunk.setEnd(idx); }
URLDecode the CharChunk
/** * URLDecode the {@link CharChunk} */
public static void decode(final CharChunk charChunk, final boolean allowEncodedSlash) throws CharConversionException { decodeAscii(charChunk, charChunk, allowEncodedSlash); }
URLDecode the CharChunk
/** * URLDecode the {@link CharChunk} */
public static void decode(final CharChunk srcCharChunk, final CharChunk dstCharChunk, final boolean allowEncodedSlash) throws CharConversionException { decodeAscii(srcCharChunk, dstCharChunk, allowEncodedSlash); }
URLDecode the CharChunk
/** * URLDecode the {@link CharChunk} */
public static void decode(final CharChunk srcCharChunk, final CharChunk dstCharChunk, final boolean allowEncodedSlash, String enc) throws CharConversionException { byte[] bytes = null; final char[] srcBuffer = srcCharChunk.getBuffer(); final int srcStart = srcCharChunk.getStart(); final int srcEnd = srcCharChunk.getEnd(); final int srcLen = srcEnd - srcStart; final char[] dstBuffer = dstCharChunk.getBuffer(); int idx = dstCharChunk.getStart(); int j = srcStart; while(j < srcEnd) { char c = srcBuffer[j]; if (c == '+') { dstBuffer[idx++] = ' '; j++; } else if (c != '%') { dstBuffer[idx++] = c; j++; } else { /* * Starting with this instance of %, process all * consecutive substrings of the form %xy. Each * substring %xy will yield a byte. Convert all * consecutive bytes obtained this way to whatever * character(s) they represent in the provided * encoding. */ try { // (numChars-i)/3 is an upper bound for the number // of remaining bytes if (bytes == null) bytes = new byte[(srcLen-j)/3]; int pos = 0; while (((j + 2) < srcLen) && (c == '%')) { final char c1 = srcBuffer[j + 1]; final char c2 = srcBuffer[j + 2]; if (!HexUtils.isHexDigit(c1) || !HexUtils.isHexDigit(c2)) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - %" + c1 + "" + c2); } int v = x2c(c1, c2); if (v < 0) { throw new IllegalArgumentException("URLDecoder: Illegal hex characters in escape (%) pattern - negative value"); } bytes[pos++] = (byte) v; j += 3; if (j < srcLen) { c = srcBuffer[j]; } } // A trailing, incomplete byte encoding such as // "%x" will cause an exception to be thrown if ((j < srcLen) && (c == '%')) { throw new IllegalArgumentException( "URLDecoder: Incomplete trailing escape (%) pattern"); } final String decodedChunk = new String(bytes, 0, pos, enc); if (!allowEncodedSlash && (decodedChunk.indexOf('/') != -1)) { throw new CharConversionException("Encoded slashes are not allowed"); } final int chunkLen = decodedChunk.length(); decodedChunk.getChars(0, chunkLen, dstBuffer, idx); idx += chunkLen; } catch (NumberFormatException e) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - " + e.getMessage()); } catch (UnsupportedEncodingException ignored) { } } } dstCharChunk.setEnd(idx); }
URLDecode the CharChunk
/** * URLDecode the {@link CharChunk} */
public static void decodeAscii(final CharChunk srcCharChunk, final CharChunk dstCharChunk, final boolean allowEncodedSlash) throws CharConversionException { final char[] srcBuffer = srcCharChunk.getBuffer(); final int srcStart = srcCharChunk.getStart(); final int srcEnd = srcCharChunk.getEnd(); final char[] dstBuffer = dstCharChunk.getBuffer(); int idx = dstCharChunk.getStart(); for (int j = srcStart; j < srcEnd; j++, idx++) { final char c = srcBuffer[j]; if (c == '+') { dstBuffer[idx] = ' '; } else if (c != '%') { dstBuffer[idx] = c; } else { // read next 2 digits if (j + 2 >= srcEnd) { throw new IllegalStateException("Unexpected termination"); } final char c1 = srcBuffer[j + 1]; final char c2 = srcBuffer[j + 2]; if (!HexUtils.isHexDigit(c1) || !HexUtils.isHexDigit(c2)) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - %" + c1 + "" + c2); } j += 2; final int res = x2c(c1, c2); if (!allowEncodedSlash && (res == '/')) { throw new CharConversionException("Encoded slashes are not allowed"); } dstBuffer[idx] = (char) res; } } dstCharChunk.setEnd(idx); }
URLDecode the String
/** * URLDecode the {@link String} */
public static String decode(final String str) throws CharConversionException { return decodeAscii(str, true); }
URLDecode the String
/** * URLDecode the {@link String} */
public static String decode(final String str, final boolean allowEncodedSlash) throws CharConversionException { return decodeAscii(str, allowEncodedSlash); }
URLDecode the String
/** * URLDecode the {@link String} */
public static String decode(String s, final boolean allowEncodedSlash, String enc) throws CharConversionException { boolean needToChange = false; int numChars = s.length(); StringBuilder sb = new StringBuilder(numChars > 500 ? numChars / 2 : numChars); int i = 0; char c; byte[] bytes = null; while (i < numChars) { c = s.charAt(i); switch (c) { case '+': sb.append(' '); i++; needToChange = true; break; case '%': /* * Starting with this instance of %, process all * consecutive substrings of the form %xy. Each * substring %xy will yield a byte. Convert all * consecutive bytes obtained this way to whatever * character(s) they represent in the provided * encoding. */ try { // (numChars-i)/3 is an upper bound for the number // of remaining bytes if (bytes == null) bytes = new byte[(numChars-i)/3]; int pos = 0; while (((i + 2) < numChars) && (c == '%')) { int v = Integer.parseInt(s.substring(i + 1, i + 3), 16); if (v < 0) { throw new IllegalArgumentException("URLDecoder: Illegal hex characters in escape (%) pattern - negative value"); } bytes[pos++] = (byte) v; i += 3; if (i < numChars) { c = s.charAt(i); } } // A trailing, incomplete byte encoding such as // "%x" will cause an exception to be thrown if ((i < numChars) && (c == '%')) { throw new IllegalArgumentException( "URLDecoder: Incomplete trailing escape (%) pattern"); } final String decodedChunk = new String(bytes, 0, pos, enc); if (!allowEncodedSlash && (decodedChunk.indexOf('/') != -1)) { throw new CharConversionException("Encoded slashes are not allowed"); } sb.append(decodedChunk); } catch (NumberFormatException e) { throw new IllegalArgumentException( "URLDecoder: Illegal hex characters in escape (%) pattern - " + e.getMessage()); } catch (UnsupportedEncodingException ignored) { } needToChange = true; break; default: sb.append(c); i++; break; } } return (needToChange? sb.toString() : s); } public static String decodeAscii(final String str, final boolean allowEncodedSlash) throws CharConversionException { if (str == null) { return null; } int mPos = 0; // mark position int strPos = 0; int strLen = str.length(); StringBuilder dec = null; // decoded string output while (strPos < strLen) { // process next metacharacter final char metaChar = str.charAt(strPos); final boolean isPlus = (metaChar == '+'); final boolean isNorm = !(isPlus || (metaChar == '%')); if (isNorm) { strPos++; } else { if (dec == null) { dec = new StringBuilder(strLen); } // if there were non-metacharacters, copy them all as a block if (mPos < strPos) { dec.append(str, mPos, strPos); } if (isPlus) { dec.append(' '); strPos++; } else { final char res = (char) Integer.parseInt(str.substring(strPos + 1, strPos + 3), 16); if (!allowEncodedSlash && (res == '/')) { throw new CharConversionException("Encoded slashes are not allowed"); } dec.append(res); strPos += 3; } mPos = strPos; } } if (dec != null) { // copy the normal characters remainder (if any) if (mPos < strPos) { dec.append(str, mPos, strPos); } return dec.toString(); } // all characters were normal return str; } private static int x2c(byte b1, byte b2) { return (HexUtils.hexDigit2Dec(b1) << 4) + HexUtils.hexDigit2Dec(b2); } private static int x2c(int c1, int c2) { return (HexUtils.hexDigit2Dec(c1) << 4) + HexUtils.hexDigit2Dec(c2); } }