Last active
October 18, 2023 09:20
-
-
Save peteroupc/5619864 to your computer and use it in GitHub Desktop.
A C# utility class for parsing query strings.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Written by Peter O. | |
// Any copyright to this work is released to the Public Domain. | |
// https://creativecommons.org/publicdomain/zero/1.0/ | |
// | |
// | |
// | |
using System; | |
using System.Collections.Generic; | |
using System.Globalization; | |
using System.Text; | |
namespace PeterO { | |
public sealed class QueryStringHelper { | |
private QueryStringHelper() {} | |
private static string[] SplitAt (string s, string delimiter) { | |
if (delimiter == null || delimiter.Length == 0) { | |
throw new ArgumentException(); | |
} | |
if (s == null || s.Length == 0) { | |
return new string[] { ""}; | |
} | |
var index = 0; | |
var first = true; | |
List<string> strings = null; | |
int delimLength = delimiter.Length; | |
while (true) { | |
int index2 = s.IndexOf (delimiter, index, StringComparison.Ordinal); | |
if (index2 < 0) { | |
if (first) { | |
return new string[] { s}; | |
} | |
strings.Add (s.Substring (index)); | |
break; | |
} else { | |
if (first) { | |
strings = new List<string>(); | |
first = false; | |
} | |
string newstr = s.Substring (index, (index2) - (index)); | |
strings.Add (newstr); | |
index = index2 + delimLength; | |
} | |
} | |
return strings.ToArray(); | |
} | |
private static int ToHexNumber (int c) { | |
if (c >= 'A' && c <= 'Z') { | |
return 10 + c - 'A'; | |
} else if (c >= 'a' && c <= 'z') { | |
return 10 + c - 'a'; | |
} else { | |
return (c >= '0' && c <= '9') ? (c - '0') : (-1); | |
} | |
} | |
private static string PercentDecodeUTF8 (string str) { | |
int len = str.Length; | |
var percent = false; | |
for (int i = 0; i < len; ++i) { | |
char c = str[i]; | |
if (c == '%') { | |
percent = true; | |
} else if (c >= 0x80) { | |
// Non-ASCII characters not allowed | |
return null; | |
} | |
} | |
if (!percent) { | |
return str; // return early if there are no percent decodings | |
} | |
var cp = 0; | |
var bytesSeen = 0; | |
var bytesNeeded = 0; | |
var lower = 0x80; | |
var upper = 0xbf; | |
var markedPos = -1; | |
var retString = new StringBuilder(); | |
for (int i = 0; i < len; ++i) { | |
int c = str[i]; | |
if (c == '%') { | |
if (i + 2 < len) { | |
int a = ToHexNumber (str[i + 1]); | |
int b = ToHexNumber (str[i + 2]); | |
if (a >= 0 && b >= 0) { | |
b = (byte)((a * 16) + b); | |
i += 2; | |
// b now contains the byte read | |
if (bytesNeeded == 0) { | |
// this is the lead byte | |
if (b < 0x80) { | |
retString.Append ((char)b); | |
continue; | |
} else if (b >= 0xc2 && b <= 0xdf) { | |
markedPos = i; | |
bytesNeeded = 1; | |
cp = b - 0xc0; | |
} else if (b >= 0xe0 && b <= 0xef) { | |
markedPos = i; | |
lower = (b == 0xe0) ? 0xa0 : 0x80; | |
upper = (b == 0xed) ? 0x9f : 0xbf; | |
bytesNeeded = 2; | |
cp = b - 0xe0; | |
} else if (b >= 0xf0 && b <= 0xf4) { | |
markedPos = i; | |
lower = (b == 0xf0) ? 0x90 : 0x80; | |
upper = (b == 0xf4) ? 0x8f : 0xbf; | |
bytesNeeded = 3; | |
cp = b - 0xf0; | |
} else { | |
// illegal byte in UTF-8 | |
retString.Append ('\uFFFD'); | |
continue; | |
} | |
cp <<= (6 * bytesNeeded); | |
continue; | |
} else { | |
// this is a second or further byte | |
if (b < lower || b > upper) { | |
// illegal trailing byte | |
cp = bytesNeeded = bytesSeen = 0; | |
lower = 0x80; | |
upper = 0xbf; | |
i = markedPos; // reset to the last marked position | |
retString.Append ('\uFFFD'); | |
continue; | |
} | |
// reset lower and upper for the third | |
// and further bytes | |
lower = 0x80; | |
upper = 0xbf; | |
++bytesSeen; | |
cp += (b - 0x80) << (6 * (bytesNeeded - bytesSeen)); | |
markedPos = i; | |
if (bytesSeen != bytesNeeded) { | |
// continue if not all bytes needed | |
// were read yet | |
continue; | |
} | |
int ret = cp; | |
cp = 0; | |
bytesSeen = 0; | |
bytesNeeded = 0; | |
// append the Unicode character | |
if (ret <= 0xffff) { | |
{ retString.Append ((char)(ret)); | |
} | |
} else { | |
retString.Append ((char)((((ret - 0x10000) >> 10) & | |
0x3ff) | 0xd800)); | |
retString.Append ((char)(((ret - 0x10000) & 0x3ff) | | |
0xdc00)); | |
} | |
continue; | |
} | |
} | |
} | |
} | |
if (bytesNeeded > 0) { | |
// we expected further bytes here, | |
// so emit a replacement character instead | |
bytesNeeded = 0; | |
retString.Append ('\uFFFD'); | |
} | |
// append the code point as is (we already | |
// checked for ASCII characters so this will | |
// be simple | |
retString.Append ((char)(c & 0xff)); | |
} | |
if (bytesNeeded > 0) { | |
// we expected further bytes here, | |
// so emit a replacement character instead | |
bytesNeeded = 0; | |
retString.Append ('\uFFFD'); | |
} | |
return retString.ToString(); | |
} | |
public static IList<string[]> ParseQueryString( | |
string input) { | |
return ParseQueryString (input, null); | |
} | |
public static IList<string[]> ParseQueryString( | |
string input, | |
string delimiter) { | |
if ((input) == null) { | |
throw new ArgumentNullException(nameof(input)); | |
} | |
if (delimiter == null) { | |
// set default delimiter to ampersand | |
delimiter = "&"; | |
} | |
// Check input for non-ASCII characters | |
for (int i = 0; i < input.Length; ++i) { | |
if (input[i] > 0x7f) { | |
throw new ArgumentException("input contains a non-ASCII character"); | |
} | |
} | |
// split on delimiter | |
string[] strings = SplitAt (input, delimiter); | |
List<string[]> pairs = new List<string[]>(); | |
foreach (var str in strings) { | |
if (str.Length == 0) { | |
continue; | |
} | |
// split on key | |
int index = str.IndexOf ('='); | |
string name = str; | |
string value = ""; // value is empty if there is no key | |
if (index >= 0) { | |
name = str.Substring (0, (index) - (0)); | |
value = str.Substring (index + 1); | |
} | |
name = name.Replace ('+', ' '); | |
value = value.Replace ('+', ' '); | |
var pair = new string[] { name, value}; | |
pairs.Add (pair); | |
} | |
foreach (var pair in pairs) { | |
// percent decode the key and value if necessary | |
pair[0] = PercentDecodeUTF8 (pair[0]); | |
pair[1] = PercentDecodeUTF8 (pair[1]); | |
} | |
return pairs; | |
} | |
private static string[] GetKeyPath (string s) { | |
int index = s.IndexOf ('['); | |
if (index < 0) { // start bracket not found | |
return new string[] { s}; | |
} | |
var path = new List<string>(); | |
path.Add (s.Substring (0, (index) - (0))); | |
++index; // move to after the bracket | |
while (true) { | |
int endBracket = s.IndexOf (']', index); | |
if (endBracket < 0) { // end bracket not found | |
path.Add (s.Substring (index)); | |
break; | |
} | |
path.Add (s.Substring (index, (endBracket) - (index))); | |
index = endBracket + 1; // move to after the end bracket | |
index = s.IndexOf ('[', index); | |
if (index < 0) { // start bracket not found | |
break; | |
} | |
++index; // move to after the start bracket | |
} | |
return path.ToArray(); | |
} | |
private static bool IsList (IDictionary<string, Object> dict) { | |
if (dict == null) { | |
return false; | |
} | |
var index = 0; | |
int count = dict.Count; | |
if (count == 0) { | |
return false; | |
} | |
while (true) { | |
if (index == count) { | |
return true; | |
} | |
string indexString = Convert.ToString (index, | |
CultureInfo.InvariantCulture); | |
if (!dict.ContainsKey (indexString)) { | |
return false; | |
} | |
++index; | |
} | |
} | |
private static IList<Object> ConvertToList (IDictionary<string, Object> | |
dict) { | |
var ret = new List<Object>(); | |
var index = 0; | |
int count = dict.Count; | |
while (index < count) { | |
string indexString = Convert.ToString (index, | |
CultureInfo.InvariantCulture); | |
ret.Add (dict[indexString]); | |
++index; | |
} | |
return ret; | |
} | |
private static void ConvertLists (IList<Object> dict) { | |
for (int i = 0; i < dict.Count; ++i) { | |
IDictionary<string, Object> value = ((dict[i] is | |
IDictionary<string, Object>) ? (IDictionary<string, Object>)dict[i] : | |
null); | |
// A list contains only indexes 0, 1, 2, and so on, | |
// with no gaps. | |
if (IsList (value)) { | |
IList<Object> newList = ConvertToList (value); | |
dict[i] = newList; | |
ConvertLists (newList); | |
} else if (value != null) { | |
// Convert the list's descendents | |
// if they are lists | |
ConvertLists (value); | |
} | |
} | |
} | |
private static void ConvertLists (IDictionary<string, Object> dict) { | |
foreach (var key in new List<string>(dict.Keys)) { | |
IDictionary<string, Object> value = ((dict[key] is | |
IDictionary<string, Object>) ? | |
(IDictionary<string, Object>)dict[key] : null); | |
// A list contains only indexes 0, 1, 2, and so on, | |
// with no gaps. | |
if (IsList (value)) { | |
IList<Object> newList = ConvertToList (value); | |
dict[key] = newList; | |
ConvertLists (newList); | |
} else if (value != null) { | |
// Convert the dictionary's descendents | |
// if they are lists | |
ConvertLists (value); | |
} | |
} | |
} | |
public static IDictionary<string, Object> QueryStringToDict (string query) { | |
return QueryStringToDict (query, "&"); | |
} | |
public static IDictionary<string, Object> QueryStringToDict (string query, | |
string delimiter) { | |
IDictionary<string, Object> root = new Dictionary<string, Object>(); | |
foreach (var keyvalue in ParseQueryString (query, delimiter)) { | |
string[] path = GetKeyPath (keyvalue[0]); | |
IDictionary<string, Object> leaf = root; | |
for (int i = 0; i < path.Length - 1; ++i) { | |
if (!leaf.ContainsKey (path[i])) { | |
// node doesn't exist so add it | |
IDictionary<string, Object> newLeaf = new Dictionary<string, Object>(); | |
leaf.Add (path[i], newLeaf); | |
leaf = newLeaf; | |
} else { | |
IDictionary<string, Object> o = ((leaf[path[i]] is | |
IDictionary<string, Object>) ? | |
(IDictionary<string, Object>)leaf[path[i]] : null); | |
if (o != null) { | |
leaf = o; | |
} else { | |
// error, not a dictionary | |
leaf = null; | |
break; | |
} | |
} | |
} | |
if (leaf != null) { | |
leaf.Add (path[path.Length - 1], keyvalue[1]); | |
} | |
} | |
// Convert array-like dictionaries to ILists | |
ConvertLists (root); | |
return root; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment