#485 ordinal string comparisons everywhere
This commit is contained in:
parent
7defe0cbb8
commit
f3551eace0
@ -38,19 +38,19 @@ namespace Flurl.Http
|
||||
|
||||
// ordinal string compare is both safest and fastest
|
||||
// https://docs.microsoft.com/en-us/dotnet/standard/base-types/best-practices-strings#recommendations-for-string-usage
|
||||
else if (pair.Name.Equals("Expires", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("Expires", true))
|
||||
cookie.Expires = DateTimeOffset.TryParse(pair.Value, out var d) ? d : (DateTimeOffset?)null;
|
||||
else if (pair.Name.Equals("Max-Age", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("Max-Age", true))
|
||||
cookie.MaxAge = int.TryParse(pair.Value, out var i) ? i : (int?)null;
|
||||
else if (pair.Name.Equals("Domain", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("Domain", true))
|
||||
cookie.Domain = pair.Value;
|
||||
else if (pair.Name.Equals("Path", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("Path", true))
|
||||
cookie.Path = pair.Value;
|
||||
else if (pair.Name.Equals("HttpOnly", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("HttpOnly", true))
|
||||
cookie.HttpOnly = true;
|
||||
else if (pair.Name.Equals("Secure", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("Secure", true))
|
||||
cookie.Secure = true;
|
||||
else if (pair.Name.Equals("SameSite", StringComparison.OrdinalIgnoreCase))
|
||||
else if (pair.Name.OrdinalEquals("SameSite", true))
|
||||
cookie.SameSite = Enum.TryParse<SameSite>(pair.Value, true, out var val) ? val : (SameSite?)null;
|
||||
}
|
||||
return cookie;
|
||||
@ -103,11 +103,11 @@ namespace Flurl.Http
|
||||
reason = "Domain cannot be set when origin URL is an IP address.";
|
||||
return false;
|
||||
}
|
||||
if (!cookie.Domain.Trim('.').Contains(".")) {
|
||||
if (!cookie.Domain.Trim('.').OrdinalContains(".")) {
|
||||
reason = $"{cookie.Domain} is not a valid value for Domain.";
|
||||
return false;
|
||||
}
|
||||
var host = cookie.Domain.StartsWith(".") ? cookie.Domain.Substring(1) : cookie.Domain;
|
||||
var host = cookie.Domain.OrdinalStartsWith(".") ? cookie.Domain.Substring(1) : cookie.Domain;
|
||||
var fakeUrl = new Url("https://" + host);
|
||||
if (fakeUrl.IsRelative || fakeUrl.Host != host) {
|
||||
reason = $"{cookie.Domain} is not a valid Domain. A non-empty Domain must be a valid URI host (no scheme, path, port, etc).";
|
||||
@ -120,7 +120,7 @@ namespace Flurl.Http
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Cookie_prefixes
|
||||
|
||||
if (cookie.Name.StartsWith("__Host-")) {
|
||||
if (cookie.Name.OrdinalStartsWith("__Host-")) {
|
||||
if (!cookie.OriginUrl.IsSecureScheme) {
|
||||
reason = "Cookie named with __Host- prefix must originate from a secure (https) domain.";
|
||||
return false;
|
||||
@ -138,7 +138,7 @@ namespace Flurl.Http
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (cookie.Name.StartsWith("__Secure-")) {
|
||||
if (cookie.Name.OrdinalStartsWith("__Secure-")) {
|
||||
if (!cookie.OriginUrl.IsSecureScheme) {
|
||||
reason = "Cookie named with __Secure- prefix must originate from a secure (https) domain.";
|
||||
return false;
|
||||
@ -150,7 +150,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
// it seems intuitive tht a non-empty path should start with /, but I can't find this in any spec
|
||||
//if (!string.IsNullOrEmpty(Path) && !Path.StartsWith("/")) {
|
||||
//if (!string.IsNullOrEmpty(Path) && !Path.OrdinalStartsWith("/")) {
|
||||
// reason = $"{Path} is not a valid Path. A non-empty Path must start with a / character.";
|
||||
// return false;
|
||||
//}
|
||||
@ -199,18 +199,18 @@ namespace Flurl.Http
|
||||
reason = "ok";
|
||||
|
||||
if (!string.IsNullOrEmpty(cookie.Domain)) {
|
||||
var domain = cookie.Domain.StartsWith(".") ? cookie.Domain.Substring(1) : cookie.Domain;
|
||||
if (requestUrl.Host.Equals(domain, StringComparison.OrdinalIgnoreCase))
|
||||
var domain = cookie.Domain.OrdinalStartsWith(".") ? cookie.Domain.Substring(1) : cookie.Domain;
|
||||
if (requestUrl.Host.OrdinalEquals(domain, true))
|
||||
return true;
|
||||
|
||||
if (requestUrl.Host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase))
|
||||
if (requestUrl.Host.OrdinalEndsWith("." + domain, true))
|
||||
return true;
|
||||
|
||||
reason = $"Cookie with Domain={cookie.Domain} should not be sent to {requestUrl.Host}.";
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
if (requestUrl.Host.Equals(cookie.OriginUrl.Host, StringComparison.OrdinalIgnoreCase))
|
||||
if (requestUrl.Host.OrdinalEquals(cookie.OriginUrl.Host, true))
|
||||
return true;
|
||||
|
||||
reason = $"Cookie set from {cookie.OriginUrl.Host} without Domain specified should only be sent to that specific host, not {requestUrl.Host}.";
|
||||
@ -225,10 +225,10 @@ namespace Flurl.Http
|
||||
if (cookie.Path == "/")
|
||||
return true;
|
||||
|
||||
var cookiePath = (cookie.Path?.StartsWith("/") == true) ? cookie.Path : cookie.OriginUrl.Path;
|
||||
var cookiePath = (cookie.Path?.OrdinalStartsWith("/") == true) ? cookie.Path : cookie.OriginUrl.Path;
|
||||
if (cookiePath == "")
|
||||
cookiePath = "/";
|
||||
else if (cookiePath.Length > 1 && cookiePath.EndsWith("/"))
|
||||
else if (cookiePath.Length > 1 && cookiePath.OrdinalEndsWith("/"))
|
||||
cookiePath = cookiePath.TrimEnd('/');
|
||||
|
||||
if (cookiePath == "/")
|
||||
@ -236,10 +236,10 @@ namespace Flurl.Http
|
||||
|
||||
var requestPath = (requestUrl.Path.Length > 0) ? requestUrl.Path : "/";
|
||||
|
||||
if (requestPath.Equals(cookiePath, StringComparison.Ordinal)) // Path is case-sensitive, unlike Domain
|
||||
if (requestPath.OrdinalEquals(cookiePath)) // Path is case-sensitive, unlike Domain
|
||||
return true;
|
||||
|
||||
if (requestPath.StartsWith(cookiePath, StringComparison.Ordinal) && requestPath[cookiePath.Length] == '/')
|
||||
if (requestPath.OrdinalStartsWith(cookiePath) && requestPath[cookiePath.Length] == '/')
|
||||
return true;
|
||||
|
||||
reason = string.IsNullOrEmpty(cookie.Path) ?
|
||||
@ -258,7 +258,7 @@ namespace Flurl.Http
|
||||
// var line = await reader.ReadLineAsync();
|
||||
// if (line == null) break;
|
||||
// if (line.Trim() == "") continue;
|
||||
// if (line.StartsWith("//")) continue;
|
||||
// if (line.OrdinalStartsWith("//")) continue;
|
||||
// if (line == domain) return true;
|
||||
// }
|
||||
// }
|
||||
|
@ -263,7 +263,7 @@ namespace Flurl.Http
|
||||
|
||||
if (Url.IsValid(location))
|
||||
redir.Url = new Url(location);
|
||||
else if (location.StartsWith("/"))
|
||||
else if (location.OrdinalStartsWith("/"))
|
||||
redir.Url = new Url(this.Url.Root).AppendPathSegment(location);
|
||||
else
|
||||
redir.Url = new Url(this.Url.Root).AppendPathSegments(this.Url.Path, location);
|
||||
|
@ -83,7 +83,7 @@ namespace Flurl.Http
|
||||
break;
|
||||
default:
|
||||
// it's a request/response-level header
|
||||
if (!name.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)) // multiple set-cookie headers are allowed
|
||||
if (!name.OrdinalEquals("Set-Cookie", true)) // multiple set-cookie headers are allowed
|
||||
msg.Headers.Remove(name);
|
||||
if (value != null)
|
||||
msg.Headers.TryAddWithoutValidation(name, new[] { value.ToInvariantString() });
|
||||
|
@ -319,7 +319,7 @@ namespace Flurl.Http.Testing
|
||||
return With(call => {
|
||||
var val = call.Request.Headers.FirstOrDefault("Authorization");
|
||||
if (val == null) return false;
|
||||
if (!val.StartsWith("Basic ")) return false;
|
||||
if (!val.OrdinalStartsWith("Basic ")) return false;
|
||||
if ((username ?? "*") == "*" && (password ?? "*") == "*") return true;
|
||||
var encodedCreds = val.Substring(6);
|
||||
try {
|
||||
|
@ -21,8 +21,8 @@ namespace Flurl.Http.Testing
|
||||
internal static bool HasAnyVerb(this FlurlCall call, string[] verbs) {
|
||||
// for good measure, check both FlurlRequest.Verb and HttpRequestMessage.Method
|
||||
return verbs.Any(verb =>
|
||||
call.Request.Verb.Method.Equals(verb, StringComparison.OrdinalIgnoreCase) &&
|
||||
call.HttpRequestMessage.Method.Method.Equals(verb, StringComparison.OrdinalIgnoreCase));
|
||||
call.Request.Verb.Method.OrdinalEquals(verb, true) &&
|
||||
call.HttpRequestMessage.Method.Method.OrdinalEquals(verb, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,7 +94,7 @@ namespace Flurl.Http.Testing
|
||||
// avoid regex'ing in simple cases
|
||||
if (string.IsNullOrEmpty(pattern) || pattern == "*") return true;
|
||||
if (string.IsNullOrEmpty(textToCheck)) return false;
|
||||
if (!pattern.Contains("*")) return textToCheck == pattern;
|
||||
if (!pattern.OrdinalContains("*")) return textToCheck == pattern;
|
||||
|
||||
var regex = "^" + Regex.Escape(pattern).Replace("\\*", "(.*)") + "$";
|
||||
return Regex.IsMatch(textToCheck ?? "", regex);
|
||||
|
@ -136,7 +136,7 @@ namespace Flurl
|
||||
/// <summary>
|
||||
/// True if Url is absolute and scheme is https or wss.
|
||||
/// </summary>
|
||||
public bool IsSecureScheme => !IsRelative && (Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) || Scheme.Equals("wss", StringComparison.OrdinalIgnoreCase));
|
||||
public bool IsSecureScheme => !IsRelative && (Scheme.OrdinalEquals("https", true) || Scheme.OrdinalEquals("wss", true));
|
||||
#endregion
|
||||
|
||||
#region ctors and parsing methods
|
||||
@ -181,18 +181,18 @@ namespace Flurl
|
||||
_scheme = uri.Scheme;
|
||||
_userInfo = uri.UserInfo;
|
||||
_host = uri.Host;
|
||||
_port = uri.Authority.EndsWith($":{uri.Port}") ? uri.Port : (int?)null; // don't default Port if not included
|
||||
_port = uri.Authority.OrdinalEndsWith($":{uri.Port}") ? uri.Port : (int?)null; // don't default Port if not included
|
||||
_pathSegments = new List<string>();
|
||||
if (uri.AbsolutePath.Length > 0 && uri.AbsolutePath != "/")
|
||||
AppendPathSegment(uri.AbsolutePath);
|
||||
_queryParams = new QueryParamCollection(uri.Query);
|
||||
_fragment = uri.Fragment.TrimStart('#'); // quirk - formal def of fragment does not include the #
|
||||
|
||||
_leadingSlash = uri.OriginalString.StartsWith(Root + "/");
|
||||
_trailingSlash = _pathSegments.Any() && uri.AbsolutePath.EndsWith("/");
|
||||
_leadingSlash = uri.OriginalString.OrdinalStartsWith(Root + "/");
|
||||
_trailingSlash = _pathSegments.Any() && uri.AbsolutePath.OrdinalEndsWith("/");
|
||||
|
||||
// more quirk fixes
|
||||
var hasAuthority = uri.OriginalString.StartsWith($"{Scheme}://");
|
||||
var hasAuthority = uri.OriginalString.OrdinalStartsWith($"{Scheme}://");
|
||||
if (hasAuthority && Authority.Length == 0 && PathSegments.Any()) {
|
||||
// Uri didn't parse Authority when it should have
|
||||
_host = _pathSegments[0];
|
||||
@ -207,11 +207,11 @@ namespace Flurl
|
||||
}
|
||||
}
|
||||
// if it's relative, System.Uri refuses to parse any of it. these hacks will force the matter
|
||||
else if (uri.OriginalString.StartsWith("//")) {
|
||||
else if (uri.OriginalString.OrdinalStartsWith("//")) {
|
||||
ParseInternal(new Uri("http:" + uri.OriginalString));
|
||||
_scheme = "";
|
||||
}
|
||||
else if (uri.OriginalString.StartsWith("/")) {
|
||||
else if (uri.OriginalString.OrdinalStartsWith("/")) {
|
||||
ParseInternal(new Uri("http://temp.com" + uri.OriginalString));
|
||||
_scheme = "";
|
||||
_host = "";
|
||||
@ -276,7 +276,7 @@ namespace Flurl
|
||||
var subpath = segment.ToInvariantString();
|
||||
foreach (var s in ParsePathSegments(subpath))
|
||||
PathSegments.Add(s);
|
||||
_trailingSlash = subpath.EndsWith("/");
|
||||
_trailingSlash = subpath.OrdinalEndsWith("/");
|
||||
}
|
||||
|
||||
_leadingSlash = true;
|
||||
@ -543,7 +543,7 @@ namespace Flurl
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to compare to this instance.</param>
|
||||
/// <returns></returns>
|
||||
public override bool Equals(object obj) => obj is Url url && this.ToString().Equals(url.ToString());
|
||||
public override bool Equals(object obj) => obj is Url url && this.ToString().OrdinalEquals(url.ToString());
|
||||
|
||||
/// <summary>
|
||||
/// Returns the hashcode for this Url.
|
||||
@ -575,9 +575,9 @@ namespace Flurl
|
||||
if (string.IsNullOrEmpty(part))
|
||||
continue;
|
||||
|
||||
if (result.EndsWith("?") || part.StartsWith("?"))
|
||||
if (result.OrdinalEndsWith("?") || part.OrdinalStartsWith("?"))
|
||||
result = CombineEnsureSingleSeparator(result, part, '?');
|
||||
else if (result.EndsWith("#") || part.StartsWith("#"))
|
||||
else if (result.OrdinalEndsWith("#") || part.OrdinalStartsWith("#"))
|
||||
result = CombineEnsureSingleSeparator(result, part, '#');
|
||||
else if (inFragment)
|
||||
result += part;
|
||||
@ -586,11 +586,11 @@ namespace Flurl
|
||||
else
|
||||
result = CombineEnsureSingleSeparator(result, part, '/');
|
||||
|
||||
if (part.Contains("#")) {
|
||||
if (part.OrdinalContains("#")) {
|
||||
inQuery = false;
|
||||
inFragment = true;
|
||||
}
|
||||
else if (!inFragment && part.Contains("?")) {
|
||||
else if (!inFragment && part.OrdinalContains("?")) {
|
||||
inQuery = true;
|
||||
}
|
||||
}
|
||||
@ -655,7 +655,7 @@ namespace Flurl
|
||||
// in that % isn't illegal if it's the start of a %-encoded sequence https://stackoverflow.com/a/47636037/62600
|
||||
|
||||
// no % characters, so avoid the regex overhead
|
||||
if (!s.Contains("%"))
|
||||
if (!s.OrdinalContains("%"))
|
||||
return Uri.EscapeUriString(s);
|
||||
|
||||
// pick out all %-hex-hex matches and avoid double-encoding
|
||||
|
@ -3,9 +3,11 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
[assembly: InternalsVisibleTo("Flurl.Http")]
|
||||
|
||||
namespace Flurl.Util
|
||||
{
|
||||
/// <summary>
|
||||
@ -47,8 +49,20 @@ namespace Flurl.Util
|
||||
obj.ToString();
|
||||
}
|
||||
|
||||
internal static bool OrdinalEquals(this string s, string value, bool ignoreCase = false) =>
|
||||
s != null && s.Equals(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
internal static bool OrdinalContains(this string s, string value, bool ignoreCase = false) =>
|
||||
s != null && s.IndexOf(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal) >= 0;
|
||||
|
||||
internal static bool OrdinalStartsWith(this string s, string value, bool ignoreCase = false) =>
|
||||
s != null && s.StartsWith(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
internal static bool OrdinalEndsWith(this string s, string value, bool ignoreCase = false) =>
|
||||
s != null && s.EndsWith(value, ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Splits at the first occurence of the given separator.
|
||||
/// Splits at the first occurrence of the given separator.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to split.</param>
|
||||
/// <param name="separator">The separator to split on.</param>
|
||||
@ -96,7 +110,7 @@ namespace Flurl.Util
|
||||
name = null;
|
||||
val = null;
|
||||
return
|
||||
item.GetType().Name.Contains("Tuple") &&
|
||||
item.GetType().Name.OrdinalContains("Tuple") &&
|
||||
TryGetProp(item, "Item1", out name) &&
|
||||
TryGetProp(item, "Item2", out val) &&
|
||||
!TryGetProp(item, "Item3", out _);
|
||||
|
Loading…
x
Reference in New Issue
Block a user