From eeeeb7a75c338b0d994ccd7140d676a612990ebc Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Thu, 19 May 2022 22:21:16 -0700 Subject: [PATCH] Optimize string.EndsWith(char) for const values (#69038) --- .../src/System/RuntimeType.CoreCLR.cs | 2 +- .../Common/src/System/Net/CookieParser.cs | 7 ++- .../src/System/IO/Pipes/PipeStream.Unix.cs | 2 +- .../Net/Http/Headers/CacheControlHeaderValue.cs | 2 +- .../Net/Http/Headers/NameValueHeaderValue.cs | 2 +- .../src/System/Net/Http/HttpContent.cs | 4 +- .../src/System/Net/HttpListener.cs | 2 +- .../src/System/Net/HttpListenerRequest.cs | 2 +- .../src/System/Net/Managed/HttpEndPointListener.cs | 2 +- .../src/System/Net/Mail/Attachment.cs | 2 +- .../src/System/Net/Mail/MailAddress.cs | 4 +- .../System.Net.Primitives/src/System/Net/Cookie.cs | 56 +++++++++++----------- .../src/System/Net/CookieContainer.cs | 2 +- .../src/System/Net/FtpControlStream.cs | 11 ++--- .../System/Environment.GetFolderPathCore.Unix.cs | 6 +-- .../src/System/Globalization/CultureData.Nls.cs | 4 +- .../src/System/Globalization/CultureData.cs | 2 +- .../Globalization/DateTimeFormatInfoScanner.cs | 2 +- .../src/System/Reflection/Module.cs | 2 +- .../src/System/String.Comparison.cs | 20 ++++++++ .../src/System/Xml/XmlBaseWriter.cs | 2 +- src/libraries/System.Private.Uri/src/System/Uri.cs | 2 +- .../src/System/Xml/Core/XmlTextWriter.cs | 2 +- .../src/System/Xml/Core/XmlWellFormedWriter.cs | 2 +- .../src/System/Xml/Dom/XmlEntityReference.cs | 2 +- .../Xml/Serialization/XmlSerializationReader.cs | 10 ++-- .../Serialization/XmlSerializationReaderILGen.cs | 2 +- .../src/System/Xml/ValidateNames.cs | 2 +- .../src/System/Xml/XmlConvert.cs | 12 +++-- .../X509Certificates/OpenSslX509ChainProcessor.cs | 2 +- .../src/System/Web/HttpUtility.cs | 2 +- .../wasm/debugger/DebuggerTestSuite/Inspector.cs | 2 +- 32 files changed, 102 insertions(+), 76 deletions(-) diff --git a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs index ed4c563..9b708c4 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/RuntimeType.CoreCLR.cs @@ -2079,7 +2079,7 @@ namespace System listType = MemberListType.CaseSensitive; } - if (allowPrefixLookup && name.EndsWith("*", StringComparison.Ordinal)) + if (allowPrefixLookup && name.EndsWith('*')) { // We set prefixLookup to true if name ends with a "*". // We will also set listType to All so that all members are included in diff --git a/src/libraries/Common/src/System/Net/CookieParser.cs b/src/libraries/Common/src/System/Net/CookieParser.cs index e50a374..1e0ee27 100644 --- a/src/libraries/Common/src/System/Net/CookieParser.cs +++ b/src/libraries/Common/src/System/Net/CookieParser.cs @@ -855,10 +855,9 @@ namespace System.Net internal static string CheckQuoted(string value) { - if (value.Length < 2 || value[0] != '\"' || value[value.Length - 1] != '\"') - return value; - - return value.Length == 2 ? string.Empty : value.Substring(1, value.Length - 2); + return (value.Length >= 2 && value.StartsWith('\"') && value.EndsWith('\"')) + ? value.Substring(1, value.Length - 2) + : value; } internal bool EndofHeader() diff --git a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs index cefced3..165c978 100644 --- a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs +++ b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeStream.Unix.cs @@ -202,7 +202,7 @@ namespace System.IO.Pipes // cross-platform with Windows (which has only '\' as an invalid char). if (Path.IsPathRooted(pipeName)) { - if (pipeName.IndexOfAny(s_invalidPathNameChars) >= 0 || pipeName[pipeName.Length - 1] == Path.DirectorySeparatorChar) + if (pipeName.IndexOfAny(s_invalidPathNameChars) >= 0 || pipeName.EndsWith(Path.DirectorySeparatorChar)) throw new PlatformNotSupportedException(SR.PlatformNotSupported_InvalidPipeNameChars); // Caller is in full control of file location. diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/CacheControlHeaderValue.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/CacheControlHeaderValue.cs index 8eacd11..a40b6b9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/CacheControlHeaderValue.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/CacheControlHeaderValue.cs @@ -537,7 +537,7 @@ namespace System.Net.Http.Headers // We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we // have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespace. string valueString = nameValue.Value; - if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"')) + if ((valueString.Length < 3) || !valueString.StartsWith('\"') || !valueString.EndsWith('\"')) { return false; } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/NameValueHeaderValue.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/NameValueHeaderValue.cs index 9ddbd0d..9084597 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/NameValueHeaderValue.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/NameValueHeaderValue.cs @@ -358,7 +358,7 @@ namespace System.Net.Http.Headers } // Trailing/leading space are not allowed - if (value[0] == ' ' || value[0] == '\t' || value[^1] == ' ' || value[^1] == '\t') + if (value.StartsWith(' ') || value.StartsWith('\t') || value.EndsWith(' ') || value.EndsWith('\t')) { ThrowFormatException(value); } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs index 9b1b944..8e508e8 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpContent.cs @@ -192,8 +192,8 @@ namespace System.Net.Http { // Remove at most a single set of quotes. if (charset.Length > 2 && - charset[0] == '\"' && - charset[charset.Length - 1] == '\"') + charset.StartsWith('\"') && + charset.EndsWith('\"')) { encoding = Encoding.GetEncoding(charset.Substring(1, charset.Length - 2)); } diff --git a/src/libraries/System.Net.HttpListener/src/System/Net/HttpListener.cs b/src/libraries/System.Net.HttpListener/src/System/Net/HttpListener.cs index 4cb2679..a8f2a68 100644 --- a/src/libraries/System.Net.HttpListener/src/System/Net/HttpListener.cs +++ b/src/libraries/System.Net.HttpListener/src/System/Net/HttpListener.cs @@ -146,7 +146,7 @@ namespace System.Net { throw new ArgumentException(SR.net_listener_host, nameof(uriPrefix)); } - if (uriPrefix[uriPrefix.Length - 1] != '/') + if (!uriPrefix.EndsWith('/')) { throw new ArgumentException(SR.net_listener_slash, nameof(uriPrefix)); } diff --git a/src/libraries/System.Net.HttpListener/src/System/Net/HttpListenerRequest.cs b/src/libraries/System.Net.HttpListener/src/System/Net/HttpListenerRequest.cs index 5bfecb4..b44dc6d 100644 --- a/src/libraries/System.Net.HttpListener/src/System/Net/HttpListenerRequest.cs +++ b/src/libraries/System.Net.HttpListener/src/System/Net/HttpListenerRequest.cs @@ -537,8 +537,8 @@ namespace System.Net internal static void FillFromString(NameValueCollection nvc, string s, bool urlencoded, Encoding encoding) { + int i = s.StartsWith('?') ? 1 : 0; int l = s.Length; - int i = (l > 0 && s[0] == '?') ? 1 : 0; while (i < l) { diff --git a/src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpEndPointListener.cs b/src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpEndPointListener.cs index 351ad0c..6753f34 100644 --- a/src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpEndPointListener.cs +++ b/src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpEndPointListener.cs @@ -178,7 +178,7 @@ namespace System.Net string host = uri.Host; int port = uri.Port; string path = WebUtility.UrlDecode(uri.AbsolutePath); - string pathSlash = path[path.Length - 1] == '/' ? path : path + "/"; + string pathSlash = path.EndsWith('/') ? path : path + "/"; HttpListener? bestMatch = null; int bestLength = -1; diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/Attachment.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/Attachment.cs index aa8430f..ab7c709 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/Attachment.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/Attachment.cs @@ -204,7 +204,7 @@ namespace System.Net.Mail ContentId = cid; return cid; } - if (cid.Length >= 2 && cid[0] == '<' && cid[cid.Length - 1] == '>') + if (cid.StartsWith('<') && cid.EndsWith('>')) { return cid.Substring(1, cid.Length - 2); } diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs index beb6d52..2af110a 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs @@ -141,9 +141,9 @@ namespace System.Net.Mail return false; } - if (displayName.Length >= 2 && displayName[0] == '\"' && displayName[^1] == '\"') + if (displayName.Length >= 2 && displayName.StartsWith('\"') && displayName.EndsWith('\"')) { - // Peal bounding quotes, they'll get re-added later. + // Peel bounding quotes, they'll get re-added later. displayName = displayName.Substring(1, displayName.Length - 2); } } diff --git a/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs b/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs index 01e82b6..ece5a17 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/Cookie.cs @@ -235,7 +235,11 @@ namespace System.Net } internal bool InternalSetName(string? value) { - if (string.IsNullOrEmpty(value) || value[0] == '$' || value.IndexOfAny(ReservedToName) != -1 || value[0] == ' ' || value[value.Length - 1] == ' ') + if (string.IsNullOrEmpty(value) + || value.StartsWith('$') + || value.StartsWith(' ') + || value.EndsWith(' ') + || value.IndexOfAny(ReservedToName) >= 0) { m_name = string.Empty; return false; @@ -339,7 +343,11 @@ namespace System.Net } // Check the name - if (string.IsNullOrEmpty(m_name) || m_name[0] == '$' || m_name.IndexOfAny(ReservedToName) != -1 || m_name[0] == ' ' || m_name[m_name.Length - 1] == ' ') + if (string.IsNullOrEmpty(m_name) || + m_name.StartsWith('$') || + m_name.StartsWith(' ') || + m_name.EndsWith(' ') || + m_name.IndexOfAny(ReservedToName) >= 0) { if (shouldThrow) { @@ -350,7 +358,7 @@ namespace System.Net // Check the value if (m_value == null || - (!(m_value.Length > 2 && m_value[0] == '\"' && m_value[m_value.Length - 1] == '\"') && m_value.IndexOfAny(ReservedToValue) != -1)) + (!(m_value.Length > 2 && m_value.StartsWith('\"') && m_value.EndsWith('\"')) && m_value.IndexOfAny(ReservedToValue) >= 0)) { if (shouldThrow) { @@ -360,8 +368,8 @@ namespace System.Net } // Check Comment syntax - if (Comment != null && !(Comment.Length > 2 && Comment[0] == '\"' && Comment[Comment.Length - 1] == '\"') - && (Comment.IndexOfAny(ReservedToValue) != -1)) + if (Comment != null && !(Comment.Length > 2 && Comment.StartsWith('\"') && Comment.EndsWith('\"')) + && (Comment.IndexOfAny(ReservedToValue) >= 0)) { if (shouldThrow) { @@ -371,8 +379,8 @@ namespace System.Net } // Check Path syntax - if (Path != null && !(Path.Length > 2 && Path[0] == '\"' && Path[Path.Length - 1] == '\"') - && (Path.IndexOfAny(ReservedToValue) != -1)) + if (Path != null && !(Path.Length > 2 && Path.StartsWith('\"') && Path.EndsWith('\"')) + && (Path.IndexOfAny(ReservedToValue) >= 0)) { if (shouldThrow) { @@ -498,7 +506,7 @@ namespace System.Net // Note: Normally Uri.AbsolutePath contains at least one "/" after parsing, // but it's possible construct Uri with an empty path using a custom UriParser int lastSlash; - if (path.Length == 0 || path[0] != '/' || (lastSlash = path.LastIndexOf('/')) == 0) + if (!path.StartsWith('/') || (lastSlash = path.LastIndexOf('/')) == 0) { m_path = "/"; break; @@ -587,35 +595,29 @@ namespace System.Net else { // Parse port list - if (value[0] != '\"' || value[value.Length - 1] != '\"') + if (!value.StartsWith('\"') || !value.EndsWith('\"')) { throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value)); } - string[] ports = value.Split(PortSplitDelimiters); - - List portList = new List(); - int port; + string[] ports = value.Split(PortSplitDelimiters, StringSplitOptions.RemoveEmptyEntries); + int[] parsedPorts = new int[ports.Length]; for (int i = 0; i < ports.Length; ++i) { - // Skip spaces - if (ports[i] != string.Empty) + if (!int.TryParse(ports[i], out int port)) { - if (!int.TryParse(ports[i], out port)) - { - throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value)); - } - - // valid values for port 0 - 0xFFFF - if ((port < 0) || (port > 0xFFFF)) - { - throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value)); - } + throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value)); + } - portList.Add(port); + // valid values for port 0 - 0xFFFF + if ((port < 0) || (port > 0xFFFF)) + { + throw new CookieException(SR.Format(SR.net_cookie_attribute, CookieFields.PortAttributeName, value)); } + + parsedPorts[i] = port; } - m_port_list = portList.ToArray(); + m_port_list = parsedPorts; m_port = value; m_version = MaxSupportedVersion; m_cookieVariant = CookieVariant.Rfc2965; diff --git a/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs b/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs index 3825092..86187da 100644 --- a/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs +++ b/src/libraries/System.Net.Primitives/src/System/Net/CookieContainer.cs @@ -927,7 +927,7 @@ namespace System.Net if (!requestPath.StartsWith(cookiePath, StringComparison.Ordinal)) return false; return requestPath.Length == cookiePath.Length || - cookiePath.Length > 0 && cookiePath[^1] == '/' || + cookiePath.EndsWith('/') || requestPath[cookiePath.Length] == '/'; } diff --git a/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs b/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs index 5ff4ba7..a093ef4 100644 --- a/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs +++ b/src/libraries/System.Net.Requests/src/System/Net/FtpControlStream.cs @@ -602,8 +602,7 @@ namespace System.Net commandList.Add(new PipelineEntry(FormatFtpCommand("RNFR", baseDir + requestFilename), flags)); string renameTo; - if (!string.IsNullOrEmpty(request.RenameTo) - && request.RenameTo.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + if (request.RenameTo is not null && request.RenameTo.StartsWith('/')) { renameTo = request.RenameTo; // Absolute path } @@ -774,7 +773,7 @@ namespace System.Net } // strip off trailing '/' on directory if present - if (directory.Length > 1 && directory[directory.Length - 1] == '/') + if (directory.Length > 1 && directory.EndsWith('/')) directory = directory.Substring(0, directory.Length - 1); } @@ -954,11 +953,11 @@ namespace System.Net escapedFilename = escapedFilename.Replace("#", "%23"); // help us out if the user forgot to add a slash to the directory name - string orginalPath = baseUri.AbsolutePath; - if (orginalPath.Length > 0 && orginalPath[orginalPath.Length - 1] != '/') + string originalPath = baseUri.AbsolutePath; + if (originalPath.Length > 0 && !originalPath.EndsWith('/')) { UriBuilder uriBuilder = new UriBuilder(baseUri); - uriBuilder.Path = orginalPath + "/"; + uriBuilder.Path = originalPath + "/"; baseUri = uriBuilder.Uri; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs index 27dbc32..7990feb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Environment.GetFolderPathCore.Unix.cs @@ -93,7 +93,7 @@ namespace System // "$XDG_DATA_HOME defines the base directory relative to which user specific data files should be stored." // "If $XDG_DATA_HOME is either not set or empty, a default equal to $HOME/.local/share should be used." string? data = GetEnvironmentVariable("XDG_DATA_HOME"); - if (string.IsNullOrEmpty(data) || data[0] != '/') + if (data is null || !data.StartsWith('/')) { data = Path.Combine(home, ".local", "share"); } @@ -137,7 +137,7 @@ namespace System // "$XDG_CONFIG_HOME defines the base directory relative to which user specific configuration files should be stored." // "If $XDG_CONFIG_HOME is either not set or empty, a default equal to $HOME/.config should be used." string? config = GetEnvironmentVariable("XDG_CONFIG_HOME"); - if (string.IsNullOrEmpty(config) || config[0] != '/') + if (config is null || !config.StartsWith('/')) { config = Path.Combine(home, ".config"); } @@ -151,7 +151,7 @@ namespace System Debug.Assert(!string.IsNullOrEmpty(fallback), $"Expected non-empty fallback"); string? envPath = GetEnvironmentVariable(key); - if (!string.IsNullOrEmpty(envPath) && envPath[0] == '/') + if (envPath is not null && envPath.StartsWith('/')) { return envPath; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Nls.cs index 9614d22..496eb65 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Nls.cs @@ -290,14 +290,14 @@ namespace System.Globalization return new int[] { 3 }; } - if (win32Str[0] == '0') + if (win32Str.StartsWith('0')) { return new int[] { 0 }; } // Since its in n;n;n;n;n format, we can always get the length quickly int[] values; - if (win32Str[^1] == '0') + if (win32Str.EndsWith('0')) { // Trailing 0 gets dropped. 1;0 -> 1 values = new int[win32Str.Length / 2]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index dd5e58b..8212dee 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -999,7 +999,7 @@ namespace System.Globalization // Our existing names mostly look like: // "English" + "United States" -> "English (United States)" // "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)" - if (EnglishLanguageName[^1] == ')') + if (EnglishLanguageName.EndsWith(')')) { // "Azeri (Latin)" + "Azerbaijan" -> "Azeri (Latin, Azerbaijan)" englishDisplayName = string.Concat( diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfoScanner.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfoScanner.cs index 463a5da..34ba2a9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfoScanner.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormatInfoScanner.cs @@ -228,7 +228,7 @@ namespace System.Globalization m_dateWords.Add(str); } - if (str[^1] == '.') + if (str.EndsWith('.')) { // Old version ignore the trailing dot in the date words. Support this as well. string strWithoutDot = str[0..^1]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Reflection/Module.cs b/src/libraries/System.Private.CoreLib/src/System/Reflection/Module.cs index 08b077c..e3fe7fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Reflection/Module.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Reflection/Module.cs @@ -182,7 +182,7 @@ namespace System.Reflection throw new InvalidFilterCriteriaException(SR.InvalidFilterCriteriaException_CritString); } // Check to see if this is a prefix or exact match requirement - if (str.Length > 0 && str[^1] == '*') + if (str.EndsWith('*')) { ReadOnlySpan slice = str.AsSpan(0, str.Length - 1); return cls.Name.AsSpan().StartsWith(slice, comparison); diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs index c70e9e5..76d8ac6 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs @@ -594,6 +594,20 @@ namespace System public bool EndsWith(char value) { + // If the string is empty, *(&_firstChar + length - 1) will deref within + // the _stringLength field, which will be all-zero. We must forbid '\0' + // from going down the optimized code path because otherwise empty strings + // would appear to end with '\0', which is incorrect. + // n.b. This optimization relies on the layout of string and is not valid + // for other data types like char[] or Span. + if (RuntimeHelpers.IsKnownConstant(value) && value != '\0') + { + // deref Length now to front-load the null check; also take this time to zero-extend + // n.b. (localLength - 1) could be negative! + nuint localLength = (uint)Length; + return Unsafe.Add(ref _firstChar, (nint)localLength - 1) == value; + } + int lastPos = Length - 1; return ((uint)lastPos < (uint)Length) && this[lastPos] == value; } @@ -1004,10 +1018,16 @@ namespace System public bool StartsWith(char value) { + // If the string is empty, _firstChar will contain the null terminator. + // We forbid '\0' from going down the optimized code path because otherwise + // empty strings would appear to begin with '\0', which is incorrect. + // n.b. This optimization relies on the layout of string and is not valid + // for other data types like char[] or Span. if (RuntimeHelpers.IsKnownConstant(value) && value != '\0') { return _firstChar == value; } + return Length != 0 && _firstChar == value; } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseWriter.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseWriter.cs index 8ae2896..3d9e7c9 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseWriter.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseWriter.cs @@ -442,7 +442,7 @@ namespace System.Xml { text = string.Empty; } - else if (text.IndexOf("--", StringComparison.Ordinal) != -1 || (text.Length > 0 && text[text.Length - 1] == '-')) + else if (text.Contains("--") || text.StartsWith('-')) { throw System.Runtime.Serialization.DiagnosticUtility.ExceptionUtility.ThrowHelperError(new ArgumentException(SR.XmlInvalidCommentChars, nameof(text))); } diff --git a/src/libraries/System.Private.Uri/src/System/Uri.cs b/src/libraries/System.Private.Uri/src/System/Uri.cs index 655da9b..ad28303 100644 --- a/src/libraries/System.Private.Uri/src/System/Uri.cs +++ b/src/libraries/System.Private.Uri/src/System/Uri.cs @@ -1279,7 +1279,7 @@ namespace System { fixed (char* fixedName = name) { - if (name[0] == '[' && name[name.Length - 1] == ']') + if (name.StartsWith('[') && name.EndsWith(']')) { // we require that _entire_ name is recognized as ipv6 address if (IPv6AddressHelper.IsValid(fixedName, 1, ref end) && end == name.Length) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextWriter.cs b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextWriter.cs index 08ec978..6a6a623 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextWriter.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextWriter.cs @@ -763,7 +763,7 @@ namespace System.Xml { try { - if (null != text && (text.Contains("--") || (text.Length != 0 && text[text.Length - 1] == '-'))) + if (null != text && (text.Contains("--") || text.StartsWith('-'))) { throw new ArgumentException(SR.Xml_InvalidCommentChars); } diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlWellFormedWriter.cs b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlWellFormedWriter.cs index b62d6c6..5a44dde 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlWellFormedWriter.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlWellFormedWriter.cs @@ -1795,7 +1795,7 @@ namespace System.Xml { throw new ArgumentException(SR.Format(SR.Xml_NamespaceDeclXmlXmlns, prefix)); } - if (prefix.Length > 0 && prefix[0] == 'x') + if (prefix.StartsWith('x')) { if (prefix == "xml") { diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Dom/XmlEntityReference.cs b/src/libraries/System.Private.Xml/src/System/Xml/Dom/XmlEntityReference.cs index d4b0bb6..c88f12f 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Dom/XmlEntityReference.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Dom/XmlEntityReference.cs @@ -36,7 +36,7 @@ namespace System.Xml { if (!doc.IsLoading) { - if (name.Length > 0 && name[0] == '#') + if (name.StartsWith('#')) { throw new ArgumentException(SR.Xdom_InvalidCharacter_EntityReference); } diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs index a578730..11480e1 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs @@ -3940,12 +3940,12 @@ namespace System.Xml.Serialization else { bool useReflection = typeDesc.UseReflection; - if (member.Source[member.Source.Length - 1] == '(' || member.Source[member.Source.Length - 1] == '{') + if (member.Source.EndsWith('(') || member.Source.EndsWith('{')) { WriteCreateInstance(typeDescFullName, a, useReflection, typeDesc.CannotNew); Writer.Write(member.Source); Writer.Write(a); - if (member.Source[member.Source.Length - 1] == '{') + if (member.Source.EndsWith('{')) Writer.WriteLine("});"); else Writer.WriteLine(");"); @@ -4511,7 +4511,7 @@ namespace System.Xml.Serialization private void WriteSourceBegin(string source) { Writer.Write(source); - if (source[source.Length - 1] != '(' && source[source.Length - 1] != '{') + if (!source.EndsWith('(') && !source.EndsWith('{')) Writer.Write(" = "); } @@ -4519,9 +4519,9 @@ namespace System.Xml.Serialization { // source could be of the form "var", "arrayVar[i]", // "collection.Add(" or "methodInfo.Invoke(collection, new object[] {" - if (source[source.Length - 1] == '(') + if (source.EndsWith('(')) Writer.Write(")"); - else if (source[source.Length - 1] == '{') + else if (source.EndsWith('{')) Writer.Write("})"); } diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs index 7936339..d2cca45 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs @@ -2186,7 +2186,7 @@ namespace System.Xml.Serialization } else { - if (member.Source[member.Source.Length - 1] == '(' || member.Source[member.Source.Length - 1] == '{') + if (member.Source.EndsWith('(') || member.Source.EndsWith('{')) { WriteCreateInstance(a, typeDesc.CannotNew, typeDesc.Type!); WriteSourceBegin(member.Source); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/ValidateNames.cs b/src/libraries/System.Private.Xml/src/System/Xml/ValidateNames.cs index 2e73ecbe..007d236 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/ValidateNames.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/ValidateNames.cs @@ -306,7 +306,7 @@ namespace System.Xml { int len, lenLocal, offset; - if (s.Length != 0 && s[0] == '*') + if (s.StartsWith('*')) { // '*' as a NameTest prefix = localName = null; diff --git a/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs b/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs index 68e658f..6b51af6 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs @@ -429,7 +429,10 @@ namespace System.Xml return token; } - if (token[0] == ' ' || token[token.Length - 1] == ' ' || token.IndexOfAny(crt) != -1 || token.IndexOf(" ", StringComparison.Ordinal) != -1) + if (token.StartsWith(' ') || + token.EndsWith(' ') || + token.IndexOfAny(crt) >= 0 || + token.Contains(" ")) { throw new XmlException(SR.Sch_NotTokenString, token); } @@ -438,12 +441,15 @@ namespace System.Xml internal static Exception? TryVerifyTOKEN(string token) { - if (token == null || token.Length == 0) + if (string.IsNullOrEmpty(token)) { return null; } - if (token[0] == ' ' || token[token.Length - 1] == ' ' || token.IndexOfAny(crt) != -1 || token.IndexOf(" ", StringComparison.Ordinal) != -1) + if (token.StartsWith(' ') || + token.EndsWith(' ') || + token.IndexOfAny(crt) >= 0 || + token.Contains(" ")) { return new XmlException(SR.Sch_NotTokenString, token); } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs index 906f1ed..ecb7808 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509ChainProcessor.cs @@ -817,7 +817,7 @@ namespace System.Security.Cryptography.X509Certificates int count = baseUri.Length + resource.Length; - if (baseUri[baseUri.Length - 1] == '/') + if (baseUri.EndsWith('/')) { return string.Create( count, diff --git a/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs b/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs index ddb3a33..0d8cc4a 100644 --- a/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs +++ b/src/libraries/System.Web.HttpUtility/src/System/Web/HttpUtility.cs @@ -87,7 +87,7 @@ namespace System.Web HttpQSCollection result = new HttpQSCollection(); int queryLength = query.Length; - int namePos = queryLength > 0 && query[0] == '?' ? 1 : 0; + int namePos = query.StartsWith('?') ? 1 : 0; if (queryLength == namePos) { return result; diff --git a/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs b/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs index aecc48b..d0c0284 100644 --- a/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs +++ b/src/mono/wasm/debugger/DebuggerTestSuite/Inspector.cs @@ -176,7 +176,7 @@ namespace DebuggerTests } else { - if (output.Length > 0 && output[^1] == '\n') + if (output.EndsWith('\n')) output = output[..^1]; } -- 2.7.4