ParameterParser.java

  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. package org.apache.commons.fileupload2.core;

  18. import java.io.UnsupportedEncodingException;
  19. import java.util.HashMap;
  20. import java.util.Locale;
  21. import java.util.Map;

  22. /**
  23.  * A simple parser intended to parse sequences of name/value pairs.
  24.  * <p>
  25.  * Parameter values are expected to be enclosed in quotes if they contain unsafe characters, such as '=' characters or separators. Parameter values are optional
  26.  * and can be omitted.
  27.  * </p>
  28.  * <p>
  29.  * {@code param1 = value; param2 = "anything goes; really"; param3}
  30.  * </p>
  31.  */
  32. public class ParameterParser {

  33.     /**
  34.      * String to be parsed.
  35.      */
  36.     private char[] chars;

  37.     /**
  38.      * Current position in the string.
  39.      */
  40.     private int pos;

  41.     /**
  42.      * Maximum position in the string.
  43.      */
  44.     private int len;

  45.     /**
  46.      * Start of a token.
  47.      */
  48.     private int i1;

  49.     /**
  50.      * End of a token.
  51.      */
  52.     private int i2;

  53.     /**
  54.      * Whether names stored in the map should be converted to lower case.
  55.      */
  56.     private boolean lowerCaseNames;

  57.     /**
  58.      * Default ParameterParser constructor.
  59.      */
  60.     public ParameterParser() {
  61.     }

  62.     /**
  63.      * A helper method to process the parsed token. This method removes leading and trailing blanks as well as enclosing quotation marks, when necessary.
  64.      *
  65.      * @param quoted {@code true} if quotation marks are expected, {@code false} otherwise.
  66.      * @return the token
  67.      */
  68.     private String getToken(final boolean quoted) {
  69.         // Trim leading white spaces
  70.         while (i1 < i2 && Character.isWhitespace(chars[i1])) {
  71.             i1++;
  72.         }
  73.         // Trim trailing white spaces
  74.         while (i2 > i1 && Character.isWhitespace(chars[i2 - 1])) {
  75.             i2--;
  76.         }
  77.         // Strip away quotation marks if necessary
  78.         if (quoted && i2 - i1 >= 2 && chars[i1] == '"' && chars[i2 - 1] == '"') {
  79.             i1++;
  80.             i2--;
  81.         }
  82.         String result = null;
  83.         if (i2 > i1) {
  84.             result = new String(chars, i1, i2 - i1);
  85.         }
  86.         return result;
  87.     }

  88.     /**
  89.      * Tests if there any characters left to parse.
  90.      *
  91.      * @return {@code true} if there are unparsed characters, {@code false} otherwise.
  92.      */
  93.     private boolean hasChar() {
  94.         return this.pos < this.len;
  95.     }

  96.     /**
  97.      * Tests {@code true} if parameter names are to be converted to lower case when name/value pairs are parsed.
  98.      *
  99.      * @return {@code true} if parameter names are to be converted to lower case when name/value pairs are parsed. Otherwise returns {@code false}
  100.      */
  101.     public boolean isLowerCaseNames() {
  102.         return this.lowerCaseNames;
  103.     }

  104.     /**
  105.      * Tests if the given character is present in the array of characters.
  106.      *
  107.      * @param ch      the character to test for presence in the array of characters
  108.      * @param charray the array of characters to test against
  109.      * @return {@code true} if the character is present in the array of characters, {@code false} otherwise.
  110.      */
  111.     private boolean isOneOf(final char ch, final char[] charray) {
  112.         var result = false;
  113.         for (final char element : charray) {
  114.             if (ch == element) {
  115.                 result = true;
  116.                 break;
  117.             }
  118.         }
  119.         return result;
  120.     }

  121.     /**
  122.      * Parses a map of name/value pairs from the given array of characters. Names are expected to be unique.
  123.      *
  124.      * @param charArray the array of characters that contains a sequence of name/value pairs
  125.      * @param separator the name/value pairs separator
  126.      * @return a map of name/value pairs
  127.      */
  128.     public Map<String, String> parse(final char[] charArray, final char separator) {
  129.         if (charArray == null) {
  130.             return new HashMap<>();
  131.         }
  132.         return parse(charArray, 0, charArray.length, separator);
  133.     }

  134.     /**
  135.      * Parses a map of name/value pairs from the given array of characters. Names are expected to be unique.
  136.      *
  137.      * @param charArray the array of characters that contains a sequence of name/value pairs
  138.      * @param offset      the initial offset.
  139.      * @param length      the length.
  140.      * @param separator the name/value pairs separator
  141.      * @return a map of name/value pairs
  142.      */
  143.     public Map<String, String> parse(final char[] charArray, final int offset, final int length, final char separator) {

  144.         if (charArray == null) {
  145.             return new HashMap<>();
  146.         }
  147.         final var params = new HashMap<String, String>();
  148.         this.chars = charArray.clone();
  149.         this.pos = offset;
  150.         this.len = length;

  151.         String paramName;
  152.         String paramValue;
  153.         while (hasChar()) {
  154.             paramName = parseToken(new char[] { '=', separator });
  155.             paramValue = null;
  156.             if (hasChar() && charArray[pos] == '=') {
  157.                 pos++; // skip '='
  158.                 paramValue = parseQuotedToken(new char[] { separator });

  159.                 if (paramValue != null) {
  160.                     try {
  161.                         paramValue = RFC2231Utils.hasEncodedValue(paramName) ? RFC2231Utils.decodeText(paramValue) : MimeUtils.decodeText(paramValue);
  162.                     } catch (final UnsupportedEncodingException ignored) {
  163.                         // let's keep the original value in this case
  164.                     }
  165.                 }
  166.             }
  167.             if (hasChar() && charArray[pos] == separator) {
  168.                 pos++; // skip separator
  169.             }
  170.             if (paramName != null && !paramName.isEmpty()) {
  171.                 paramName = RFC2231Utils.stripDelimiter(paramName);
  172.                 if (this.lowerCaseNames) {
  173.                     paramName = paramName.toLowerCase(Locale.ROOT);
  174.                 }
  175.                 params.put(paramName, paramValue);
  176.             }
  177.         }
  178.         return params;
  179.     }

  180.     /**
  181.      * Parses a map of name/value pairs from the given string. Names are expected to be unique.
  182.      *
  183.      * @param str       the string that contains a sequence of name/value pairs
  184.      * @param separator the name/value pairs separator
  185.      * @return a map of name/value pairs
  186.      */
  187.     public Map<String, String> parse(final String str, final char separator) {
  188.         if (str == null) {
  189.             return new HashMap<>();
  190.         }
  191.         return parse(str.toCharArray(), separator);
  192.     }

  193.     /**
  194.      * Parses a map of name/value pairs from the given string. Names are expected to be unique. Multiple separators may be specified and the earliest found in
  195.      * the input string is used.
  196.      *
  197.      * @param str        the string that contains a sequence of name/value pairs
  198.      * @param separators the name/value pairs separators
  199.      * @return a map of name/value pairs
  200.      */
  201.     public Map<String, String> parse(final String str, final char[] separators) {
  202.         if (separators == null || separators.length == 0) {
  203.             return new HashMap<>();
  204.         }
  205.         var separator = separators[0];
  206.         if (str != null) {
  207.             var idx = str.length();
  208.             for (final char separator2 : separators) {
  209.                 final var tmp = str.indexOf(separator2);
  210.                 if (tmp != -1 && tmp < idx) {
  211.                     idx = tmp;
  212.                     separator = separator2;
  213.                 }
  214.             }
  215.         }
  216.         return parse(str, separator);
  217.     }

  218.     /**
  219.      * Parses out a token until any of the given terminators is encountered outside the quotation marks.
  220.      *
  221.      * @param terminators the array of terminating characters. Any of these characters when encountered outside the quotation marks signify the end of the token
  222.      * @return the token
  223.      */
  224.     private String parseQuotedToken(final char[] terminators) {
  225.         char ch;
  226.         i1 = pos;
  227.         i2 = pos;
  228.         var quoted = false;
  229.         var charEscaped = false;
  230.         while (hasChar()) {
  231.             ch = chars[pos];
  232.             if (!quoted && isOneOf(ch, terminators)) {
  233.                 break;
  234.             }
  235.             if (!charEscaped && ch == '"') {
  236.                 quoted = !quoted;
  237.             }
  238.             charEscaped = !charEscaped && ch == '\\';
  239.             i2++;
  240.             pos++;

  241.         }
  242.         return getToken(true);
  243.     }

  244.     /**
  245.      * Parses out a token until any of the given terminators is encountered.
  246.      *
  247.      * @param terminators the array of terminating characters. Any of these characters when encountered signify the end of the token
  248.      * @return the token
  249.      */
  250.     private String parseToken(final char[] terminators) {
  251.         char ch;
  252.         i1 = pos;
  253.         i2 = pos;
  254.         while (hasChar()) {
  255.             ch = chars[pos];
  256.             if (isOneOf(ch, terminators)) {
  257.                 break;
  258.             }
  259.             i2++;
  260.             pos++;
  261.         }
  262.         return getToken(false);
  263.     }

  264.     /**
  265.      * Sets the flag if parameter names are to be converted to lower case when name/value pairs are parsed.
  266.      *
  267.      * @param lowerCaseNames {@code true} if parameter names are to be converted to lower case when name/value pairs are parsed. {@code false} otherwise.
  268.      */
  269.     public void setLowerCaseNames(final boolean lowerCaseNames) {
  270.         this.lowerCaseNames = lowerCaseNames;
  271.     }

  272. }