using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; using System.Text.Encodings.Web; using Microsoft.Extensions.Primitives; namespace Iceshrimp.Frontend.Core.Miscellaneous; // Adapted under MIT from https://raw.githubusercontent.com/dotnet/aspnetcore/3f1acb59718cadf111a0a796681e3d3509bb3381/src/Http/Http.Abstractions/src/QueryString.cs /// /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string /// [DebuggerDisplay("{Value}")] public readonly struct QueryString : IEquatable { /// /// Represents the empty query string. This field is read-only. /// public static readonly QueryString Empty = new(string.Empty); /// /// Initialize the query string with a given value. This value must be in escaped and delimited format with /// a leading '?' character. /// /// The query string to be assigned to the Value property. public QueryString(string? value) { if (!string.IsNullOrEmpty(value) && value[0] != '?') { throw new ArgumentException(@"The leading '?' must be included for a non-empty query.", nameof(value)); } Value = value; } /// /// The escaped query string with the leading '?' character /// public string? Value { get; } /// /// True if the query string is not empty /// [MemberNotNullWhen(true, nameof(Value))] public bool HasValue => !string.IsNullOrEmpty(Value); /// /// Provides the query string escaped in a way which is correct for combining into the URI representation. /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially /// dangerous are escaped. /// /// The query string value public override string ToString() { return ToUriComponent(); } /// /// Provides the query string escaped in a way which is correct for combining into the URI representation. /// A leading '?' character will be included unless the Value is null or empty. Characters which are potentially /// dangerous are escaped. /// /// The query string value public string ToUriComponent() { // Escape things properly so System.Uri doesn't mis-interpret the data. return HasValue ? Value.Replace("#", "%23") : string.Empty; } /// /// Returns an QueryString given the query as it is escaped in the URI format. The string MUST NOT contain any /// value that is not a query. /// /// The escaped query as it appears in the URI format. /// The resulting QueryString public static QueryString FromUriComponent(string uriComponent) { if (string.IsNullOrEmpty(uriComponent)) { return new QueryString(string.Empty); } return new QueryString(uriComponent); } /// /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. /// /// The Uri object /// The resulting QueryString public static QueryString FromUriComponent(Uri uri) { ArgumentNullException.ThrowIfNull(uri); var queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); if (!string.IsNullOrEmpty(queryValue)) { queryValue = "?" + queryValue; } return new QueryString(queryValue); } /// /// Create a query string with a single given parameter name and value. /// /// The un-encoded parameter name /// The un-encoded parameter value /// The resulting QueryString public static QueryString Create(string name, string value) { ArgumentNullException.ThrowIfNull(name); if (!string.IsNullOrEmpty(value)) { value = UrlEncoder.Default.Encode(value); } return new QueryString($"?{UrlEncoder.Default.Encode(name)}={value}"); } /// /// Creates a query string composed from the given name value pairs. /// /// /// The resulting QueryString public static QueryString Create(IEnumerable> parameters) { var builder = new StringBuilder(); var first = true; foreach (var pair in parameters) { AppendKeyValuePair(builder, pair.Key, pair.Value, first); first = false; } return new QueryString(builder.ToString()); } /// /// Creates a query string composed from the given name value pairs. /// /// /// The resulting QueryString public static QueryString Create(IEnumerable> parameters) { var builder = new StringBuilder(); var first = true; foreach (var pair in parameters) { // If nothing in this pair.Values, append null value and continue if (StringValues.IsNullOrEmpty(pair.Value)) { AppendKeyValuePair(builder, pair.Key, null, first); first = false; continue; } // Otherwise, loop through values in pair.Value foreach (var value in pair.Value) { AppendKeyValuePair(builder, pair.Key, value, first); first = false; } } return new QueryString(builder.ToString()); } /// /// Concatenates to the current query string. /// /// The to concatenate. /// The concatenated . public QueryString Add(QueryString other) { if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) { return other; } if (!other.HasValue || other.Value.Equals("?", StringComparison.Ordinal)) { return this; } // ?name1=value1 Add ?name2=value2 returns ?name1=value1&name2=value2 return new QueryString(string.Concat(Value, "&", other.Value.AsSpan(1))); } /// /// Concatenates a query string with and /// to the current query string. /// /// The name of the query string to concatenate. /// The value of the query string to concatenate. /// The concatenated . public QueryString Add(string name, string value) { ArgumentNullException.ThrowIfNull(name); if (!HasValue || Value.Equals("?", StringComparison.Ordinal)) { return Create(name, value); } var builder = new StringBuilder(Value); AppendKeyValuePair(builder, name, value, first: false); return new QueryString(builder.ToString()); } /// /// Evalutes if the current query string is equal to . /// /// The to compare. /// if the query strings are equal. public bool Equals(QueryString other) { if (!HasValue && !other.HasValue) { return true; } return string.Equals(Value, other.Value, StringComparison.Ordinal); } /// /// Evaluates if the current query string is equal to an object . /// /// An object to compare. /// if the query strings are equal. public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) { return !HasValue; } return obj is QueryString && Equals((QueryString)obj); } /// /// Gets a hash code for the value. /// /// The hash code as an . public override int GetHashCode() { return HasValue ? Value.GetHashCode() : 0; } /// /// Evaluates if one query string is equal to another. /// /// A instance. /// A instance. /// if the query strings are equal. public static bool operator ==(QueryString left, QueryString right) { return left.Equals(right); } /// /// Evaluates if one query string is not equal to another. /// /// A instance. /// A instance. /// if the query strings are not equal. public static bool operator !=(QueryString left, QueryString right) { return !left.Equals(right); } /// /// Concatenates and into a single query string. /// /// A instance. /// A instance. /// The concatenated . public static QueryString operator +(QueryString left, QueryString right) { return left.Add(right); } private static void AppendKeyValuePair(StringBuilder builder, string key, string? value, bool first) { builder.Append(first ? '?' : '&'); builder.Append(UrlEncoder.Default.Encode(key)); builder.Append('='); if (!string.IsNullOrEmpty(value)) { builder.Append(UrlEncoder.Default.Encode(value)); } } }