Files
linux-packaging-mono/external/aspnetwebstack/src/System.Net.Http.Formatting/Formatting/FormUrlEncodedJson.cs

610 lines
24 KiB
C#
Raw Normal View History

// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Linq;
using System.Text;
using Newtonsoft.Json.Linq;
namespace System.Net.Http.Formatting
{
/// <summary>
/// This class provides a low-level API for parsing HTML form URL-encoded data, also known as <c>application/x-www-form-urlencoded</c>
/// data. The output of the parser is a <see cref="JObject"/> instance.
/// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
/// is not intended to be called directly from user code.</remarks>
/// </summary>
internal static class FormUrlEncodedJson
{
private const string ApplicationFormUrlEncoded = @"application/x-www-form-urlencoded";
private const int MinDepth = 0;
private static readonly string[] _emptyPath = new string[]
{
String.Empty
};
/// <summary>
/// Parses a collection of query string values as a <see cref="JObject"/>.
/// </summary>
/// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
/// is not intended to be called directly from user code.</remarks>
/// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
/// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
/// <returns>The <see cref="JObject"/> corresponding to the given query string values.</returns>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
public static JObject Parse(IEnumerable<KeyValuePair<string, string>> nameValuePairs)
{
return ParseInternal(nameValuePairs, Int32.MaxValue, true);
}
/// <summary>
/// Parses a collection of query string values as a <see cref="JObject"/>.
/// </summary>
/// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
/// is not intended to be called directly from user code.</remarks>
/// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
/// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
/// <param name="maxDepth">The maximum depth of object graph encoded as <c>x-www-form-urlencoded</c>.</param>
/// <returns>The <see cref="JObject"/> corresponding to the given query string values.</returns>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
public static JObject Parse(IEnumerable<KeyValuePair<string, string>> nameValuePairs, int maxDepth)
{
return ParseInternal(nameValuePairs, maxDepth, true);
}
/// <summary>
/// Parses a collection of query string values as a <see cref="JObject"/>.
/// </summary>
/// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
/// is not intended to be called directly from user code.</remarks>
/// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
/// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
/// <param name="value">The parsed result or null if parsing failed.</param>
/// <returns><c>true</c> if <paramref name="nameValuePairs"/> was parsed successfully; otherwise false.</returns>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
public static bool TryParse(IEnumerable<KeyValuePair<string, string>> nameValuePairs, out JObject value)
{
return (value = ParseInternal(nameValuePairs, Int32.MaxValue, false)) != null;
}
/// <summary>
/// Parses a collection of query string values as a <see cref="JObject"/>.
/// </summary>
/// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
/// is not intended to be called directly from user code.</remarks>
/// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
/// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
/// <param name="maxDepth">The maximum depth of object graph encoded as <c>x-www-form-urlencoded</c>.</param>
/// <param name="value">The parsed result or null if parsing failed.</param>
/// <returns><c>true</c> if <paramref name="nameValuePairs"/> was parsed successfully; otherwise false.</returns>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
public static bool TryParse(IEnumerable<KeyValuePair<string, string>> nameValuePairs, int maxDepth, out JObject value)
{
return (value = ParseInternal(nameValuePairs, maxDepth, false)) != null;
}
/// <summary>
/// Parses a collection of query string values as a <see cref="JObject"/>.
/// </summary>
/// <remarks>This is a low-level API intended for use by other APIs. It has been optimized for performance and
/// is not intended to be called directly from user code.</remarks>
/// <param name="nameValuePairs">The collection of query string name-value pairs parsed in lexical order. Both names
/// and values must be un-escaped so that they don't contain any <see cref="Uri"/> encoding.</param>
/// <param name="maxDepth">The maximum depth of object graph encoded as <c>x-www-form-urlencoded</c>.</param>
/// <param name="throwOnError">Indicates whether to throw an exception on error or return false</param>
/// <returns>The <see cref="JObject"/> corresponding to the given query string values.</returns>
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is a low-level API used by other APIs to provide end-user functionality.")]
private static JObject ParseInternal(IEnumerable<KeyValuePair<string, string>> nameValuePairs, int maxDepth, bool throwOnError)
{
if (nameValuePairs == null)
{
throw new ArgumentNullException("nameValuePairs");
}
if (maxDepth <= MinDepth)
{
throw new ArgumentOutOfRangeException("maxDepth", maxDepth, RS.Format(Properties.Resources.ArgumentMustBeGreaterThan, MinDepth));
}
JObject result = new JObject();
foreach (var nameValuePair in nameValuePairs)
{
string key = nameValuePair.Key;
string value = nameValuePair.Value;
// value is preserved, even if it's null, "undefined", "null", String.Empty, etc when converting to JToken.
if (key == null)
{
if (String.IsNullOrEmpty(value))
{
if (throwOnError)
{
throw new ArgumentException(Properties.Resources.QueryStringNameShouldNotNull, "nameValuePairs");
}
return null;
}
string[] path = new string[] { value };
if (!Insert(result, path, null, throwOnError))
{
return null;
}
}
else
{
string[] path = GetPath(key, maxDepth, throwOnError);
if (path == null || !Insert(result, path, value, throwOnError))
{
return null;
}
}
}
FixContiguousArrays(result);
return result;
}
private static string[] GetPath(string key, int maxDepth, bool throwOnError)
{
Contract.Assert(key != null, "Key cannot be null (this function is only called by Parse if key != null)");
if (String.IsNullOrWhiteSpace(key))
{
return _emptyPath;
}
if (!ValidateQueryString(key, throwOnError))
{
return null;
}
string[] path = key.Split('[');
for (int i = 0; i < path.Length; i++)
{
if (path[i].EndsWith("]", StringComparison.Ordinal))
{
path[i] = path[i].Substring(0, path[i].Length - 1);
}
}
// For consistency with JSON, the depth of a[b]=1 is 3 (which is the depth of {a:{b:1}}, given
// that in the JSON-XML mapping there's a <root> element wrapping the JSON object:
// <root><a><b>1</b></a></root>. So if the length of the path is greater than *or equal* to
// maxDepth, then we throw.
if (path.Length >= maxDepth)
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.MaxDepthExceeded, maxDepth));
}
return null;
}
return path;
}
private static bool ValidateQueryString(string key, bool throwOnError)
{
bool hasUnMatchedLeftBraket = false;
for (int i = 0; i < key.Length; i++)
{
switch (key[i])
{
case '[':
if (!hasUnMatchedLeftBraket)
{
hasUnMatchedLeftBraket = true;
}
else
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.NestedBracketNotValid, ApplicationFormUrlEncoded, i));
}
return false;
}
break;
case ']':
if (hasUnMatchedLeftBraket)
{
hasUnMatchedLeftBraket = false;
}
else
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.UnMatchedBracketNotValid, ApplicationFormUrlEncoded, i));
}
return false;
}
break;
}
}
if (hasUnMatchedLeftBraket)
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.NestedBracketNotValid, ApplicationFormUrlEncoded, key.LastIndexOf('[')));
}
return false;
}
return true;
}
private static bool Insert(JObject root, string[] path, string value, bool throwOnError)
{
// to-do: verify consistent with new parsing, whether single value is in path or value
Contract.Assert(root != null, "Root object can't be null");
JObject current = root;
JObject parent = null;
for (int i = 0; i < path.Length - 1; i++)
{
if (String.IsNullOrEmpty(path[i]))
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.InvalidArrayInsert, BuildPathString(path, i)));
}
return false;
}
if (!((IDictionary<string, JToken>)current).ContainsKey(path[i]))
{
current[path[i]] = new JObject();
}
else
{
// Since the loop goes up to the next-to-last item in the path, if we hit a null
// or a primitive, then we have a mismatching node.
if (current[path[i]] == null || current[path[i]] is JValue)
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, i)));
}
return false;
}
}
parent = current;
current = current[path[i]] as JObject;
}
string lastKey = path[path.Length - 1];
if (String.IsNullOrEmpty(lastKey) && path.Length > 1)
{
if (!AddToArray(parent, path, value, throwOnError))
{
return false;
}
}
else
{
if (current == null)
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, path.Length - 1)));
}
return false;
}
if (!AddToObject(current, path, value, throwOnError))
{
return false;
}
}
return true;
}
private static bool AddToObject(JObject obj, string[] path, string value, bool throwOnError)
{
Contract.Assert(obj != null, "JsonObject cannot be null");
int pathIndex = path.Length - 1;
string key = path[pathIndex];
if (((IDictionary<string, JToken>)obj).ContainsKey(key))
{
if (obj[key] == null || obj[key].Type == JTokenType.Null)
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, pathIndex)));
}
return false;
}
bool isRoot = path.Length == 1;
if (isRoot)
{
// jQuery 1.3 behavior, make it into an array(object) if primitive
if (obj[key].Type == JTokenType.String)
{
string oldValue = obj[key].ToObject<string>();
JObject jo = new JObject();
jo.Add("0", oldValue);
jo.Add("1", value);
obj[key] = jo;
}
else if (obj[key] is JObject)
{
// if it was already an object, simply add the value
JObject jo = obj[key] as JObject;
string index = GetIndex(jo, throwOnError);
if (index == null)
{
return false;
}
jo.Add(index, value);
}
}
else
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.JQuery13CompatModeNotSupportNestedJson, BuildPathString(path, pathIndex)));
}
return false;
}
}
else
{
// if the object didn't contain the key, simply add it now
// the null check here is necessary because otherwise the created JValue type will be implictly cast as a string JValue
if (value == null)
{
obj[key] = null;
}
else
{
obj[key] = value;
}
}
return true;
}
// JsonObject passed in is semantically an array
private static bool AddToArray(JObject parent, string[] path, string value, bool throwOnError)
{
Contract.Assert(parent != null, "Parent cannot be null");
Contract.Assert(path.Length >= 2, "The path must be at least 2, one for the ending [], and one for before the '[' (which can be empty)");
string parentPath = path[path.Length - 2];
Contract.Assert(((IDictionary<string, JToken>)parent).ContainsKey(parentPath), "It was added on insert to get to this point");
JObject jo = parent[parentPath] as JObject;
if (jo == null)
{
// a[b][c]=1&a[b][]=2 => invalid
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, BuildPathString(path, path.Length - 1)));
}
return false;
}
else
{
string index = GetIndex(jo, throwOnError);
if (index == null)
{
return false;
}
jo.Add(index, value);
}
return true;
}
// TODO: consider optimize it by only look at the last one
private static string GetIndex(JObject jsonObject, bool throwOnError)
{
int max = -1;
if (jsonObject.Count > 0)
{
IEnumerable<string> keys = ((IDictionary<string, JToken>)jsonObject).Keys;
foreach (var key in keys)
{
int tempInt;
if (Int32.TryParse(key, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out tempInt) && tempInt > max)
{
max = tempInt;
}
else
{
if (throwOnError)
{
throw new ArgumentException(RS.Format(Properties.Resources.FormUrlEncodedMismatchingTypes, key));
}
return null;
}
}
}
max++;
return max.ToString(CultureInfo.InvariantCulture);
}
private static void FixContiguousArrays(JToken jv)
{
JArray ja = jv as JArray;
if (ja != null)
{
for (int i = 0; i < ja.Count; i++)
{
if (ja[i] != null)
{
ja[i] = FixSingleContiguousArray(ja[i]);
FixContiguousArrays(ja[i]);
}
}
}
else
{
JObject jo = jv as JObject;
if (jo != null && jo.Count > 0)
{
List<string> keys = new List<string>(((IDictionary<string, JToken>)jo).Keys);
foreach (string key in keys)
{
if (jo[key] != null)
{
jo[key] = FixSingleContiguousArray(jo[key]);
FixContiguousArrays(jo[key]);
}
}
}
}
//// do nothing for primitives
}
private static JToken FixSingleContiguousArray(JToken original)
{
JObject jo = original as JObject;
if (jo != null && jo.Count > 0)
{
List<string> childKeys = new List<string>(((IDictionary<string, JToken>)jo).Keys);
List<string> sortedKeys;
if (CanBecomeArray(childKeys, out sortedKeys))
{
JArray newResult = new JArray();
foreach (string sortedKey in sortedKeys)
{
newResult.Add(jo[sortedKey]);
}
return newResult;
}
}
return original;
}
private static bool CanBecomeArray(List<string> keys, out List<string> sortedKeys)
{
List<ArrayCandidate> intKeys = new List<ArrayCandidate>();
sortedKeys = null;
bool areContiguousIndices = true;
foreach (string key in keys)
{
int intKey;
if (!Int32.TryParse(key, NumberStyles.None, CultureInfo.InvariantCulture, out intKey))
{
// if not a non-negative number, it cannot become an array
areContiguousIndices = false;
break;
}
string strKey = intKey.ToString(CultureInfo.InvariantCulture);
if (!strKey.Equals(key, StringComparison.Ordinal))
{
// int.Parse returned true, but it's not really the same number.
// It's the case for strings such as "1\0".
areContiguousIndices = false;
break;
}
intKeys.Add(new ArrayCandidate(intKey, strKey));
}
if (areContiguousIndices)
{
intKeys.Sort((x, y) => x.Key - y.Key);
for (int i = 0; i < intKeys.Count; i++)
{
if (intKeys[i].Key != i)
{
areContiguousIndices = false;
break;
}
}
}
if (areContiguousIndices)
{
sortedKeys = new List<string>(intKeys.Select(x => x.Value));
}
return areContiguousIndices;
}
private static string BuildPathString(string[] path, int i)
{
StringBuilder errorPath = new StringBuilder(path[0]);
for (int p = 1; p <= i; p++)
{
errorPath.AppendFormat("[{0}]", path[p]);
}
return errorPath.ToString();
}
/// <summary>
/// Class that wraps key-value pairs.
/// </summary>
/// <remarks>
/// This use of this class avoids a FxCop warning CA908 which happens if using various generic types.
/// </remarks>
private class ArrayCandidate
{
/// <summary>
/// Initializes a new instance of the <see cref="ArrayCandidate"/> class.
/// </summary>
/// <param name="key">The key of this <see cref="ArrayCandidate"/> instance.</param>
/// <param name="value">The value of this <see cref="ArrayCandidate"/> instance.</param>
public ArrayCandidate(int key, string value)
{
Key = key;
Value = value;
}
/// <summary>
/// Gets or sets the key of this <see cref="ArrayCandidate"/> instance.
/// </summary>
/// <value>
/// The key of this <see cref="ArrayCandidate"/> instance.
/// </value>
public int Key { get; set; }
/// <summary>
/// Gets or sets the value of this <see cref="ArrayCandidate"/> instance.
/// </summary>
/// <value>
/// The value of this <see cref="ArrayCandidate"/> instance.
/// </value>
public string Value { get; set; }
}
}
}