using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; namespace gehGassi.Pwned { public static class QueryHelpers { /// /// Append the given query key and value to the URI. /// /// The base URI. /// The name of the query key. /// The query value. /// The combined result. public static string AddQueryString(string uri, string name, string value) { if (uri == null) { throw new ArgumentNullException(nameof(uri)); } if (name == null) { throw new ArgumentNullException(nameof(name)); } if (value == null) { throw new ArgumentNullException(nameof(value)); } return AddQueryString( uri, new[] { new KeyValuePair(name, value) }); } /// /// Append the given query keys and values to the uri. /// /// The base uri. /// A collection of name value query pairs to append. /// The combined result. public static string AddQueryString(string uri, IDictionary queryString) { if (uri == null) { throw new ArgumentNullException(nameof(uri)); } if (queryString == null) { throw new ArgumentNullException(nameof(queryString)); } return AddQueryString(uri, (IEnumerable>)queryString); } private static string AddQueryString( string uri, IEnumerable> queryString) { if (uri == null) { throw new ArgumentNullException(nameof(uri)); } if (queryString == null) { throw new ArgumentNullException(nameof(queryString)); } var anchorIndex = uri.IndexOf('#'); var uriToBeAppended = uri; var anchorText = ""; // If there is an anchor, then the query string must be inserted before its first occurence. if (anchorIndex != -1) { anchorText = uri.Substring(anchorIndex); uriToBeAppended = uri.Substring(0, anchorIndex); } var queryIndex = uriToBeAppended.IndexOf('?'); var hasQuery = queryIndex != -1; var sb = new StringBuilder(); sb.Append(uriToBeAppended); foreach (var parameter in queryString) { sb.Append(hasQuery ? '&' : '?'); sb.Append(UrlEncoder.Default.Encode(parameter.Key)); sb.Append('='); sb.Append(UrlEncoder.Default.Encode(parameter.Value)); hasQuery = true; } sb.Append(anchorText); return sb.ToString(); } /// /// Parse a query string into its component key and value parts. /// /// The raw query string value, with or without the leading '?'. /// A collection of parsed keys and values. public static Dictionary ParseQuery(string queryString) { var result = ParseNullableQuery(queryString); if (result == null) { return new Dictionary(); } return result; } /// /// Parse a query string into its component key and value parts. /// /// The raw query string value, with or without the leading '?'. /// A collection of parsed keys and values, null if there are no entries. public static Dictionary ParseNullableQuery(string queryString) { var accumulator = new KeyValueAccumulator(); if (string.IsNullOrEmpty(queryString) || queryString == "?") { return null; } int scanIndex = 0; if (queryString[0] == '?') { scanIndex = 1; } int textLength = queryString.Length; int equalIndex = queryString.IndexOf('='); if (equalIndex == -1) { equalIndex = textLength; } while (scanIndex < textLength) { int delimiterIndex = queryString.IndexOf('&', scanIndex); if (delimiterIndex == -1) { delimiterIndex = textLength; } if (equalIndex < delimiterIndex) { while (scanIndex != equalIndex && char.IsWhiteSpace(queryString[scanIndex])) { ++scanIndex; } string name = queryString.Substring(scanIndex, equalIndex - scanIndex); string value = queryString.Substring(equalIndex + 1, delimiterIndex - equalIndex - 1); accumulator.Append( Uri.UnescapeDataString(name.Replace('+', ' ')), Uri.UnescapeDataString(value.Replace('+', ' '))); equalIndex = queryString.IndexOf('=', delimiterIndex); if (equalIndex == -1) { equalIndex = textLength; } } else { if (delimiterIndex > scanIndex) { accumulator.Append(queryString.Substring(scanIndex, delimiterIndex - scanIndex), string.Empty); } } scanIndex = delimiterIndex + 1; } if (!accumulator.HasValues) { return null; } return accumulator.GetResults(); } } public struct KeyValueAccumulator { private Dictionary _accumulator; private Dictionary> _expandingAccumulator; public void Append(string key, string value) { if (_accumulator == null) { _accumulator = new Dictionary(StringComparer.OrdinalIgnoreCase); } StringValues values; if (_accumulator.TryGetValue(key, out values)) { if (values.Count == 0) { // Marker entry for this key to indicate entry already in expanding list dictionary _expandingAccumulator[key].Add(value); } else if (values.Count == 1) { // Second value for this key _accumulator[key] = new string[] { values[0], value }; } else { // Third value for this key // Add zero count entry and move to data to expanding list dictionary _accumulator[key] = default(StringValues); if (_expandingAccumulator == null) { _expandingAccumulator = new Dictionary>(StringComparer.OrdinalIgnoreCase); } // Already 3 entries so use starting allocated as 8; then use List's expansion mechanism for more var list = new List(8); var array = values.ToArray(); list.Add(array[0]); list.Add(array[1]); list.Add(value); _expandingAccumulator[key] = list; } } else { // First value for this key _accumulator[key] = new StringValues(value); } ValueCount++; } public bool HasValues => ValueCount > 0; public int KeyCount => _accumulator?.Count ?? 0; public int ValueCount { get; private set; } public Dictionary GetResults() { if (_expandingAccumulator != null) { // Coalesce count 3+ multi-value entries into _accumulator dictionary foreach (var entry in _expandingAccumulator) { _accumulator[entry.Key] = new StringValues(entry.Value.ToArray()); } } return _accumulator ?? new Dictionary(0, StringComparer.OrdinalIgnoreCase); } } }