#485 ordinal string comparisons everywhere

This commit is contained in:
Todd 2020-09-26 12:24:38 -05:00
parent 7defe0cbb8
commit f3551eace0
7 changed files with 58 additions and 44 deletions

View File

@ -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;
// }
// }

View File

@ -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);

View File

@ -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() });

View File

@ -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 {

View File

@ -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);

View File

@ -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

View File

@ -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 _);