You've already forked linux-packaging-mono
610 lines
24 KiB
C#
610 lines
24 KiB
C#
// 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; }
|
|
}
|
|
}
|
|
}
|