diff --git a/src/Components/WebView/WebView/src/PathString.cs b/src/Components/WebView/WebView/src/PathString.cs deleted file mode 100644 index b8bb145e5f6ec3ae3cade0812d70dd4f9a4df390..0000000000000000000000000000000000000000 --- a/src/Components/WebView/WebView/src/PathString.cs +++ /dev/null @@ -1,482 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// NOTE: This file is copied from src/Http/Http.Abstractions/src/PathString.cs -// and made internal with a namespace change. -// It can't be referenced directly from the StaticFiles package because that would cause this package to require -// Microsoft.AspNetCore.App, thus preventing it from being used anywhere ASP.NET Core isn't supported (such as -// various platforms that .NET MAUI runs on, such as Android and iOS). - -using System; -using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text; - -namespace Microsoft.AspNetCore.Components.WebView -{ - /// <summary> - /// Provides correct escaping for Path and PathBase values when needed to reconstruct a request or redirect URI string - /// </summary> - [TypeConverter(typeof(PathStringConverter))] - internal readonly struct PathString : IEquatable<PathString> - { - /// <summary> - /// Represents the empty path. This field is read-only. - /// </summary> - public static readonly PathString Empty = new(string.Empty); - - /// <summary> - /// Initialize the path string with a given value. This value must be in unescaped format. Use - /// PathString.FromUriComponent(value) if you have a path value which is in an escaped format. - /// </summary> - /// <param name="value">The unescaped path to be assigned to the Value property.</param> - public PathString(string? value) - { - if (!string.IsNullOrEmpty(value) && value[0] != '/') - { - throw new ArgumentException(Resources.FormatException_PathMustStartWithSlash(nameof(value)), nameof(value)); - } - Value = value; - } - - /// <summary> - /// The unescaped path value - /// </summary> - public string? Value { get; } - - /// <summary> - /// True if the path is not empty - /// </summary> - [MemberNotNullWhen(true, nameof(Value))] - public bool HasValue - { - get { return !string.IsNullOrEmpty(Value); } - } - - /// <summary> - /// Provides the path string escaped in a way which is correct for combining into the URI representation. - /// </summary> - /// <returns>The escaped path value</returns> - public override string ToString() - { - return ToUriComponent(); - } - - /// <summary> - /// Provides the path string escaped in a way which is correct for combining into the URI representation. - /// </summary> - /// <returns>The escaped path value</returns> - public string ToUriComponent() - { - if (!HasValue) - { - return string.Empty; - } - - var value = Value; - var i = 0; - for (; i < value.Length; i++) - { - if (!PathStringHelper.IsValidPathChar(value[i]) || PathStringHelper.IsPercentEncodedChar(value, i)) - { - break; - } - } - - if (i < value.Length) - { - return ToEscapedUriComponent(value, i); - } - - return value; - } - - private static string ToEscapedUriComponent(string value, int i) - { - StringBuilder? buffer = null; - - var start = 0; - var count = i; - var requiresEscaping = false; - - while (i < value.Length) - { - var isPercentEncodedChar = PathStringHelper.IsPercentEncodedChar(value, i); - if (PathStringHelper.IsValidPathChar(value[i]) || isPercentEncodedChar) - { - if (requiresEscaping) - { - // the current segment requires escape - buffer ??= new StringBuilder(value.Length * 3); - buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); - - requiresEscaping = false; - start = i; - count = 0; - } - - if (isPercentEncodedChar) - { - count += 3; - i += 3; - } - else - { - count++; - i++; - } - } - else - { - if (!requiresEscaping) - { - // the current segment doesn't require escape - buffer ??= new StringBuilder(value.Length * 3); - buffer.Append(value, start, count); - - requiresEscaping = true; - start = i; - count = 0; - } - - count++; - i++; - } - } - - if (count == value.Length && !requiresEscaping) - { - return value; - } - else - { - if (count > 0) - { - buffer ??= new StringBuilder(value.Length * 3); - - if (requiresEscaping) - { - buffer.Append(Uri.EscapeDataString(value.Substring(start, count))); - } - else - { - buffer.Append(value, start, count); - } - } - - return buffer?.ToString() ?? string.Empty; - } - } - - /// <summary> - /// Returns an PathString given the path as it is escaped in the URI format. The string MUST NOT contain any - /// value that is not a path. - /// </summary> - /// <param name="uriComponent">The escaped path as it appears in the URI format.</param> - /// <returns>The resulting PathString</returns> - public static PathString FromUriComponent(string uriComponent) - { - // REVIEW: what is the exactly correct thing to do? - return new PathString(Uri.UnescapeDataString(uriComponent)); - } - - /// <summary> - /// Returns an PathString given the path as from a Uri object. Relative Uri objects are not supported. - /// </summary> - /// <param name="uri">The Uri object</param> - /// <returns>The resulting PathString</returns> - public static PathString FromUriComponent(Uri uri) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - // REVIEW: what is the exactly correct thing to do? - return new PathString("/" + uri.GetComponents(UriComponents.Path, UriFormat.Unescaped)); - } - - /// <summary> - /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/>. - /// </summary> - /// <param name="other">The <see cref="PathString"/> to compare.</param> - /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> - public bool StartsWithSegments(PathString other) - { - return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared - /// using the specified comparison option. - /// </summary> - /// <param name="other">The <see cref="PathString"/> to compare.</param> - /// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param> - /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> - public bool StartsWithSegments(PathString other, StringComparison comparisonType) - { - var value1 = Value ?? string.Empty; - var value2 = other.Value ?? string.Empty; - if (value1.StartsWith(value2, comparisonType)) - { - return value1.Length == value2.Length || value1[value2.Length] == '/'; - } - return false; - } - - /// <summary> - /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns - /// the remaining segments. - /// </summary> - /// <param name="other">The <see cref="PathString"/> to compare.</param> - /// <param name="remaining">The remaining segments after the match.</param> - /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> - public bool StartsWithSegments(PathString other, out PathString remaining) - { - return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out remaining); - } - - /// <summary> - /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared - /// using the specified comparison option and returns the remaining segments. - /// </summary> - /// <param name="other">The <see cref="PathString"/> to compare.</param> - /// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param> - /// <param name="remaining">The remaining segments after the match.</param> - /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> - public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString remaining) - { - var value1 = Value ?? string.Empty; - var value2 = other.Value ?? string.Empty; - if (value1.StartsWith(value2, comparisonType)) - { - if (value1.Length == value2.Length || value1[value2.Length] == '/') - { - remaining = new PathString(value1[value2.Length..]); - return true; - } - } - remaining = Empty; - return false; - } - - /// <summary> - /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> and returns - /// the matched and remaining segments. - /// </summary> - /// <param name="other">The <see cref="PathString"/> to compare.</param> - /// <param name="matched">The matched segments with the original casing in the source value.</param> - /// <param name="remaining">The remaining segments after the match.</param> - /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> - public bool StartsWithSegments(PathString other, out PathString matched, out PathString remaining) - { - return StartsWithSegments(other, StringComparison.OrdinalIgnoreCase, out matched, out remaining); - } - - /// <summary> - /// Determines whether the beginning of this <see cref="PathString"/> instance matches the specified <see cref="PathString"/> when compared - /// using the specified comparison option and returns the matched and remaining segments. - /// </summary> - /// <param name="other">The <see cref="PathString"/> to compare.</param> - /// <param name="comparisonType">One of the enumeration values that determines how this <see cref="PathString"/> and value are compared.</param> - /// <param name="matched">The matched segments with the original casing in the source value.</param> - /// <param name="remaining">The remaining segments after the match.</param> - /// <returns>true if value matches the beginning of this string; otherwise, false.</returns> - public bool StartsWithSegments(PathString other, StringComparison comparisonType, out PathString matched, out PathString remaining) - { - var value1 = Value ?? string.Empty; - var value2 = other.Value ?? string.Empty; - if (value1.StartsWith(value2, comparisonType)) - { - if (value1.Length == value2.Length || value1[value2.Length] == '/') - { - matched = new PathString(value1.Substring(0, value2.Length)); - remaining = new PathString(value1[value2.Length..]); - return true; - } - } - remaining = Empty; - matched = Empty; - return false; - } - - /// <summary> - /// Adds two PathString instances into a combined PathString value. - /// </summary> - /// <returns>The combined PathString value</returns> - public PathString Add(PathString other) - { - if (HasValue && - other.HasValue && - Value[^1] == '/') - { - // If the path string has a trailing slash and the other string has a leading slash, we need - // to trim one of them. - var combined = string.Concat(Value.AsSpan(), other.Value.AsSpan(1)); - return new PathString(combined); - } - - return new PathString(Value + other.Value); - } - - /// <summary> - /// Combines a PathString and QueryString into the joined URI formatted string value. - /// </summary> - /// <returns>The joined URI formatted string value</returns> - public string Add(QueryString other) - { - return ToUriComponent() + other.ToUriComponent(); - } - - /// <summary> - /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. - /// </summary> - /// <param name="other">The second PathString for comparison.</param> - /// <returns>True if both PathString values are equal</returns> - public bool Equals(PathString other) - { - return Equals(other, StringComparison.OrdinalIgnoreCase); - } - - /// <summary> - /// Compares this PathString value to another value using a specific StringComparison type - /// </summary> - /// <param name="other">The second PathString for comparison</param> - /// <param name="comparisonType">The StringComparison type to use</param> - /// <returns>True if both PathString values are equal</returns> - public bool Equals(PathString other, StringComparison comparisonType) - { - if (!HasValue && !other.HasValue) - { - return true; - } - return string.Equals(Value, other.Value, comparisonType); - } - - /// <summary> - /// Compares this PathString value to another value. The default comparison is StringComparison.OrdinalIgnoreCase. - /// </summary> - /// <param name="obj">The second PathString for comparison.</param> - /// <returns>True if both PathString values are equal</returns> - public override bool Equals(object? obj) - { - if (obj is null) - { - return !HasValue; - } - return obj is PathString pathString && Equals(pathString); - } - - /// <summary> - /// Returns the hash code for the PathString value. The hash code is provided by the OrdinalIgnoreCase implementation. - /// </summary> - /// <returns>The hash code</returns> - public override int GetHashCode() - { - return (HasValue ? StringComparer.OrdinalIgnoreCase.GetHashCode(Value) : 0); - } - - /// <summary> - /// Operator call through to Equals - /// </summary> - /// <param name="left">The left parameter</param> - /// <param name="right">The right parameter</param> - /// <returns>True if both PathString values are equal</returns> - public static bool operator ==(PathString left, PathString right) - { - return left.Equals(right); - } - - /// <summary> - /// Operator call through to Equals - /// </summary> - /// <param name="left">The left parameter</param> - /// <param name="right">The right parameter</param> - /// <returns>True if both PathString values are not equal</returns> - public static bool operator !=(PathString left, PathString right) - { - return !left.Equals(right); - } - - /// <summary> - /// </summary> - /// <param name="left">The left parameter</param> - /// <param name="right">The right parameter</param> - /// <returns>The ToString combination of both values</returns> - public static string operator +(string left, PathString right) - { - // This overload exists to prevent the implicit string<->PathString converter from - // trying to call the PathString+PathString operator for things that are not path strings. - return string.Concat(left, right.ToString()); - } - - /// <summary> - /// </summary> - /// <param name="left">The left parameter</param> - /// <param name="right">The right parameter</param> - /// <returns>The ToString combination of both values</returns> - public static string operator +(PathString left, string? right) - { - // This overload exists to prevent the implicit string<->PathString converter from - // trying to call the PathString+PathString operator for things that are not path strings. - return string.Concat(left.ToString(), right); - } - - /// <summary> - /// Operator call through to Add - /// </summary> - /// <param name="left">The left parameter</param> - /// <param name="right">The right parameter</param> - /// <returns>The PathString combination of both values</returns> - public static PathString operator +(PathString left, PathString right) - { - return left.Add(right); - } - - /// <summary> - /// Operator call through to Add - /// </summary> - /// <param name="left">The left parameter</param> - /// <param name="right">The right parameter</param> - /// <returns>The PathString combination of both values</returns> - public static string operator +(PathString left, QueryString right) - { - return left.Add(right); - } - - /// <summary> - /// Implicitly creates a new PathString from the given string. - /// </summary> - /// <param name="s"></param> - public static implicit operator PathString(string? s) - => ConvertFromString(s); - - /// <summary> - /// Implicitly calls ToString(). - /// </summary> - /// <param name="path"></param> - public static implicit operator string(PathString path) - => path.ToString(); - - internal static PathString ConvertFromString(string? s) - => string.IsNullOrEmpty(s) ? new PathString(s) : FromUriComponent(s); - } - - internal sealed class PathStringConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - => sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - => value is string @string - ? PathString.ConvertFromString(@string) - : base.ConvertFrom(context, culture, value); - - public override object ConvertTo(ITypeDescriptorContext context, - CultureInfo culture, object value, Type destinationType) - => destinationType == typeof(string) - ? value.ToString() ?? string.Empty - : base.ConvertTo(context, culture, value, destinationType); - } -} diff --git a/src/Components/WebView/WebView/src/PathStringHelper.cs b/src/Components/WebView/WebView/src/PathStringHelper.cs deleted file mode 100644 index e132eeb13098ef41edbc3937415ded9dec4b1e54..0000000000000000000000000000000000000000 --- a/src/Components/WebView/WebView/src/PathStringHelper.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// NOTE: This file is copied from src/Http/Http.Abstractions/src/Internal/PathStringHelper.cs -// and made internal with a namespace change. -// It can't be referenced directly from the StaticFiles package because that would cause this package to require -// Microsoft.AspNetCore.App, thus preventing it from being used anywhere ASP.NET Core isn't supported (such as -// various platforms that .NET MAUI runs on, such as Android and iOS). - -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace Microsoft.AspNetCore.Components.WebView -{ - internal static class PathStringHelper - { - // uint[] bits uses 1 cache line (Array info + 16 bytes) - // bool[] would use 3 cache lines (Array info + 128 bytes) - // So we use 128 bits rather than 128 bytes/bools - private static readonly uint[] ValidPathChars = { - 0b_0000_0000__0000_0000__0000_0000__0000_0000, // 0x00 - 0x1F - 0b_0010_1111__1111_1111__1111_1111__1101_0010, // 0x20 - 0x3F - 0b_1000_0111__1111_1111__1111_1111__1111_1111, // 0x40 - 0x5F - 0b_0100_0111__1111_1111__1111_1111__1111_1110, // 0x60 - 0x7F - }; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidPathChar(char c) - { - // Use local array and uint .Length compare to elide the bounds check on array access - var validChars = ValidPathChars; - var i = (int)c; - - // Array is in chunks of 32 bits, so get offset by dividing by 32 - var offset = i >> 5; // i / 32; - // Significant bit position is the remainder of the above calc; i % 32 => i & 31 - var significantBit = 1u << (i & 31); - - // Check offset in bounds and check if significant bit set - return (uint)offset < (uint)validChars.Length && - ((validChars[offset] & significantBit) != 0); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsPercentEncodedChar(string str, int index) - { - var len = (uint)str.Length; - if (str[index] == '%' && index < len - 2) - { - return AreFollowingTwoCharsHex(str, index); - } - - return false; - } - - [MethodImpl(MethodImplOptions.NoInlining)] - private static bool AreFollowingTwoCharsHex(string str, int index) - { - Debug.Assert(index < str.Length - 2); - - var c1 = str[index + 1]; - var c2 = str[index + 2]; - return IsHexadecimalChar(c1) && IsHexadecimalChar(c2); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsHexadecimalChar(char c) - { - // Between 0 - 9 or uppercased between A - F - return (uint)(c - '0') <= 9 || (uint)((c & ~0x20) - 'A') <= ('F' - 'A'); - } - } -} diff --git a/src/Components/WebView/WebView/src/QueryString.cs b/src/Components/WebView/WebView/src/QueryString.cs deleted file mode 100644 index f2a23c6b2e883e4696b699099abe8b5e48aa4ace..0000000000000000000000000000000000000000 --- a/src/Components/WebView/WebView/src/QueryString.cs +++ /dev/null @@ -1,304 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// NOTE: This file is copied from src/Http/Http.Abstractions/src/QueryString.cs -// and made internal with a namespace change. -// It can't be referenced directly from the StaticFiles package because that would cause this package to require -// Microsoft.AspNetCore.App, thus preventing it from being used anywhere ASP.NET Core isn't supported (such as -// various platforms that .NET MAUI runs on, such as Android and iOS). - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Text.Encodings.Web; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.Components.WebView -{ - /// <summary> - /// Provides correct handling for QueryString value when needed to reconstruct a request or redirect URI string - /// </summary> - internal readonly struct QueryString : IEquatable<QueryString> - { - /// <summary> - /// Represents the empty query string. This field is read-only. - /// </summary> - public static readonly QueryString Empty = new QueryString(string.Empty); - - /// <summary> - /// Initialize the query string with a given value. This value must be in escaped and delimited format with - /// a leading '?' character. - /// </summary> - /// <param name="value">The query string to be assigned to the Value property.</param> - 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; - } - - /// <summary> - /// The escaped query string with the leading '?' character - /// </summary> - public string? Value { get; } - - /// <summary> - /// True if the query string is not empty - /// </summary> - public bool HasValue => !string.IsNullOrEmpty(Value); - - /// <summary> - /// 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. - /// </summary> - /// <returns>The query string value</returns> - public override string ToString() - { - return ToUriComponent(); - } - - /// <summary> - /// 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. - /// </summary> - /// <returns>The query string value</returns> - public string ToUriComponent() - { - // Escape things properly so System.Uri doesn't mis-interpret the data. - return !string.IsNullOrEmpty(Value) ? Value!.Replace("#", "%23") : string.Empty; - } - - /// <summary> - /// 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. - /// </summary> - /// <param name="uriComponent">The escaped query as it appears in the URI format.</param> - /// <returns>The resulting QueryString</returns> - public static QueryString FromUriComponent(string uriComponent) - { - if (string.IsNullOrEmpty(uriComponent)) - { - return new QueryString(string.Empty); - } - return new QueryString(uriComponent); - } - - /// <summary> - /// Returns an QueryString given the query as from a Uri object. Relative Uri objects are not supported. - /// </summary> - /// <param name="uri">The Uri object</param> - /// <returns>The resulting QueryString</returns> - public static QueryString FromUriComponent(Uri uri) - { - if (uri == null) - { - throw new ArgumentNullException(nameof(uri)); - } - - string queryValue = uri.GetComponents(UriComponents.Query, UriFormat.UriEscaped); - if (!string.IsNullOrEmpty(queryValue)) - { - queryValue = "?" + queryValue; - } - return new QueryString(queryValue); - } - - /// <summary> - /// Create a query string with a single given parameter name and value. - /// </summary> - /// <param name="name">The un-encoded parameter name</param> - /// <param name="value">The un-encoded parameter value</param> - /// <returns>The resulting QueryString</returns> - public static QueryString Create(string name, string value) - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (!string.IsNullOrEmpty(value)) - { - value = UrlEncoder.Default.Encode(value); - } - return new QueryString($"?{UrlEncoder.Default.Encode(name)}={value}"); - } - - /// <summary> - /// Creates a query string composed from the given name value pairs. - /// </summary> - /// <param name="parameters"></param> - /// <returns>The resulting QueryString</returns> - public static QueryString Create(IEnumerable<KeyValuePair<string, string?>> 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()); - } - - /// <summary> - /// Creates a query string composed from the given name value pairs. - /// </summary> - /// <param name="parameters"></param> - /// <returns>The resulting QueryString</returns> - public static QueryString Create(IEnumerable<KeyValuePair<string, StringValues>> 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()); - } - - /// <summary> - /// Concatenates <paramref name="other"/> to the current query string. - /// </summary> - /// <param name="other">The <see cref="QueryString"/> to concatenate.</param> - /// <returns>The concatenated <see cref="QueryString"/>.</returns> - 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))); - } - - /// <summary> - /// Concatenates a query string with <paramref name="name"/> and <paramref name="value"/> - /// to the current query string. - /// </summary> - /// <param name="name">The name of the query string to concatenate.</param> - /// <param name="value">The value of the query string to concatenate.</param> - /// <returns>The concatenated <see cref="QueryString"/>.</returns> - public QueryString Add(string name, string value) - { - if (name == null) - { - throw new ArgumentNullException(nameof(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()); - } - - /// <summary> - /// Evalutes if the current query string is equal to <paramref name="other"/>. - /// </summary> - /// <param name="other">The <see cref="QueryString"/> to compare.</param> - /// <returns><see langword="true"/> if the ssquery strings are equal.</returns> - public bool Equals(QueryString other) - { - if (!HasValue && !other.HasValue) - { - return true; - } - return string.Equals(Value, other.Value, StringComparison.Ordinal); - } - - /// <summary> - /// Evaluates if the current query string is equal to an object <paramref name="obj"/>. - /// </summary> - /// <param name="obj">An object to compare.</param> - /// <returns><see langword="true" /> if the query strings are equal.</returns> - public override bool Equals(object? obj) - { - if (ReferenceEquals(null, obj)) - { - return !HasValue; - } - return obj is QueryString && Equals((QueryString)obj); - } - - /// <summary> - /// Gets a hash code for the value. - /// </summary> - /// <returns>The hash code as an <see cref="int"/>.</returns> - public override int GetHashCode() - { - return (HasValue ? Value!.GetHashCode() : 0); - } - - /// <summary> - /// Evaluates if one query string is equal to another. - /// </summary> - /// <param name="left">A <see cref="QueryString"/> instance.</param> - /// <param name="right">A <see cref="QueryString"/> instance.</param> - /// <returns><see langword="true" /> if the query strings are equal.</returns> - public static bool operator ==(QueryString left, QueryString right) - { - return left.Equals(right); - } - - /// <summary> - /// Evaluates if one query string is not equal to another. - /// </summary> - /// <param name="left">A <see cref="QueryString"/> instance.</param> - /// <param name="right">A <see cref="QueryString"/> instance.</param> - /// <returns><see langword="true" /> if the query strings are not equal.</returns> - public static bool operator !=(QueryString left, QueryString right) - { - return !left.Equals(right); - } - - /// <summary> - /// Concatenates <paramref name="left"/> and <paramref name="right"/> into a single query string. - /// </summary> - /// <param name="left">A <see cref="QueryString"/> instance.</param> - /// <param name="right">A <see cref="QueryString"/> instance.</param> - /// <returns>The concatenated <see cref="QueryString"/>.</returns> - 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)); - } - } - } -}