| // |
| // ======================================================================== |
| // Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.websocket.api.util; |
| |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.NoSuchElementException; |
| |
| /** |
| * Provide some consistent Http header value and Extension configuration parameter quoting support. |
| * <p> |
| * While QuotedStringTokenizer exists in jetty-util, and works great with http header values, using it in websocket-api is undesired. |
| * <ul> |
| * <li>Using QuotedStringTokenizer would introduce a dependency to jetty-util that would need to be exposed via the WebAppContext classloader</li> |
| * <li>ABNF defined extension parameter parsing requirements of RFC-6455 (WebSocket) ABNF, is slightly different than the ABNF parsing defined in RFC-2616 |
| * (HTTP/1.1).</li> |
| * <li>Future HTTPbis ABNF changes for parsing will impact QuotedStringTokenizer</li> |
| * </ul> |
| * It was decided to keep this implementation separate for the above reasons. |
| */ |
| public class QuoteUtil |
| { |
| private static class DeQuotingStringIterator implements Iterator<String> |
| { |
| private enum State |
| { |
| START, |
| TOKEN, |
| QUOTE_SINGLE, |
| QUOTE_DOUBLE |
| } |
| |
| private final String input; |
| private final String delims; |
| private StringBuilder token; |
| private boolean hasToken = false; |
| private int i = 0; |
| |
| public DeQuotingStringIterator(String input, String delims) |
| { |
| this.input = input; |
| this.delims = delims; |
| int len = input.length(); |
| token = new StringBuilder(len > 1024?512:len / 2); |
| } |
| |
| private void appendToken(char c) |
| { |
| if (hasToken) |
| { |
| token.append(c); |
| } |
| else |
| { |
| if (Character.isWhitespace(c)) |
| { |
| return; // skip whitespace at start of token. |
| } |
| else |
| { |
| token.append(c); |
| hasToken = true; |
| } |
| } |
| } |
| |
| @Override |
| public boolean hasNext() |
| { |
| // already found a token |
| if (hasToken) |
| { |
| return true; |
| } |
| |
| State state = State.START; |
| boolean escape = false; |
| int inputLen = input.length(); |
| |
| while (i < inputLen) |
| { |
| char c = input.charAt(i++); |
| |
| switch (state) |
| { |
| case START: |
| { |
| if (c == '\'') |
| { |
| state = State.QUOTE_SINGLE; |
| appendToken(c); |
| } |
| else if (c == '\"') |
| { |
| state = State.QUOTE_DOUBLE; |
| appendToken(c); |
| } |
| else |
| { |
| appendToken(c); |
| state = State.TOKEN; |
| } |
| break; |
| } |
| case TOKEN: |
| { |
| if (delims.indexOf(c) >= 0) |
| { |
| // System.out.printf("hasNext/t: %b [%s]%n",hasToken,token); |
| return hasToken; |
| } |
| else if (c == '\'') |
| { |
| state = State.QUOTE_SINGLE; |
| } |
| else if (c == '\"') |
| { |
| state = State.QUOTE_DOUBLE; |
| } |
| appendToken(c); |
| break; |
| } |
| case QUOTE_SINGLE: |
| { |
| if (escape) |
| { |
| escape = false; |
| appendToken(c); |
| } |
| else if (c == '\'') |
| { |
| appendToken(c); |
| state = State.TOKEN; |
| } |
| else if (c == '\\') |
| { |
| escape = true; |
| } |
| else |
| { |
| appendToken(c); |
| } |
| break; |
| } |
| case QUOTE_DOUBLE: |
| { |
| if (escape) |
| { |
| escape = false; |
| appendToken(c); |
| } |
| else if (c == '\"') |
| { |
| appendToken(c); |
| state = State.TOKEN; |
| } |
| else if (c == '\\') |
| { |
| escape = true; |
| } |
| else |
| { |
| appendToken(c); |
| } |
| break; |
| } |
| } |
| // System.out.printf("%s <%s> : [%s]%n",state,c,token); |
| } |
| // System.out.printf("hasNext/e: %b [%s]%n",hasToken,token); |
| return hasToken; |
| } |
| |
| @Override |
| public String next() |
| { |
| if (!hasNext()) |
| { |
| throw new NoSuchElementException(); |
| } |
| String ret = token.toString(); |
| token.setLength(0); |
| hasToken = false; |
| return QuoteUtil.dequote(ret.trim()); |
| } |
| |
| @Override |
| public void remove() |
| { |
| throw new UnsupportedOperationException("Remove not supported with this iterator"); |
| } |
| } |
| |
| /** |
| * ABNF from RFC 2616, RFC 822, and RFC 6455 specified characters requiring quoting. |
| */ |
| public static final String ABNF_REQUIRED_QUOTING = "\"'\\\n\r\t\f\b%+ ;="; |
| |
| private static final char UNICODE_TAG = 0xFFFF; |
| private static final char[] escapes = new char[32]; |
| |
| static |
| { |
| Arrays.fill(escapes,UNICODE_TAG); |
| // non-unicode |
| escapes['\b'] = 'b'; |
| escapes['\t'] = 't'; |
| escapes['\n'] = 'n'; |
| escapes['\f'] = 'f'; |
| escapes['\r'] = 'r'; |
| } |
| |
| private static int dehex(byte b) |
| { |
| if ((b >= '0') && (b <= '9')) |
| { |
| return (byte)(b - '0'); |
| } |
| if ((b >= 'a') && (b <= 'f')) |
| { |
| return (byte)((b - 'a') + 10); |
| } |
| if ((b >= 'A') && (b <= 'F')) |
| { |
| return (byte)((b - 'A') + 10); |
| } |
| throw new IllegalArgumentException("!hex:" + Integer.toHexString(0xff & b)); |
| } |
| |
| /** |
| * Remove quotes from a string, only if the input string start with and end with the same quote character. |
| * |
| * @param str |
| * the string to remove surrounding quotes from |
| * @return the de-quoted string |
| */ |
| public static String dequote(String str) |
| { |
| char start = str.charAt(0); |
| if ((start == '\'') || (start == '\"')) |
| { |
| // possibly quoted |
| char end = str.charAt(str.length() - 1); |
| if (start == end) |
| { |
| // dequote |
| return str.substring(1,str.length() - 1); |
| } |
| } |
| return str; |
| } |
| |
| public static void escape(StringBuilder buf, String str) |
| { |
| for (char c : str.toCharArray()) |
| { |
| if (c >= 32) |
| { |
| // non special character |
| if ((c == '"') || (c == '\\')) |
| { |
| buf.append('\\'); |
| } |
| buf.append(c); |
| } |
| else |
| { |
| // special characters, requiring escaping |
| char escaped = escapes[c]; |
| |
| // is this a unicode escape? |
| if (escaped == UNICODE_TAG) |
| { |
| buf.append("\\u00"); |
| if (c < 0x10) |
| { |
| buf.append('0'); |
| } |
| buf.append(Integer.toString(c,16)); // hex |
| } |
| else |
| { |
| // normal escape |
| buf.append('\\').append(escaped); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Simple quote of a string, escaping where needed. |
| * |
| * @param buf |
| * the StringBuilder to append to |
| * @param str |
| * the string to quote |
| */ |
| public static void quote(StringBuilder buf, String str) |
| { |
| buf.append('"'); |
| escape(buf,str); |
| buf.append('"'); |
| } |
| |
| /** |
| * Append into buf the provided string, adding quotes if needed. |
| * <p> |
| * Quoting is determined if any of the characters in the <code>delim</code> are found in the input <code>str</code>. |
| * |
| * @param buf |
| * the buffer to append to |
| * @param str |
| * the string to possibly quote |
| * @param delim |
| * the delimiter characters that will trigger automatic quoting |
| */ |
| public static void quoteIfNeeded(StringBuilder buf, String str, String delim) |
| { |
| if (str == null) |
| { |
| return; |
| } |
| // check for delimiters in input string |
| int len = str.length(); |
| if (len == 0) |
| { |
| return; |
| } |
| int ch; |
| for (int i = 0; i < len; i++) |
| { |
| ch = str.codePointAt(i); |
| if (delim.indexOf(ch) >= 0) |
| { |
| // found a delimiter codepoint. we need to quote it. |
| quote(buf,str); |
| return; |
| } |
| } |
| |
| // no special delimiters used, no quote needed. |
| buf.append(str); |
| } |
| |
| /** |
| * Create an iterator of the input string, breaking apart the string at the provided delimiters, removing quotes and triming the parts of the string as |
| * needed. |
| * |
| * @param str |
| * the input string to split apart |
| * @param delims |
| * the delimiter characters to split the string on |
| * @return the iterator of the parts of the string, trimmed, with quotes around the string part removed, and unescaped |
| */ |
| public static Iterator<String> splitAt(String str, String delims) |
| { |
| return new DeQuotingStringIterator(str.trim(),delims); |
| } |
| |
| public static String unescape(String str) |
| { |
| if (str == null) |
| { |
| // nothing there |
| return null; |
| } |
| |
| int len = str.length(); |
| if (len <= 1) |
| { |
| // impossible to be escaped |
| return str; |
| } |
| |
| StringBuilder ret = new StringBuilder(len - 2); |
| boolean escaped = false; |
| char c; |
| for (int i = 0; i < len; i++) |
| { |
| c = str.charAt(i); |
| if (escaped) |
| { |
| escaped = false; |
| switch (c) |
| { |
| case 'n': |
| ret.append('\n'); |
| break; |
| case 'r': |
| ret.append('\r'); |
| break; |
| case 't': |
| ret.append('\t'); |
| break; |
| case 'f': |
| ret.append('\f'); |
| break; |
| case 'b': |
| ret.append('\b'); |
| break; |
| case '\\': |
| ret.append('\\'); |
| break; |
| case '/': |
| ret.append('/'); |
| break; |
| case '"': |
| ret.append('"'); |
| break; |
| case 'u': |
| ret.append((char)((dehex((byte)str.charAt(i++)) << 24) + (dehex((byte)str.charAt(i++)) << 16) + (dehex((byte)str.charAt(i++)) << 8) + (dehex((byte)str |
| .charAt(i++))))); |
| break; |
| default: |
| ret.append(c); |
| } |
| } |
| else if (c == '\\') |
| { |
| escaped = true; |
| } |
| else |
| { |
| ret.append(c); |
| } |
| } |
| return ret.toString(); |
| } |
| |
| public static String join(Object[] objs, String delim) |
| { |
| if (objs == null) |
| { |
| return ""; |
| } |
| StringBuilder ret = new StringBuilder(); |
| int len = objs.length; |
| for (int i = 0; i < len; i++) |
| { |
| if (i > 0) |
| { |
| ret.append(delim); |
| } |
| if (objs[i] instanceof String) |
| { |
| ret.append('"').append(objs[i]).append('"'); |
| } |
| else |
| { |
| ret.append(objs[i]); |
| } |
| } |
| return ret.toString(); |
| } |
| |
| public static String join(Collection<?> objs, String delim) |
| { |
| if (objs == null) |
| { |
| return ""; |
| } |
| StringBuilder ret = new StringBuilder(); |
| boolean needDelim = false; |
| for (Object obj : objs) |
| { |
| if (needDelim) |
| { |
| ret.append(delim); |
| } |
| if (obj instanceof String) |
| { |
| ret.append('"').append(obj).append('"'); |
| } |
| else |
| { |
| ret.append(obj); |
| } |
| needDelim = true; |
| } |
| return ret.toString(); |
| } |
| } |