diff --git a/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs b/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs index 76770428a0..da9188dad3 100644 --- a/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs +++ b/src/Microsoft.AspNetCore.Http.Extensions/HttpRequestMultipartExtensions.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Http.Extensions { return string.Empty; } - return HeaderUtilities.RemoveQuotes(mediaType.Boundary); + return HeaderUtilities.RemoveQuotes(mediaType.Boundary).ToString(); } } } diff --git a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs index cd8b491ffd..f091e3b166 100644 --- a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs +++ b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Http.Features @@ -293,14 +294,14 @@ namespace Microsoft.AspNetCore.Http.Features { // Content-Disposition: form-data; name="key"; return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") - && string.IsNullOrEmpty(contentDisposition.FileName) && string.IsNullOrEmpty(contentDisposition.FileNameStar); + && StringSegment.IsNullOrEmpty(contentDisposition.FileName) && StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar); } private bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition) { // Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg" return contentDisposition != null && contentDisposition.DispositionType.Equals("form-data") - && (!string.IsNullOrEmpty(contentDisposition.FileName) || !string.IsNullOrEmpty(contentDisposition.FileNameStar)); + && (!StringSegment.IsNullOrEmpty(contentDisposition.FileName) || !StringSegment.IsNullOrEmpty(contentDisposition.FileNameStar)); } // Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq" @@ -308,7 +309,7 @@ namespace Microsoft.AspNetCore.Http.Features private static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit) { var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary); - if (string.IsNullOrWhiteSpace(boundary)) + if (StringSegment.IsNullOrEmpty(boundary)) { throw new InvalidDataException("Missing content-type boundary."); } @@ -316,7 +317,7 @@ namespace Microsoft.AspNetCore.Http.Features { throw new InvalidDataException($"Multipart boundary length limit {lengthLimit} exceeded."); } - return boundary; + return boundary.ToString(); } } } diff --git a/src/Microsoft.AspNetCore.Http/Internal/RequestCookieCollection.cs b/src/Microsoft.AspNetCore.Http/Internal/RequestCookieCollection.cs index d02c9fade5..ca85fd1d04 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/RequestCookieCollection.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/RequestCookieCollection.cs @@ -75,8 +75,8 @@ namespace Microsoft.AspNetCore.Http.Internal for (var i = 0; i < cookies.Count; i++) { var cookie = cookies[i]; - var name = Uri.UnescapeDataString(cookie.Name); - var value = Uri.UnescapeDataString(cookie.Value); + var name = Uri.UnescapeDataString(cookie.Name.Value); + var value = Uri.UnescapeDataString(cookie.Value.Value); store[name] = value; } diff --git a/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs b/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs index b1ba2ff47e..70d7741f64 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FileMultipartSection.cs @@ -39,11 +39,11 @@ namespace Microsoft.AspNetCore.WebUtilities Section = section; _contentDispositionHeader = header; - Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name) ?? string.Empty; + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); FileName = HeaderUtilities.RemoveQuotes( - _contentDispositionHeader.FileNameStar ?? - _contentDispositionHeader.FileName ?? - string.Empty); + _contentDispositionHeader.FileNameStar.HasValue ? + _contentDispositionHeader.FileNameStar : + _contentDispositionHeader.FileName).ToString(); } /// diff --git a/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs b/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs index 652cbdeb12..01af0455b8 100644 --- a/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs +++ b/src/Microsoft.AspNetCore.WebUtilities/FormMultipartSection.cs @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.WebUtilities Section = section; _contentDispositionHeader = header; - Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name); + Name = HeaderUtilities.RemoveQuotes(_contentDispositionHeader.Name).ToString(); } /// diff --git a/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs b/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs index 1b4038d21b..f3caaafb70 100644 --- a/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs +++ b/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using Microsoft.Extensions.Primitives; + namespace Microsoft.Net.Http.Headers { internal abstract class BaseHeaderParser : HttpHeaderParser @@ -10,9 +12,9 @@ namespace Microsoft.Net.Http.Headers { } - protected abstract int GetParsedValueLength(string value, int startIndex, out T parsedValue); + protected abstract int GetParsedValueLength(StringSegment value, int startIndex, out T parsedValue); - public sealed override bool TryParseValue(string value, ref int index, out T parsedValue) + public sealed override bool TryParseValue(StringSegment value, ref int index, out T parsedValue) { parsedValue = default(T); @@ -21,7 +23,7 @@ namespace Microsoft.Net.Http.Headers // Accept: text/xml; q=1 // Accept: // Accept: text/plain; q=0.2 - if (string.IsNullOrEmpty(value) || (index == value.Length)) + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) { return SupportsMultipleValues; } diff --git a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs index af31316667..81e18faf47 100644 --- a/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/CacheControlHeaderValue.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -32,10 +33,10 @@ namespace Microsoft.Net.Http.Headers private static readonly HttpHeaderParser Parser = new GenericHeaderParser(true, GetCacheControlLength); - private static readonly Action CheckIsValidTokenAction = CheckIsValidToken; + private static readonly Action CheckIsValidTokenAction = CheckIsValidToken; private bool _noCache; - private ICollection _noCacheHeaders; + private ICollection _noCacheHeaders; private bool _noStore; private TimeSpan? _maxAge; private TimeSpan? _sharedMaxAge; @@ -46,7 +47,7 @@ namespace Microsoft.Net.Http.Headers private bool _onlyIfCached; private bool _public; private bool _private; - private ICollection _privateHeaders; + private ICollection _privateHeaders; private bool _mustRevalidate; private bool _proxyRevalidate; private IList _extensions; @@ -62,13 +63,13 @@ namespace Microsoft.Net.Http.Headers set { _noCache = value; } } - public ICollection NoCacheHeaders + public ICollection NoCacheHeaders { get { if (_noCacheHeaders == null) { - _noCacheHeaders = new ObjectCollection(CheckIsValidTokenAction); + _noCacheHeaders = new ObjectCollection(CheckIsValidTokenAction); } return _noCacheHeaders; } @@ -134,13 +135,13 @@ namespace Microsoft.Net.Http.Headers set { _private = value; } } - public ICollection PrivateHeaders + public ICollection PrivateHeaders { get { if (_privateHeaders == null) { - _privateHeaders = new ObjectCollection(CheckIsValidTokenAction); + _privateHeaders = new ObjectCollection(CheckIsValidTokenAction); } return _privateHeaders; } @@ -259,13 +260,13 @@ namespace Microsoft.Net.Http.Headers } if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders, - StringComparer.OrdinalIgnoreCase)) + StringSegmentComparer.OrdinalIgnoreCase)) { return false; } if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders, - StringComparer.OrdinalIgnoreCase)) + StringSegmentComparer.OrdinalIgnoreCase)) { return false; } @@ -299,7 +300,7 @@ namespace Microsoft.Net.Http.Headers { foreach (var noCacheHeader in _noCacheHeaders) { - result = result ^ StringComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader); + result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader); } } @@ -307,7 +308,7 @@ namespace Microsoft.Net.Http.Headers { foreach (var privateHeader in _privateHeaders) { - result = result ^ StringComparer.OrdinalIgnoreCase.GetHashCode(privateHeader); + result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(privateHeader); } } @@ -322,7 +323,7 @@ namespace Microsoft.Net.Http.Headers return result; } - public static CacheControlHeaderValue Parse(string input) + public static CacheControlHeaderValue Parse(StringSegment input) { int index = 0; // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. @@ -334,7 +335,7 @@ namespace Microsoft.Net.Http.Headers return result; } - public static bool TryParse(string input, out CacheControlHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out CacheControlHeaderValue parsedValue) { int index = 0; // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null. @@ -346,13 +347,13 @@ namespace Microsoft.Net.Http.Headers return false; } - private static int GetCacheControlLength(string input, int startIndex, out CacheControlHeaderValue parsedValue) + private static int GetCacheControlLength(StringSegment input, int startIndex, out CacheControlHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -403,7 +404,7 @@ namespace Microsoft.Net.Http.Headers switch (name.Length) { case 6: - if (string.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTokenOnlyValue(nameValue, ref cc._public); } @@ -414,11 +415,11 @@ namespace Microsoft.Net.Http.Headers break; case 7: - if (string.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTimeSpan(nameValue, ref cc._maxAge); } - else if(string.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase)) + else if(StringSegment.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders); } @@ -429,15 +430,15 @@ namespace Microsoft.Net.Http.Headers break; case 8: - if (string.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders); } - else if (string.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTokenOnlyValue(nameValue, ref cc._noStore); } - else if (string.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge); } @@ -448,7 +449,7 @@ namespace Microsoft.Net.Http.Headers break; case 9: - if (string.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase)) { success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit)); if (success) @@ -456,7 +457,7 @@ namespace Microsoft.Net.Http.Headers cc._maxStale = true; } } - else if (string.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTimeSpan(nameValue, ref cc._minFresh); } @@ -467,7 +468,7 @@ namespace Microsoft.Net.Http.Headers break; case 12: - if (string.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform); } @@ -478,7 +479,7 @@ namespace Microsoft.Net.Http.Headers break; case 14: - if (string.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached); } @@ -489,7 +490,7 @@ namespace Microsoft.Net.Http.Headers break; case 15: - if (string.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate); } @@ -500,7 +501,7 @@ namespace Microsoft.Net.Http.Headers break; case 16: - if (string.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase)) { success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate); } @@ -538,7 +539,7 @@ namespace Microsoft.Net.Http.Headers private static bool TrySetOptionalTokenList( NameValueHeaderValue nameValue, ref bool boolField, - ref ICollection destination) + ref ICollection destination) { Contract.Requires(nameValue != null); @@ -582,10 +583,10 @@ namespace Microsoft.Net.Http.Headers if (destination == null) { - destination = new ObjectCollection(CheckIsValidTokenAction); + destination = new ObjectCollection(CheckIsValidTokenAction); } - destination.Add(valueString.Substring(current, tokenLength)); + destination.Add(valueString.Subsegment(current, tokenLength)); current = current + tokenLength; } @@ -637,10 +638,10 @@ namespace Microsoft.Net.Http.Headers sb.Append(value); } - private static void AppendValues(StringBuilder sb, IEnumerable values) + private static void AppendValues(StringBuilder sb, IEnumerable values) { var first = true; - foreach (string value in values) + foreach (var value in values) { if (first) { @@ -655,7 +656,7 @@ namespace Microsoft.Net.Http.Headers } } - private static void CheckIsValidToken(string item) + private static void CheckIsValidToken(StringSegment item) { HeaderUtilities.CheckValidToken(item, nameof(item)); } diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs index fbae25b6d1..ff9faf066b 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValue.cs @@ -6,7 +6,9 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; +using System.Linq; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -20,26 +22,28 @@ namespace Microsoft.Net.Http.Headers private const string ModificationDateString = "modification-date"; private const string ReadDateString = "read-date"; private const string SizeString = "size"; + private static readonly char[] QuestionMark = new char[] { '?' }; + private static readonly char[] SingleQuote = new char[] { '\'' }; private static readonly HttpHeaderParser Parser = new GenericHeaderParser(false, GetDispositionTypeLength); // Use list instead of dictionary since we may have multiple parameters with the same name. private ObjectCollection _parameters; - private string _dispositionType; + private StringSegment _dispositionType; private ContentDispositionHeaderValue() { // Used by the parser to create a new instance of this type. } - public ContentDispositionHeaderValue(string dispositionType) + public ContentDispositionHeaderValue(StringSegment dispositionType) { CheckDispositionTypeFormat(dispositionType, "dispositionType"); _dispositionType = dispositionType; } - public string DispositionType + public StringSegment DispositionType { get { return _dispositionType; } set @@ -63,19 +67,19 @@ namespace Microsoft.Net.Http.Headers // Helpers to access specific parameters in the list - public string Name + public StringSegment Name { get { return GetName(NameString); } set { SetName(NameString, value); } } - public string FileName + public StringSegment FileName { get { return GetName(FileNameString); } set { SetName(FileNameString, value); } } - public string FileNameStar + public StringSegment FileNameStar { get { return GetName(FileNameStarString); } set { SetName(FileNameStarString, value); } @@ -146,9 +150,9 @@ namespace Microsoft.Net.Http.Headers /// Sets both FileName and FileNameStar using encodings appropriate for HTTP headers. /// /// - public void SetHttpFileName(string fileName) + public void SetHttpFileName(StringSegment fileName) { - if (!string.IsNullOrEmpty(fileName)) + if (!StringSegment.IsNullOrEmpty(fileName)) { FileName = Sanatize(fileName); } @@ -164,7 +168,7 @@ namespace Microsoft.Net.Http.Headers /// The FileNameStar paraemter is removed. /// /// - public void SetMimeFileName(string fileName) + public void SetMimeFileName(StringSegment fileName) { FileNameStar = null; FileName = fileName; @@ -184,42 +188,41 @@ namespace Microsoft.Net.Http.Headers return false; } - return (string.Compare(_dispositionType, other._dispositionType, StringComparison.OrdinalIgnoreCase) == 0) && + return _dispositionType.Equals(other._dispositionType, StringComparison.OrdinalIgnoreCase) && HeaderUtilities.AreEqualCollections(_parameters, other._parameters); } public override int GetHashCode() { // The dispositionType string is case-insensitive. - return StringComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters); + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_dispositionType) ^ NameValueHeaderValue.GetHashCode(_parameters); } - public static ContentDispositionHeaderValue Parse(string input) + public static ContentDispositionHeaderValue Parse(StringSegment input) { var index = 0; return Parser.ParseValue(input, ref index); } - public static bool TryParse(string input, out ContentDispositionHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out ContentDispositionHeaderValue parsedValue) { var index = 0; return Parser.TryParseValue(input, ref index, out parsedValue); } - private static int GetDispositionTypeLength(string input, int startIndex, out ContentDispositionHeaderValue parsedValue) + private static int GetDispositionTypeLength(StringSegment input, int startIndex, out ContentDispositionHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } // Caller must remove leading whitespaces. If not, we'll return 0. - string dispositionType = null; - var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out dispositionType); + var dispositionTypeLength = GetDispositionTypeExpressionLength(input, startIndex, out var dispositionType); if (dispositionTypeLength == 0) { @@ -247,7 +250,7 @@ namespace Microsoft.Net.Http.Headers return current - startIndex; } - private static int GetDispositionTypeExpressionLength(string input, int startIndex, out string dispositionType) + private static int GetDispositionTypeExpressionLength(StringSegment input, int startIndex, out StringSegment dispositionType) { Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length)); @@ -263,20 +266,19 @@ namespace Microsoft.Net.Http.Headers return 0; } - dispositionType = input.Substring(startIndex, typeLength); + dispositionType = input.Subsegment(startIndex, typeLength); return typeLength; } - private static void CheckDispositionTypeFormat(string dispositionType, string parameterName) + private static void CheckDispositionTypeFormat(StringSegment dispositionType, string parameterName) { - if (string.IsNullOrEmpty(dispositionType)) + if (StringSegment.IsNullOrEmpty(dispositionType)) { throw new ArgumentException("An empty string is not allowed.", parameterName); } // When adding values using strongly typed objects, no leading/trailing LWS (whitespaces) are allowed. - string tempDispositionType; - var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out tempDispositionType); + var dispositionTypeLength = GetDispositionTypeExpressionLength(dispositionType, 0, out var tempDispositionType); if ((dispositionTypeLength == 0) || (tempDispositionType.Length != dispositionType.Length)) { throw new FormatException(string.Format(CultureInfo.InvariantCulture, @@ -291,11 +293,11 @@ namespace Microsoft.Net.Http.Headers var dateParameter = NameValueHeaderValue.Find(_parameters, parameter); if (dateParameter != null) { - string dateString = dateParameter.Value; + var dateString = dateParameter.Value; // Should have quotes, remove them. if (IsQuoted(dateString)) { - dateString = dateString.Substring(1, dateString.Length - 2); + dateString = dateString.Subsegment(1, dateString.Length - 2); } DateTimeOffset date; if (HttpRuleParser.TryStringToDate(dateString, out date)) @@ -357,17 +359,17 @@ namespace Microsoft.Net.Http.Headers return result; } // May not have been encoded - return nameParameter.Value; + return nameParameter.Value.ToString(); } return null; } // Add/update the given parameter in the list, encoding if necessary. // Remove if value is null/Empty - private void SetName(string parameter, string value) + private void SetName(StringSegment parameter, StringSegment value) { var nameParameter = NameValueHeaderValue.Find(_parameters, parameter); - if (string.IsNullOrEmpty(value)) + if (StringSegment.IsNullOrEmpty(value)) { // Remove parameter if (nameParameter != null) @@ -377,7 +379,7 @@ namespace Microsoft.Net.Http.Headers } else { - var processedValue = string.Empty; + var processedValue = StringSegment.Empty; if (parameter.EndsWith("*", StringComparison.Ordinal)) { processedValue = Encode5987(value); @@ -399,14 +401,14 @@ namespace Microsoft.Net.Http.Headers } // Returns input for decoding failures, as the content might not be encoded - private string EncodeAndQuoteMime(string input) + private StringSegment EncodeAndQuoteMime(StringSegment input) { var result = input; var needsQuotes = false; // Remove bounding quotes, they'll get re-added later if (IsQuoted(result)) { - result = result.Substring(1, result.Length - 2); + result = result.Subsegment(1, result.Length - 2); needsQuotes = true; } @@ -423,8 +425,7 @@ namespace Microsoft.Net.Http.Headers if (needsQuotes) { // '\' and '"' must be escaped in a quoted string - result = result.Replace(@"\", @"\\"); - result = result.Replace(@"""", @"\"""); + result = result.ToString().Replace(@"\", @"\\").Replace(@"""", @"\"""); // Re-add quotes "value" result = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", result); } @@ -432,7 +433,7 @@ namespace Microsoft.Net.Http.Headers } // Replaces characters not suitable for HTTP headers with '_' rather than MIME encoding them. - private string Sanatize(string input) + private StringSegment Sanatize(StringSegment input) { var result = input; @@ -455,7 +456,7 @@ namespace Microsoft.Net.Http.Headers } // Returns true if the value starts and ends with a quote - private bool IsQuoted(string value) + private bool IsQuoted(StringSegment value) { Contract.Assert(value != null); @@ -464,13 +465,13 @@ namespace Microsoft.Net.Http.Headers } // tspecials are required to be in a quoted string. Only non-ascii needs to be encoded. - private bool RequiresEncoding(string input) + private bool RequiresEncoding(StringSegment input) { Contract.Assert(input != null); - foreach (char c in input) + for (int i = 0; i < input.Length; i++) { - if ((int)c > 0x7f) + if ((int)input[i] > 0x7f) { return true; } @@ -479,15 +480,23 @@ namespace Microsoft.Net.Http.Headers } // Encode using MIME encoding - private string EncodeMime(string input) + private unsafe string EncodeMime(StringSegment input) { - var buffer = Encoding.UTF8.GetBytes(input); - var encodedName = Convert.ToBase64String(buffer); - return string.Format(CultureInfo.InvariantCulture, "=?utf-8?B?{0}?=", encodedName); + fixed (char* chars = input.Buffer) + { + var byteCount = Encoding.UTF8.GetByteCount(chars + input.Offset, input.Length); + var buffer = new byte[byteCount]; + fixed (byte* bytes = buffer) + { + Encoding.UTF8.GetBytes(chars + input.Offset, input.Length, bytes, byteCount); + } + var encodedName = Convert.ToBase64String(buffer); + return "=?utf-8?B?" + encodedName + "?="; + } } // Attempt to decode MIME encoded strings - private bool TryDecodeMime(string input, out string output) + private bool TryDecodeMime(StringSegment input, out string output) { Contract.Assert(input != null); @@ -498,9 +507,11 @@ namespace Microsoft.Net.Http.Headers { return false; } - var parts = processedInput.Split('?'); + + var parts = processedInput.Split(QuestionMark).ToArray(); // "=, encodingName, encodingType, encodedData, =" - if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" || parts[2].ToLowerInvariant() != "b") + if (parts.Length != 5 || parts[0] != "\"=" || parts[4] != "=\"" + || !parts[2].Equals("b", StringComparison.OrdinalIgnoreCase)) { // Not encoded. // This does not support multi-line encoding. @@ -510,8 +521,8 @@ namespace Microsoft.Net.Http.Headers try { - var encoding = Encoding.GetEncoding(parts[1]); - var bytes = Convert.FromBase64String(parts[3]); + var encoding = Encoding.GetEncoding(parts[1].ToString()); + var bytes = Convert.FromBase64String(parts[3].ToString()); output = encoding.GetString(bytes, 0, bytes.Length); return true; } @@ -528,11 +539,12 @@ namespace Microsoft.Net.Http.Headers // Encode a string using RFC 5987 encoding // encoding'lang'PercentEncodedSpecials - private string Encode5987(string input) + private string Encode5987(StringSegment input) { var builder = new StringBuilder("UTF-8\'\'"); - foreach (char c in input) + for (int i = 0; i < input.Length; i++) { + var c = input[i]; // attr-char = ALPHA / DIGIT / "!" / "#" / "$" / "&" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" // ; token except ( "*" / "'" / "%" ) if (c > 0x7F) // Encodes as multiple utf-8 bytes @@ -569,10 +581,11 @@ namespace Microsoft.Net.Http.Headers // Attempt to decode using RFC 5987 encoding. // encoding'language'my%20string - private bool TryDecode5987(string input, out string output) + private bool TryDecode5987(StringSegment input, out string output) { output = null; - var parts = input.Split('\''); + + var parts = input.Split(SingleQuote).ToArray(); if (parts.Length != 3) { return false; @@ -582,7 +595,7 @@ namespace Microsoft.Net.Http.Headers byte[] unescapedBytes = null; try { - var encoding = Encoding.GetEncoding(parts[0]); + var encoding = Encoding.GetEncoding(parts[0].ToString()); var dataString = parts[2]; unescapedBytes = ArrayPool.Shared.Rent(dataString.Length); @@ -629,7 +642,7 @@ namespace Microsoft.Net.Http.Headers return true; } - private static bool IsHexEncoding(string pattern, int index) + private static bool IsHexEncoding(StringSegment pattern, int index) { if ((pattern.Length - index) < 3) { @@ -661,7 +674,7 @@ namespace Microsoft.Net.Http.Headers return true; } - private static byte HexUnescape(string pattern, ref int index) + private static byte HexUnescape(StringSegment pattern, ref int index) { if ((index < 0) || (index >= pattern.Length)) { diff --git a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs index 0a275e55b8..9ef74baa0c 100644 --- a/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs +++ b/src/Microsoft.Net.Http.Headers/ContentDispositionHeaderValueIdentityExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -23,7 +24,7 @@ namespace Microsoft.Net.Http.Headers } return header.DispositionType.Equals("form-data") - && (!string.IsNullOrEmpty(header.FileName) || !string.IsNullOrEmpty(header.FileNameStar)); + && (!StringSegment.IsNullOrEmpty(header.FileName) || !StringSegment.IsNullOrEmpty(header.FileNameStar)); } /// @@ -39,7 +40,7 @@ namespace Microsoft.Net.Http.Headers } return header.DispositionType.Equals("form-data") - && string.IsNullOrEmpty(header.FileName) && string.IsNullOrEmpty(header.FileNameStar); + && StringSegment.IsNullOrEmpty(header.FileName) && StringSegment.IsNullOrEmpty(header.FileNameStar); } } } diff --git a/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs index c187491dfc..99583cdf47 100644 --- a/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/ContentRangeHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.Contracts; using System.Globalization; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -13,7 +14,7 @@ namespace Microsoft.Net.Http.Headers private static readonly HttpHeaderParser Parser = new GenericHeaderParser(false, GetContentRangeLength); - private string _unit; + private StringSegment _unit; private long? _from; private long? _to; private long? _length; @@ -77,7 +78,7 @@ namespace Microsoft.Net.Http.Headers _unit = HeaderUtilities.BytesUnit; } - public string Unit + public StringSegment Unit { get { return _unit; } set @@ -122,12 +123,12 @@ namespace Microsoft.Net.Http.Headers } return ((_from == other._from) && (_to == other._to) && (_length == other._length) && - (string.Compare(_unit, other._unit, StringComparison.OrdinalIgnoreCase) == 0)); + StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase)); } public override int GetHashCode() { - var result = StringComparer.OrdinalIgnoreCase.GetHashCode(_unit); + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit); if (HasRange) { @@ -144,7 +145,8 @@ namespace Microsoft.Net.Http.Headers public override string ToString() { - var sb = new StringBuilder(_unit); + var sb = new StringBuilder(); + sb.Append(_unit); sb.Append(' '); if (HasRange) @@ -171,25 +173,25 @@ namespace Microsoft.Net.Http.Headers return sb.ToString(); } - public static ContentRangeHeaderValue Parse(string input) + public static ContentRangeHeaderValue Parse(StringSegment input) { var index = 0; return Parser.ParseValue(input, ref index); } - public static bool TryParse(string input, out ContentRangeHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out ContentRangeHeaderValue parsedValue) { var index = 0; return Parser.TryParseValue(input, ref index, out parsedValue); } - private static int GetContentRangeLength(string input, int startIndex, out ContentRangeHeaderValue parsedValue) + private static int GetContentRangeLength(StringSegment input, int startIndex, out ContentRangeHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -202,7 +204,7 @@ namespace Microsoft.Net.Http.Headers return 0; } - var unit = input.Substring(startIndex, unitLength); + var unit = input.Subsegment(startIndex, unitLength); var current = startIndex + unitLength; var separatorLength = HttpRuleParser.GetWhitespaceLength(input, current); @@ -259,7 +261,7 @@ namespace Microsoft.Net.Http.Headers return current - startIndex; } - private static bool TryGetLengthLength(string input, ref int current, out int lengthLength) + private static bool TryGetLengthLength(StringSegment input, ref int current, out int lengthLength) { lengthLength = 0; @@ -284,7 +286,7 @@ namespace Microsoft.Net.Http.Headers return true; } - private static bool TryGetRangeLength(string input, ref int current, out int fromLength, out int toStartIndex, out int toLength) + private static bool TryGetRangeLength(StringSegment input, ref int current, out int fromLength, out int toStartIndex, out int toLength) { fromLength = 0; toStartIndex = 0; @@ -341,8 +343,8 @@ namespace Microsoft.Net.Http.Headers } private static bool TryCreateContentRange( - string input, - string unit, + StringSegment input, + StringSegment unit, int fromStartIndex, int fromLength, int toStartIndex, @@ -354,13 +356,13 @@ namespace Microsoft.Net.Http.Headers parsedValue = null; long from = 0; - if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Substring(fromStartIndex, fromLength), out from)) + if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) { return false; } long to = 0; - if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Substring(toStartIndex, toLength), out to)) + if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) { return false; } @@ -372,7 +374,7 @@ namespace Microsoft.Net.Http.Headers } long length = 0; - if ((lengthLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Substring(lengthStartIndex, lengthLength), + if ((lengthLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(lengthStartIndex, lengthLength), out length)) { return false; diff --git a/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs b/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs index 9253e881e5..a94b61d319 100644 --- a/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs +++ b/src/Microsoft.Net.Http.Headers/CookieHeaderParser.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -12,7 +13,7 @@ namespace Microsoft.Net.Http.Headers { } - public sealed override bool TryParseValue(string value, ref int index, out CookieHeaderValue parsedValue) + public sealed override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue parsedValue) { parsedValue = null; @@ -21,7 +22,7 @@ namespace Microsoft.Net.Http.Headers // Accept: text/xml; q=1 // Accept: // Accept: text/plain; q=0.2 - if (string.IsNullOrEmpty(value) || (index == value.Length)) + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) { return SupportsMultipleValues; } @@ -62,7 +63,7 @@ namespace Microsoft.Net.Http.Headers return true; } - private static int GetNextNonEmptyOrWhitespaceIndex(string input, int startIndex, bool skipEmptyValues, out bool separatorFound) + private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) { Contract.Requires(input != null); Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. diff --git a/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs index 77adcf5441..3061b7d2fa 100644 --- a/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/CookieHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -14,16 +15,16 @@ namespace Microsoft.Net.Http.Headers private static readonly CookieHeaderParser SingleValueParser = new CookieHeaderParser(supportsMultipleValues: false); private static readonly CookieHeaderParser MultipleValueParser = new CookieHeaderParser(supportsMultipleValues: true); - private string _name; - private string _value; + private StringSegment _name; + private StringSegment _value; private CookieHeaderValue() { // Used by the parser to create a new instance of this type. } - public CookieHeaderValue(string name) - : this(name, string.Empty) + public CookieHeaderValue(StringSegment name) + : this(name, StringSegment.Empty) { if (name == null) { @@ -31,7 +32,7 @@ namespace Microsoft.Net.Http.Headers } } - public CookieHeaderValue(string name, string value) + public CookieHeaderValue(StringSegment name, StringSegment value) { if (name == null) { @@ -47,7 +48,7 @@ namespace Microsoft.Net.Http.Headers Value = value; } - public string Name + public StringSegment Name { get { return _name; } set @@ -57,7 +58,7 @@ namespace Microsoft.Net.Http.Headers } } - public string Value + public StringSegment Value { get { return _value; } set @@ -79,24 +80,13 @@ namespace Microsoft.Net.Http.Headers return header.ToString(); } - private static void AppendSegment(StringBuilder builder, string name, string value) - { - builder.Append("; "); - builder.Append(name); - if (value != null) - { - builder.Append("="); - builder.Append(value); - } - } - - public static CookieHeaderValue Parse(string input) + public static CookieHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } - public static bool TryParse(string input, out CookieHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out CookieHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); @@ -123,13 +113,13 @@ namespace Microsoft.Net.Http.Headers } // name=value; name="value" - internal static bool TryGetCookieLength(string input, ref int offset, out CookieHeaderValue parsedValue) + internal static bool TryGetCookieLength(StringSegment input, ref int offset, out CookieHeaderValue parsedValue) { Contract.Requires(offset >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (offset >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) { return false; } @@ -146,7 +136,7 @@ namespace Microsoft.Net.Http.Headers { return false; } - result._name = input.Substring(offset, itemLength); + result._name = input.Subsegment(offset, itemLength); offset += itemLength; // = (no spaces) @@ -166,7 +156,7 @@ namespace Microsoft.Net.Http.Headers // cookie-value = *cookie-octet / ( DQUOTE* cookie-octet DQUOTE ) // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash - internal static string GetCookieValue(string input, ref int offset) + internal static StringSegment GetCookieValue(StringSegment input, ref int offset) { Contract.Requires(input != null); Contract.Requires(offset >= 0); @@ -176,7 +166,7 @@ namespace Microsoft.Net.Http.Headers if (offset >= input.Length) { - return string.Empty; + return StringSegment.Empty; } var inQuotes = false; @@ -202,7 +192,7 @@ namespace Microsoft.Net.Http.Headers if (offset == input.Length || input[offset] != '"') { // Missing final quote - return string.Empty; + return StringSegment.Empty; } offset++; } @@ -210,13 +200,13 @@ namespace Microsoft.Net.Http.Headers int length = offset - startIndex; if (offset > startIndex) { - return input.Substring(startIndex, length); + return input.Subsegment(startIndex, length); } - return string.Empty; + return StringSegment.Empty; } - private static bool ReadEqualsSign(string input, ref int offset) + private static bool ReadEqualsSign(StringSegment input, ref int offset) { // = (no spaces) if (offset >= input.Length || input[offset] != '=') @@ -238,7 +228,7 @@ namespace Microsoft.Net.Http.Headers return !(c == '"' || c == ',' || c == ';' || c == '\\'); } - internal static void CheckNameFormat(string name, string parameterName) + internal static void CheckNameFormat(StringSegment name, string parameterName) { if (name == null) { @@ -251,7 +241,7 @@ namespace Microsoft.Net.Http.Headers } } - internal static void CheckValueFormat(string value, string parameterName) + internal static void CheckValueFormat(StringSegment value, string parameterName) { if (value == null) { @@ -275,8 +265,8 @@ namespace Microsoft.Net.Http.Headers return false; } - return string.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) - && string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); + return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase); } public override int GetHashCode() diff --git a/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs b/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs index 7d09920c77..e46cee3a34 100644 --- a/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/EntityTagHeaderValue.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -22,7 +23,7 @@ namespace Microsoft.Net.Http.Headers private static EntityTagHeaderValue AnyType; - private string _tag; + private StringSegment _tag; private bool _isWeak; private EntityTagHeaderValue() @@ -30,20 +31,20 @@ namespace Microsoft.Net.Http.Headers // Used by the parser to create a new instance of this type. } - public EntityTagHeaderValue(string tag) + public EntityTagHeaderValue(StringSegment tag) : this(tag, false) { } - public EntityTagHeaderValue(string tag, bool isWeak) + public EntityTagHeaderValue(StringSegment tag, bool isWeak) { - if (string.IsNullOrEmpty(tag)) + if (StringSegment.IsNullOrEmpty(tag)) { throw new ArgumentException("An empty string is not allowed.", nameof(tag)); } int length = 0; - if (!isWeak && string.Equals(tag, "*", StringComparison.Ordinal)) + if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal)) { // * is valid, but W/* isn't. _tag = tag; @@ -74,7 +75,7 @@ namespace Microsoft.Net.Http.Headers } } - public string Tag + public StringSegment Tag { get { return _tag; } } @@ -88,9 +89,9 @@ namespace Microsoft.Net.Http.Headers { if (_isWeak) { - return "W/" + _tag; + return "W/" + _tag.ToString(); } - return _tag; + return _tag.ToString(); } /// @@ -112,7 +113,7 @@ namespace Microsoft.Net.Http.Headers } // Since the tag is a quoted-string we treat it case-sensitive. - return _isWeak == other._isWeak && string.Equals(_tag, other._tag, StringComparison.Ordinal); + return _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal); } public override int GetHashCode() @@ -139,21 +140,21 @@ namespace Microsoft.Net.Http.Headers if (useStrongComparison) { - return !IsWeak && !other.IsWeak && string.Equals(Tag, other.Tag, StringComparison.Ordinal); + return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); } else { - return string.Equals(Tag, other.Tag, StringComparison.Ordinal); + return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal); } } - public static EntityTagHeaderValue Parse(string input) + public static EntityTagHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } - public static bool TryParse(string input, out EntityTagHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out EntityTagHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); @@ -179,13 +180,13 @@ namespace Microsoft.Net.Http.Headers return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); } - internal static int GetEntityTagLength(string input, int startIndex, out EntityTagHeaderValue parsedValue) + internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -235,7 +236,7 @@ namespace Microsoft.Net.Http.Headers } else { - parsedValue._tag = input.Substring(tagStartIndex, tagLength); + parsedValue._tag = input.Subsegment(tagStartIndex, tagLength); parsedValue._isWeak = isWeak; } diff --git a/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs b/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs index 63f9b8aed0..a2fbf720f9 100644 --- a/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs +++ b/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs @@ -2,12 +2,13 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { internal sealed class GenericHeaderParser : BaseHeaderParser { - internal delegate int GetParsedValueLengthDelegate(string value, int startIndex, out T parsedValue); + internal delegate int GetParsedValueLengthDelegate(StringSegment value, int startIndex, out T parsedValue); private GetParsedValueLengthDelegate _getParsedValueLength; @@ -22,7 +23,7 @@ namespace Microsoft.Net.Http.Headers _getParsedValueLength = getParsedValueLength; } - protected override int GetParsedValueLength(string value, int startIndex, out T parsedValue) + protected override int GetParsedValueLength(StringSegment value, int startIndex, out T parsedValue) { return _getParsedValueLength(value, startIndex, out parsedValue); } diff --git a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs index 9f49ba742c..abcf102076 100644 --- a/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs +++ b/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs @@ -72,9 +72,9 @@ namespace Microsoft.Net.Http.Headers return null; } - internal static void CheckValidToken(string value, string parameterName) + internal static void CheckValidToken(StringSegment value, string parameterName) { - if (string.IsNullOrEmpty(value)) + if (StringSegment.IsNullOrEmpty(value)) { throw new ArgumentException("An empty string is not allowed.", parameterName); } @@ -85,21 +85,6 @@ namespace Microsoft.Net.Http.Headers } } - internal static void CheckValidQuotedString(string value, string parameterName) - { - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentException("An empty string is not allowed.", parameterName); - } - - int length; - if ((HttpRuleParser.GetQuotedStringLength(value, 0, out length) != HttpParseResult.Parsed) || - (length != value.Length)) // no trailing spaces allowed - { - throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid quoted string '{0}'.", value)); - } - } - internal static bool AreEqualCollections(ICollection x, ICollection y) { return AreEqualCollections(x, y, null); @@ -167,7 +152,7 @@ namespace Microsoft.Net.Http.Headers } internal static int GetNextNonEmptyOrWhitespaceIndex( - string input, + StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) @@ -368,11 +353,6 @@ namespace Microsoft.Net.Http.Headers return false; } - internal static bool TryParseNonNegativeInt32(string value, out int result) - { - return TryParseNonNegativeInt32(new StringSegment(value), out result); - } - /// /// Try to convert a string representation of a positive number to its 64-bit signed integer equivalent. /// A return value indicates whether the conversion succeeded or failed. @@ -388,12 +368,7 @@ namespace Microsoft.Net.Http.Headers /// result will be overwritten. /// /// true if parsing succeeded; otherwise, false. - public static bool TryParseNonNegativeInt64(string value, out long result) - { - return TryParseNonNegativeInt64(new StringSegment(value), out result); - } - - internal static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int result) + public static unsafe bool TryParseNonNegativeInt32(StringSegment value, out int result) { if (string.IsNullOrEmpty(value.Buffer) || value.Length == 0) { @@ -483,7 +458,7 @@ namespace Microsoft.Net.Http.Headers // Strict and fast RFC7231 5.3.1 Quality value parser (and without memory allocation) // See https://tools.ietf.org/html/rfc7231#section-5.3.1 // Check is made to verify if the value is between 0 and 1 (and it returns False if the check fails). - internal static bool TryParseQualityDouble(string input, int startIndex, out double quality, out int length) + internal static bool TryParseQualityDouble(StringSegment input, int startIndex, out double quality, out int length) { quality = 0; length = 0; @@ -602,7 +577,7 @@ namespace Microsoft.Net.Http.Headers return new string(charBuffer, position, _int64MaxStringLength - position); } - public static bool TryParseDate(string input, out DateTimeOffset result) + public static bool TryParseDate(StringSegment input, out DateTimeOffset result) { return HttpRuleParser.TryStringToDate(input, out result); } @@ -617,11 +592,11 @@ namespace Microsoft.Net.Http.Headers return dateTime.ToRfc1123String(quoted); } - public static string RemoveQuotes(string input) + public static StringSegment RemoveQuotes(StringSegment input) { - if (!string.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"') + if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"') { - input = input.Substring(1, input.Length - 2); + input = input.Subsegment(1, input.Length - 2); } return input; } diff --git a/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs b/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs index 383a304dcf..027a9de438 100644 --- a/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs +++ b/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -26,9 +27,9 @@ namespace Microsoft.Net.Http.Headers // pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0 // for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first // non-whitespace after the separator ','. - public abstract bool TryParseValue(string value, ref int index, out T parsedValue); + public abstract bool TryParseValue(StringSegment value, ref int index, out T parsedValue); - public T ParseValue(string value, ref int index) + public T ParseValue(StringSegment value, ref int index) { // Index may be value.Length (e.g. both 0). This may be allowed for some headers (e.g. Accept but not // allowed by others (e.g. Content-Length). The parser has to decide if this is valid or not. @@ -40,7 +41,7 @@ namespace Microsoft.Net.Http.Headers if (!TryParseValue(value, ref index, out result)) { throw new FormatException(string.Format(CultureInfo.InvariantCulture, - "The header contains invalid values at index {0}: '{1}'", index, value ?? "")); + "The header contains invalid values at index {0}: '{1}'", index, value.Value ?? "")); } return result; } diff --git a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs index 0ce5833600..5a6adf9415 100644 --- a/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs +++ b/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics.Contracts; using System.Globalization; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -89,7 +90,7 @@ namespace Microsoft.Net.Http.Headers } [Pure] - internal static int GetTokenLength(string input, int startIndex) + internal static int GetTokenLength(StringSegment input, int startIndex) { Contract.Requires(input != null); Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); @@ -112,7 +113,7 @@ namespace Microsoft.Net.Http.Headers return input.Length - startIndex; } - internal static int GetWhitespaceLength(string input, int startIndex) + internal static int GetWhitespaceLength(StringSegment input, int startIndex) { Contract.Requires(input != null); Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); @@ -156,7 +157,7 @@ namespace Microsoft.Net.Http.Headers return input.Length - startIndex; } - internal static int GetNumberLength(string input, int startIndex, bool allowDecimal) + internal static int GetNumberLength(StringSegment input, int startIndex, bool allowDecimal) { Contract.Requires(input != null); Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); @@ -201,7 +202,7 @@ namespace Microsoft.Net.Http.Headers return current - startIndex; } - internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length) + internal static HttpParseResult GetQuotedStringLength(StringSegment input, int startIndex, out int length) { var nestedCount = 0; return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); @@ -209,7 +210,7 @@ namespace Microsoft.Net.Http.Headers // quoted-pair = "\" CHAR // CHAR = - internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length) + internal static HttpParseResult GetQuotedPairLength(StringSegment input, int startIndex, out int length) { Contract.Requires(input != null); Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); @@ -237,8 +238,8 @@ namespace Microsoft.Net.Http.Headers // Try the various date formats in the order listed above. // We should accept a wide verity of common formats, but only output RFC 1123 style dates. - internal static bool TryStringToDate(string input, out DateTimeOffset result) => - DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, + internal static bool TryStringToDate(StringSegment input, out DateTimeOffset result) => + DateTimeOffset.TryParseExact(input.ToString(), DateFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); // TEXT = @@ -253,7 +254,7 @@ namespace Microsoft.Net.Http.Headers // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) // is unusual. private static HttpParseResult GetExpressionLength( - string input, + StringSegment input, int startIndex, char openChar, char closeChar, diff --git a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs index 0ec7666402..bfda2aac81 100644 --- a/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/MediaTypeHeaderValue.cs @@ -7,6 +7,7 @@ using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -22,7 +23,7 @@ namespace Microsoft.Net.Http.Headers // Use a collection instead of a dictionary since we may have multiple parameters with the same name. private ObjectCollection _parameters; - private string _mediaType; + private StringSegment _mediaType; private bool _isReadOnly; private MediaTypeHeaderValue() @@ -30,23 +31,23 @@ namespace Microsoft.Net.Http.Headers // Used by the parser to create a new instance of this type. } - public MediaTypeHeaderValue(string mediaType) + public MediaTypeHeaderValue(StringSegment mediaType) { CheckMediaTypeFormat(mediaType, "mediaType"); _mediaType = mediaType; } - public MediaTypeHeaderValue(string mediaType, double quality) + public MediaTypeHeaderValue(StringSegment mediaType, double quality) : this(mediaType) { Quality = quality; } - public string Charset + public StringSegment Charset { get { - return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value; + return NameValueHeaderValue.Find(_parameters, CharsetString)?.Value.Value; } set { @@ -54,7 +55,7 @@ namespace Microsoft.Net.Http.Headers // We don't prevent a user from setting whitespace-only charsets. Like we can't prevent a user from // setting a non-existing charset. var charsetParameter = NameValueHeaderValue.Find(_parameters, CharsetString); - if (string.IsNullOrEmpty(value)) + if (StringSegment.IsNullOrEmpty(value)) { // Remove charset parameter if (charsetParameter != null) @@ -81,11 +82,11 @@ namespace Microsoft.Net.Http.Headers get { var charset = Charset; - if (!string.IsNullOrWhiteSpace(charset)) + if (!StringSegment.IsNullOrEmpty(charset)) { try { - return Encoding.GetEncoding(charset); + return Encoding.GetEncoding(charset.Value); } catch (ArgumentException) { @@ -108,17 +109,17 @@ namespace Microsoft.Net.Http.Headers } } - public string Boundary + public StringSegment Boundary { get { - return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value; + return NameValueHeaderValue.Find(_parameters, BoundaryString)?.Value ?? default(StringSegment); } set { HeaderUtilities.ThrowIfReadOnly(IsReadOnly); var boundaryParameter = NameValueHeaderValue.Find(_parameters, BoundaryString); - if (string.IsNullOrEmpty(value)) + if (StringSegment.IsNullOrEmpty(value)) { // Remove charset parameter if (boundaryParameter != null) @@ -169,7 +170,7 @@ namespace Microsoft.Net.Http.Headers } } - public string MediaType + public StringSegment MediaType { get { return _mediaType; } set @@ -180,19 +181,19 @@ namespace Microsoft.Net.Http.Headers } } - public string Type + public StringSegment Type { get { - return _mediaType.Substring(0, _mediaType.IndexOf('/')); + return _mediaType.Subsegment(0, _mediaType.IndexOf('/')); } } - public string SubType + public StringSegment SubType { get { - return _mediaType.Substring(_mediaType.IndexOf('/') + 1); + return _mediaType.Subsegment(_mediaType.IndexOf('/') + 1); } } @@ -214,7 +215,7 @@ namespace Microsoft.Net.Http.Headers { get { - return string.Compare(_mediaType, _mediaType.IndexOf('/') + 1, "*", 0, 1, StringComparison.Ordinal) == 0; + return SubType.Equals("*", StringComparison.Ordinal); } } @@ -245,31 +246,15 @@ namespace Microsoft.Net.Http.Headers return false; } - // PERF: Avoid doing anything here that allocates a substring, this is a very hot path - // for content-negotiation. - var indexOfSlash = _mediaType.IndexOf('/'); - // "text/plain" is a subset of "text/plain", "text/*" and "*/*". "*/*" is a subset only of "*/*". - if (string.Compare( - strA: _mediaType, - indexA: 0, - strB: otherMediaType._mediaType, - indexB: 0, - length: indexOfSlash, - comparisonType: StringComparison.OrdinalIgnoreCase) != 0) + if (!Type.Equals(otherMediaType.Type, comparisonType: StringComparison.OrdinalIgnoreCase)) { if (!otherMediaType.MatchesAllTypes) { return false; } } - else if (string.Compare( - strA: MediaType, - indexA: indexOfSlash + 1, - strB: otherMediaType._mediaType, - indexB: indexOfSlash + 1, // We know the Type is equal, so the index of '/' is the same in both strings. - length: _mediaType.Length - indexOfSlash, - comparisonType: StringComparison.OrdinalIgnoreCase) != 0) + else if (!SubType.Equals(otherMediaType.SubType, comparisonType: StringComparison.OrdinalIgnoreCase)) { if (!otherMediaType.MatchesAllSubTypes) { @@ -285,7 +270,7 @@ namespace Microsoft.Net.Http.Headers // parameters locally; they make this one more specific. foreach (var parameter in otherMediaType._parameters) { - if (string.Equals(parameter.Name, "q", StringComparison.OrdinalIgnoreCase)) + if (parameter.Name.Equals("q", StringComparison.OrdinalIgnoreCase)) { // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first // "q" parameter (if any) separates the media-range parameter(s) from the accept-params. @@ -299,7 +284,7 @@ namespace Microsoft.Net.Http.Headers return false; } - if (!string.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) + if (!StringSegment.Equals(parameter.Value, localParameter.Value, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -352,7 +337,10 @@ namespace Microsoft.Net.Http.Headers public override string ToString() { - return _mediaType + NameValueHeaderValue.ToString(_parameters, ';', true); + var builder = new StringBuilder(); + builder.Append(_mediaType); + NameValueHeaderValue.ToString(_parameters, separator: ';', leadingSeparator: true, destination: builder); + return builder.ToString(); } public override bool Equals(object obj) @@ -364,23 +352,23 @@ namespace Microsoft.Net.Http.Headers return false; } - return (string.Compare(_mediaType, other._mediaType, StringComparison.OrdinalIgnoreCase) == 0) && + return _mediaType.Equals(other._mediaType, StringComparison.OrdinalIgnoreCase) && HeaderUtilities.AreEqualCollections(_parameters, other._parameters); } public override int GetHashCode() { // The media-type string is case-insensitive. - return StringComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters); + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_mediaType) ^ NameValueHeaderValue.GetHashCode(_parameters); } - public static MediaTypeHeaderValue Parse(string input) + public static MediaTypeHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } - public static bool TryParse(string input, out MediaTypeHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out MediaTypeHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); @@ -406,20 +394,19 @@ namespace Microsoft.Net.Http.Headers return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); } - private static int GetMediaTypeLength(string input, int startIndex, out MediaTypeHeaderValue parsedValue) + private static int GetMediaTypeLength(StringSegment input, int startIndex, out MediaTypeHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } // Caller must remove leading whitespace. If not, we'll return 0. - string mediaType = null; - var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out mediaType); + var mediaTypeLength = MediaTypeHeaderValue.GetMediaTypeExpressionLength(input, startIndex, out var mediaType); if (mediaTypeLength == 0) { @@ -451,7 +438,7 @@ namespace Microsoft.Net.Http.Headers return current - startIndex; } - private static int GetMediaTypeExpressionLength(string input, int startIndex, out string mediaType) + private static int GetMediaTypeExpressionLength(StringSegment input, int startIndex, out StringSegment mediaType) { Contract.Requires((input != null) && (input.Length > 0) && (startIndex < input.Length)); @@ -490,7 +477,7 @@ namespace Microsoft.Net.Http.Headers var mediaTypeLength = current + subtypeLength - startIndex; if (typeLength + subtypeLength + 1 == mediaTypeLength) { - mediaType = input.Substring(startIndex, mediaTypeLength); + mediaType = input.Subsegment(startIndex, mediaTypeLength); } else { @@ -500,17 +487,16 @@ namespace Microsoft.Net.Http.Headers return mediaTypeLength; } - private static void CheckMediaTypeFormat(string mediaType, string parameterName) + private static void CheckMediaTypeFormat(StringSegment mediaType, string parameterName) { - if (string.IsNullOrEmpty(mediaType)) + if (StringSegment.IsNullOrEmpty(mediaType)) { throw new ArgumentException("An empty string is not allowed.", parameterName); } // When adding values using strongly typed objects, no leading/trailing LWS (whitespace) is allowed. // Also no LWS between type and subtype is allowed. - string tempMediaType; - var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out tempMediaType); + var mediaTypeLength = GetMediaTypeExpressionLength(mediaType, 0, out var tempMediaType); if ((mediaTypeLength == 0) || (tempMediaType.Length != mediaType.Length)) { throw new FormatException(string.Format(CultureInfo.InvariantCulture, "Invalid media type '{0}'.", mediaType)); diff --git a/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs b/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs index a04b8d7164..76b48e0093 100644 --- a/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -18,8 +19,8 @@ namespace Microsoft.Net.Http.Headers internal static readonly HttpHeaderParser MultipleValueParser = new GenericHeaderParser(true, GetNameValueLength); - private string _name; - private string _value; + private StringSegment _name; + private StringSegment _value; private bool _isReadOnly; private NameValueHeaderValue() @@ -27,12 +28,12 @@ namespace Microsoft.Net.Http.Headers // Used by the parser to create a new instance of this type. } - public NameValueHeaderValue(string name) + public NameValueHeaderValue(StringSegment name) : this(name, null) { } - public NameValueHeaderValue(string name, string value) + public NameValueHeaderValue(StringSegment name, StringSegment value) { CheckNameValueFormat(name, value); @@ -40,12 +41,12 @@ namespace Microsoft.Net.Http.Headers _value = value; } - public string Name + public StringSegment Name { get { return _name; } } - public string Value + public StringSegment Value { get { return _value; } set @@ -90,9 +91,9 @@ namespace Microsoft.Net.Http.Headers { Contract.Assert(_name != null); - var nameHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_name); + var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name); - if (!string.IsNullOrEmpty(_value)) + if (!StringSegment.IsNullOrEmpty(_value)) { // If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase // and retrieve the hash code. @@ -101,7 +102,7 @@ namespace Microsoft.Net.Http.Headers return nameHashCode ^ _value.GetHashCode(); } - return nameHashCode ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_value); + return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); } return nameHashCode; @@ -116,7 +117,7 @@ namespace Microsoft.Net.Http.Headers return false; } - if (string.Compare(_name, other._name, StringComparison.OrdinalIgnoreCase) != 0) + if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -125,29 +126,29 @@ namespace Microsoft.Net.Http.Headers // case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect" // header. We treat all quoted-strings the same: case-sensitive comparison. - if (string.IsNullOrEmpty(_value)) + if (StringSegment.IsNullOrEmpty(_value)) { - return string.IsNullOrEmpty(other._value); + return StringSegment.IsNullOrEmpty(other._value); } if (_value[0] == '"') { // We have a quoted string, so we need to do case-sensitive comparison. - return (string.CompareOrdinal(_value, other._value) == 0); + return (_value.Equals(other._value, StringComparison.Ordinal)); } else { - return (string.Compare(_value, other._value, StringComparison.OrdinalIgnoreCase) == 0); + return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase)); } } - public static NameValueHeaderValue Parse(string input) + public static NameValueHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } - public static bool TryParse(string input, out NameValueHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out NameValueHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); @@ -175,11 +176,11 @@ namespace Microsoft.Net.Http.Headers public override string ToString() { - if (!string.IsNullOrEmpty(_value)) + if (!StringSegment.IsNullOrEmpty(_value)) { return _name + "=" + _value; } - return _name; + return _name.ToString(); } internal static void ToString( @@ -202,7 +203,12 @@ namespace Microsoft.Net.Http.Headers destination.Append(separator); destination.Append(' '); } - destination.Append(values[i].ToString()); + destination.Append(values[i].Name); + if (!StringSegment.IsNullOrEmpty(values[i].Value)) + { + destination.Append('='); + destination.Append(values[i].Value); + } } } @@ -235,14 +241,14 @@ namespace Microsoft.Net.Http.Headers return result; } - private static int GetNameValueLength(string input, int startIndex, out NameValueHeaderValue parsedValue) + private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue parsedValue) { Contract.Requires(input != null); Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -256,7 +262,7 @@ namespace Microsoft.Net.Http.Headers return 0; } - var name = input.Substring(startIndex, nameLength); + var name = input.Subsegment(startIndex, nameLength); var current = startIndex + nameLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); @@ -280,7 +286,7 @@ namespace Microsoft.Net.Http.Headers // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation. parsedValue = new NameValueHeaderValue(); parsedValue._name = name; - parsedValue._value = input.Substring(current, valueLength); + parsedValue._value = input.Subsegment(current, valueLength); current = current + valueLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces return current - startIndex; @@ -289,7 +295,7 @@ namespace Microsoft.Net.Http.Headers // Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3 // name/value pairs to 'nameValueCollection' if 'delimiter' equals ','. internal static int GetNameValueListLength( - string input, + StringSegment input, int startIndex, char delimiter, IList nameValueCollection) @@ -297,7 +303,7 @@ namespace Microsoft.Net.Http.Headers Contract.Requires(nameValueCollection != null); Contract.Requires(startIndex >= 0); - if ((string.IsNullOrEmpty(input)) || (startIndex >= input.Length)) + if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) { return 0; } @@ -330,7 +336,7 @@ namespace Microsoft.Net.Http.Headers } } - public static NameValueHeaderValue Find(IList values, string name) + public static NameValueHeaderValue Find(IList values, StringSegment name) { Contract.Requires((name != null) && (name.Length > 0)); @@ -342,7 +348,7 @@ namespace Microsoft.Net.Http.Headers for (var i = 0; i < values.Count; i++) { var value = values[i]; - if (string.Compare(value.Name, name, StringComparison.OrdinalIgnoreCase) == 0) + if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) { return value; } @@ -350,7 +356,7 @@ namespace Microsoft.Net.Http.Headers return null; } - internal static int GetValueLength(string input, int startIndex) + internal static int GetValueLength(StringSegment input, int startIndex) { Contract.Requires(input != null); @@ -373,16 +379,16 @@ namespace Microsoft.Net.Http.Headers return valueLength; } - private static void CheckNameValueFormat(string name, string value) + private static void CheckNameValueFormat(StringSegment name, StringSegment value) { HeaderUtilities.CheckValidToken(name, nameof(name)); CheckValueFormat(value); } - private static void CheckValueFormat(string value) + private static void CheckValueFormat(StringSegment value) { // Either value is null/empty or a valid token/quoted string - if (!(string.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))) + if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))) { throw new FormatException(string.Format(System.Globalization.CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value)); } diff --git a/src/Microsoft.Net.Http.Headers/ObjectCollection.cs b/src/Microsoft.Net.Http.Headers/ObjectCollection.cs index f6d91e1508..db5f876b53 100644 --- a/src/Microsoft.Net.Http.Headers/ObjectCollection.cs +++ b/src/Microsoft.Net.Http.Headers/ObjectCollection.cs @@ -11,7 +11,7 @@ namespace Microsoft.Net.Http.Headers // type to throw if 'null' gets added. Collection internally uses List which comes at some cost. In addition // Collection.Add() calls List.InsertItem() which is an O(n) operation (compared to O(1) for List.Add()). // This type is only used for very small collections (1-2 items) to keep the impact of using Collection small. - internal class ObjectCollection : Collection where T : class + internal class ObjectCollection : Collection { internal static readonly Action DefaultValidator = CheckNotNull; internal static readonly ObjectCollection EmptyReadOnlyCollection diff --git a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs index 4960e4beab..f1ebee276c 100644 --- a/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeConditionHeaderValue.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -85,26 +86,26 @@ namespace Microsoft.Net.Http.Headers return _entityTag.GetHashCode(); } - public static RangeConditionHeaderValue Parse(string input) + public static RangeConditionHeaderValue Parse(StringSegment input) { var index = 0; return Parser.ParseValue(input, ref index); } - public static bool TryParse(string input, out RangeConditionHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out RangeConditionHeaderValue parsedValue) { var index = 0; return Parser.TryParseValue(input, ref index, out parsedValue); } - private static int GetRangeConditionLength(string input, int startIndex, out RangeConditionHeaderValue parsedValue) + private static int GetRangeConditionLength(StringSegment input, int startIndex, out RangeConditionHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; // Make sure we have at least 2 characters - if (string.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex + 1 >= input.Length)) { return 0; } @@ -141,7 +142,7 @@ namespace Microsoft.Net.Http.Headers } else { - if (!HttpRuleParser.TryStringToDate(input.Substring(current), out date)) + if (!HttpRuleParser.TryStringToDate(input.Subsegment(current), out date)) { return 0; } diff --git a/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs index 3657e24c27..934b6b6cc1 100644 --- a/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Text; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -13,7 +14,7 @@ namespace Microsoft.Net.Http.Headers private static readonly HttpHeaderParser Parser = new GenericHeaderParser(false, GetRangeLength); - private string _unit; + private StringSegment _unit; private ICollection _ranges; public RangeHeaderValue() @@ -28,7 +29,7 @@ namespace Microsoft.Net.Http.Headers Ranges.Add(new RangeItemHeaderValue(from, to)); } - public string Unit + public StringSegment Unit { get { return _unit; } set @@ -52,7 +53,8 @@ namespace Microsoft.Net.Http.Headers public override string ToString() { - var sb = new StringBuilder(_unit); + var sb = new StringBuilder(); + sb.Append(_unit); sb.Append('='); var first = true; @@ -84,13 +86,13 @@ namespace Microsoft.Net.Http.Headers return false; } - return (string.Compare(_unit, other._unit, StringComparison.OrdinalIgnoreCase) == 0) && + return StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase) && HeaderUtilities.AreEqualCollections(Ranges, other.Ranges); } public override int GetHashCode() { - var result = StringComparer.OrdinalIgnoreCase.GetHashCode(_unit); + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit); foreach (var range in Ranges) { @@ -100,25 +102,25 @@ namespace Microsoft.Net.Http.Headers return result; } - public static RangeHeaderValue Parse(string input) + public static RangeHeaderValue Parse(StringSegment input) { var index = 0; return Parser.ParseValue(input, ref index); } - public static bool TryParse(string input, out RangeHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out RangeHeaderValue parsedValue) { var index = 0; return Parser.TryParseValue(input, ref index, out parsedValue); } - private static int GetRangeLength(string input, int startIndex, out RangeHeaderValue parsedValue) + private static int GetRangeLength(StringSegment input, int startIndex, out RangeHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -132,7 +134,7 @@ namespace Microsoft.Net.Http.Headers } RangeHeaderValue result = new RangeHeaderValue(); - result._unit = input.Substring(startIndex, unitLength); + result._unit = input.Subsegment(startIndex, unitLength); var current = startIndex + unitLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); diff --git a/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs b/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs index 866d4b8e8b..99fdbfef5c 100644 --- a/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/RangeItemHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -87,7 +88,7 @@ namespace Microsoft.Net.Http.Headers // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty // list segments are allowed, e.g. ",1-2, , 3-4,,". internal static int GetRangeItemListLength( - string input, + StringSegment input, int startIndex, ICollection rangeCollection) { @@ -96,7 +97,7 @@ namespace Microsoft.Net.Http.Headers Contract.Ensures((Contract.Result() == 0) || (rangeCollection.Count > 0), "If we can parse the string, then we expect to have at least one range item."); - if ((string.IsNullOrEmpty(input)) || (startIndex >= input.Length)) + if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length)) { return 0; } @@ -140,7 +141,7 @@ namespace Microsoft.Net.Http.Headers } } - internal static int GetRangeItemLength(string input, int startIndex, out RangeItemHeaderValue parsedValue) + internal static int GetRangeItemLength(StringSegment input, int startIndex, out RangeItemHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); @@ -148,7 +149,7 @@ namespace Microsoft.Net.Http.Headers parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -202,14 +203,14 @@ namespace Microsoft.Net.Http.Headers // Try convert first value to int64 long from = 0; - if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Substring(fromStartIndex, fromLength), out from)) + if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from)) { return 0; } // Try convert second value to int64 long to = 0; - if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Substring(toStartIndex, toLength), out to)) + if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to)) { return 0; } diff --git a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs index 10c68bb5b1..f3477648de 100644 --- a/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/SetCookieHeaderValue.cs @@ -31,20 +31,20 @@ namespace Microsoft.Net.Http.Headers private static readonly HttpHeaderParser MultipleValueParser = new GenericHeaderParser(true, GetSetCookieLength); - private string _name; - private string _value; + private StringSegment _name; + private StringSegment _value; private SetCookieHeaderValue() { // Used by the parser to create a new instance of this type. } - public SetCookieHeaderValue(string name) - : this(name, string.Empty) + public SetCookieHeaderValue(StringSegment name) + : this(name, StringSegment.Empty) { } - public SetCookieHeaderValue(string name, string value) + public SetCookieHeaderValue(StringSegment name, StringSegment value) { if (name == null) { @@ -60,7 +60,7 @@ namespace Microsoft.Net.Http.Headers Value = value; } - public string Name + public StringSegment Name { get { return _name; } set @@ -70,7 +70,7 @@ namespace Microsoft.Net.Http.Headers } } - public string Value + public StringSegment Value { get { return _value; } set @@ -84,10 +84,9 @@ namespace Microsoft.Net.Http.Headers public TimeSpan? MaxAge { get; set; } - public string Domain { get; set; } + public StringSegment Domain { get; set; } - // TODO: PathString? - public string Path { get; set; } + public StringSegment Path { get; set; } public bool Secure { get; set; } @@ -186,7 +185,7 @@ namespace Microsoft.Net.Http.Headers return sb.ToString(); } - private static void AppendSegment(ref InplaceStringBuilder builder, string name, string value) + private static void AppendSegment(ref InplaceStringBuilder builder, StringSegment name, StringSegment value) { builder.Append(SeparatorToken); builder.Append(name); @@ -247,7 +246,7 @@ namespace Microsoft.Net.Http.Headers } } - private static void AppendSegment(StringBuilder builder, string name, string value) + private static void AppendSegment(StringBuilder builder, StringSegment name, StringSegment value) { builder.Append("; "); builder.Append(name); @@ -258,13 +257,13 @@ namespace Microsoft.Net.Http.Headers } } - public static SetCookieHeaderValue Parse(string input) + public static SetCookieHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } - public static bool TryParse(string input, out SetCookieHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out SetCookieHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); @@ -291,14 +290,14 @@ namespace Microsoft.Net.Http.Headers } // name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly - private static int GetSetCookieLength(string input, int startIndex, out SetCookieHeaderValue parsedValue) + private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); var offset = startIndex; parsedValue = null; - if (string.IsNullOrEmpty(input) || (offset >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) { return 0; } @@ -315,7 +314,7 @@ namespace Microsoft.Net.Http.Headers { return 0; } - result._name = input.Substring(offset, itemLength); + result._name = input.Subsegment(offset, itemLength); offset += itemLength; // = (no spaces) @@ -352,11 +351,11 @@ namespace Microsoft.Net.Http.Headers // Trailing ';' or leading into garbage. Let the next parser fail. break; } - var token = input.Substring(offset, itemLength); + var token = input.Subsegment(offset, itemLength); offset += itemLength; // expires-av = "Expires=" sane-cookie-date - if (string.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(token, ExpiresToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) @@ -373,7 +372,7 @@ namespace Microsoft.Net.Http.Headers result.Expires = expirationDate; } // max-age-av = "Max-Age=" non-zero-digit *DIGIT - else if (string.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(token, MaxAgeToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) @@ -386,7 +385,7 @@ namespace Microsoft.Net.Http.Headers { return 0; } - var numberString = input.Substring(offset, itemLength); + var numberString = input.Subsegment(offset, itemLength); long maxAge; if (!HeaderUtilities.TryParseNonNegativeInt64(numberString, out maxAge)) { @@ -398,7 +397,7 @@ namespace Microsoft.Net.Http.Headers } // domain-av = "Domain=" domain-value // domain-value = ; defined in [RFC1034], Section 3.5, as enhanced by [RFC1123], Section 2.1 - else if (string.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(token, DomainToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) @@ -410,7 +409,7 @@ namespace Microsoft.Net.Http.Headers } // path-av = "Path=" path-value // path-value = - else if (string.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(token, PathToken, StringComparison.OrdinalIgnoreCase)) { // = (no spaces) if (!ReadEqualsSign(input, ref offset)) @@ -421,13 +420,13 @@ namespace Microsoft.Net.Http.Headers result.Path = ReadToSemicolonOrEnd(input, ref offset); } // secure-av = "Secure" - else if (string.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(token, SecureToken, StringComparison.OrdinalIgnoreCase)) { result.Secure = true; } // samesite-av = "SameSite" / "SameSite=" samesite-value // samesite-value = "Strict" / "Lax" - else if (string.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase)) { if (!ReadEqualsSign(input, ref offset)) { @@ -437,7 +436,7 @@ namespace Microsoft.Net.Http.Headers { var enforcementMode = ReadToSemicolonOrEnd(input, ref offset); - if (string.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase)) + if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase)) { result.SameSite = SameSiteMode.Lax; } @@ -448,7 +447,7 @@ namespace Microsoft.Net.Http.Headers } } // httponly-av = "HttpOnly" - else if (string.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) + else if (StringSegment.Equals(token, HttpOnlyToken, StringComparison.OrdinalIgnoreCase)) { result.HttpOnly = true; } @@ -463,7 +462,7 @@ namespace Microsoft.Net.Http.Headers return offset - startIndex; } - private static bool ReadEqualsSign(string input, ref int offset) + private static bool ReadEqualsSign(StringSegment input, ref int offset) { // = (no spaces) if (offset >= input.Length || input[offset] != '=') @@ -474,7 +473,7 @@ namespace Microsoft.Net.Http.Headers return true; } - private static string ReadToSemicolonOrEnd(string input, ref int offset) + private static StringSegment ReadToSemicolonOrEnd(StringSegment input, ref int offset) { var end = input.IndexOf(';', offset); if (end < 0) @@ -483,7 +482,7 @@ namespace Microsoft.Net.Http.Headers end = input.Length; } var itemLength = end - offset; - var result = input.Substring(offset, itemLength); + var result = input.Subsegment(offset, itemLength); offset += itemLength; return result; } @@ -497,12 +496,12 @@ namespace Microsoft.Net.Http.Headers return false; } - return string.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) - && string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase) + return StringSegment.Equals(_name, other._name, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase) && Expires.Equals(other.Expires) && MaxAge.Equals(other.MaxAge) - && string.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) - && string.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(Domain, other.Domain, StringComparison.OrdinalIgnoreCase) + && StringSegment.Equals(Path, other.Path, StringComparison.OrdinalIgnoreCase) && Secure == other.Secure && SameSite == other.SameSite && HttpOnly == other.HttpOnly; @@ -510,12 +509,12 @@ namespace Microsoft.Net.Http.Headers public override int GetHashCode() { - return StringComparer.OrdinalIgnoreCase.GetHashCode(_name) - ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_value) + return StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name) + ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value) ^ (Expires.HasValue ? Expires.GetHashCode() : 0) ^ (MaxAge.HasValue ? MaxAge.GetHashCode() : 0) - ^ (Domain != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0) - ^ (Path != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0) + ^ (Domain != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Domain) : 0) + ^ (Path != null ? StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(Path) : 0) ^ Secure.GetHashCode() ^ SameSite.GetHashCode() ^ HttpOnly.GetHashCode(); diff --git a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs index 13ac4fb15c..deba2d2697 100644 --- a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs +++ b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValue.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -15,7 +16,7 @@ namespace Microsoft.Net.Http.Headers private static readonly HttpHeaderParser MultipleValueParser = new GenericHeaderParser(true, GetStringWithQualityLength); - private string _value; + private StringSegment _value; private double? _quality; private StringWithQualityHeaderValue() @@ -23,14 +24,14 @@ namespace Microsoft.Net.Http.Headers // Used by the parser to create a new instance of this type. } - public StringWithQualityHeaderValue(string value) + public StringWithQualityHeaderValue(StringSegment value) { HeaderUtilities.CheckValidToken(value, nameof(value)); _value = value; } - public StringWithQualityHeaderValue(string value, double quality) + public StringWithQualityHeaderValue(StringSegment value, double quality) { HeaderUtilities.CheckValidToken(value, nameof(value)); @@ -43,7 +44,7 @@ namespace Microsoft.Net.Http.Headers _quality = quality; } - public string Value + public StringSegment Value { get { return _value; } } @@ -60,7 +61,7 @@ namespace Microsoft.Net.Http.Headers return _value + "; q=" + _quality.Value.ToString("0.0##", NumberFormatInfo.InvariantInfo); } - return _value; + return _value.ToString(); } public override bool Equals(object obj) @@ -72,7 +73,7 @@ namespace Microsoft.Net.Http.Headers return false; } - if (string.Compare(_value, other._value, StringComparison.OrdinalIgnoreCase) != 0) + if (!StringSegment.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase)) { return false; } @@ -92,7 +93,7 @@ namespace Microsoft.Net.Http.Headers public override int GetHashCode() { - var result = StringComparer.OrdinalIgnoreCase.GetHashCode(_value); + var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value); if (_quality.HasValue) { @@ -102,13 +103,13 @@ namespace Microsoft.Net.Http.Headers return result; } - public static StringWithQualityHeaderValue Parse(string input) + public static StringWithQualityHeaderValue Parse(StringSegment input) { var index = 0; return SingleValueParser.ParseValue(input, ref index); } - public static bool TryParse(string input, out StringWithQualityHeaderValue parsedValue) + public static bool TryParse(StringSegment input, out StringWithQualityHeaderValue parsedValue) { var index = 0; return SingleValueParser.TryParseValue(input, ref index, out parsedValue); @@ -134,13 +135,13 @@ namespace Microsoft.Net.Http.Headers return MultipleValueParser.TryParseStrictValues(input, out parsedValues); } - private static int GetStringWithQualityLength(string input, int startIndex, out StringWithQualityHeaderValue parsedValue) + private static int GetStringWithQualityLength(StringSegment input, int startIndex, out StringWithQualityHeaderValue parsedValue) { Contract.Requires(startIndex >= 0); parsedValue = null; - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) + if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length)) { return 0; } @@ -154,7 +155,7 @@ namespace Microsoft.Net.Http.Headers } StringWithQualityHeaderValue result = new StringWithQualityHeaderValue(); - result._value = input.Substring(startIndex, valueLength); + result._value = input.Subsegment(startIndex, valueLength); var current = startIndex + valueLength; current = current + HttpRuleParser.GetWhitespaceLength(input, current); @@ -177,7 +178,7 @@ namespace Microsoft.Net.Http.Headers return current - startIndex; } - private static bool TryReadQuality(string input, StringWithQualityHeaderValue result, ref int index) + private static bool TryReadQuality(StringSegment input, StringWithQualityHeaderValue result, ref int index) { var current = index; diff --git a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs index 0d092ec19d..961cc07841 100644 --- a/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs +++ b/src/Microsoft.Net.Http.Headers/StringWithQualityHeaderValueComparer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers { @@ -64,13 +65,13 @@ namespace Microsoft.Net.Http.Headers return 1; } - if (!string.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase)) + if (!StringSegment.Equals(stringWithQuality1.Value, stringWithQuality2.Value, StringComparison.OrdinalIgnoreCase)) { - if (string.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal)) + if (StringSegment.Equals(stringWithQuality1.Value, "*", StringComparison.Ordinal)) { return -1; } - else if (string.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal)) + else if (StringSegment.Equals(stringWithQuality2.Value, "*", StringComparison.Ordinal)) { return 1; } diff --git a/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs index 89b1ab0f10..17f9623347 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/ContentDispositionHeaderValueTest.cs @@ -46,8 +46,8 @@ namespace Microsoft.Net.Http.Headers var contentDisposition = new ContentDispositionHeaderValue("inline"); Assert.Equal("inline", contentDisposition.DispositionType); Assert.Equal(0, contentDisposition.Parameters.Count); - Assert.Null(contentDisposition.Name); - Assert.Null(contentDisposition.FileName); + Assert.Null(contentDisposition.Name.Value); + Assert.Null(contentDisposition.FileName.Value); Assert.Null(contentDisposition.CreationDate); Assert.Null(contentDisposition.ModificationDate); Assert.Null(contentDisposition.ReadDate); @@ -81,7 +81,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("name", contentDisposition.Parameters.First().Name); contentDisposition.Name = null; - Assert.Null(contentDisposition.Name); + Assert.Null(contentDisposition.Name.Value); Assert.Equal(0, contentDisposition.Parameters.Count); contentDisposition.Name = null; // It's OK to set it again to null; no exception. } @@ -103,7 +103,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("NAME", contentDisposition.Parameters.First().Name); contentDisposition.Parameters.Remove(name); - Assert.Null(contentDisposition.Name); + Assert.Null(contentDisposition.Name.Value); } [Fact] @@ -123,7 +123,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); contentDisposition.Parameters.Remove(fileName); - Assert.Null(contentDisposition.FileName); + Assert.Null(contentDisposition.FileName.Value); } [Fact] @@ -138,7 +138,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("\"=?utf-8?B?RmlsZcODTmFtZS5iYXQ=?=\"", contentDisposition.Parameters.First().Value); contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); - Assert.Null(contentDisposition.FileName); + Assert.Null(contentDisposition.FileName.Value); } [Fact] @@ -160,7 +160,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("FILENAME", contentDisposition.Parameters.First().Name); contentDisposition.Parameters.Remove(fileName); - Assert.Null(contentDisposition.FileName); + Assert.Null(contentDisposition.FileName.Value); } [Fact] @@ -173,7 +173,7 @@ namespace Microsoft.Net.Http.Headers contentDisposition.Parameters.Add(fileNameStar); Assert.Equal(1, contentDisposition.Parameters.Count); Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); - Assert.Null(contentDisposition.FileNameStar); // Decode failure + Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure contentDisposition.FileNameStar = "new_name"; Assert.Equal("new_name", contentDisposition.FileNameStar); @@ -182,7 +182,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("UTF-8\'\'new_name", contentDisposition.Parameters.First().Value); contentDisposition.Parameters.Remove(fileNameStar); - Assert.Null(contentDisposition.FileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); } [Fact] @@ -197,7 +197,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("UTF-8\'\'File%C3%83Name.bat", contentDisposition.Parameters.First().Value); contentDisposition.Parameters.Remove(contentDisposition.Parameters.First()); - Assert.Null(contentDisposition.FileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); } [Fact] @@ -211,7 +211,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal(1, contentDisposition.Parameters.Count); Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); Assert.Equal("utf-99'lang'File%CZName.bat", contentDisposition.Parameters.First().Value); - Assert.Null(contentDisposition.FileNameStar); // Decode failure + Assert.Null(contentDisposition.FileNameStar.Value); // Decode failure contentDisposition.FileNameStar = "new_name"; Assert.Equal("new_name", contentDisposition.FileNameStar); @@ -219,7 +219,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("FILENAME*", contentDisposition.Parameters.First().Name); contentDisposition.Parameters.Remove(fileNameStar); - Assert.Null(contentDisposition.FileNameStar); + Assert.Null(contentDisposition.FileNameStar.Value); } [Fact] diff --git a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs index 0a72951240..1d5c9e9f65 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/MediaTypeHeaderValueTest.cs @@ -45,7 +45,7 @@ namespace Microsoft.Net.Http.Headers var mediaType = new MediaTypeHeaderValue("text/plain"); Assert.Equal("text/plain", mediaType.MediaType); Assert.Equal(0, mediaType.Parameters.Count); - Assert.Null(mediaType.Charset); + Assert.Null(mediaType.Charset.Value); } [Fact] @@ -70,7 +70,7 @@ namespace Microsoft.Net.Http.Headers var mediaType0 = new MediaTypeHeaderValue("text/plain"); var mediaType1 = mediaType0.Copy(); Assert.NotSame(mediaType0, mediaType1); - Assert.Same(mediaType0.MediaType, mediaType1.MediaType); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); } @@ -81,7 +81,7 @@ namespace Microsoft.Net.Http.Headers var mediaType0 = new MediaTypeHeaderValue("text/plain"); var mediaType1 = mediaType0.CopyAsReadOnly(); Assert.NotSame(mediaType0, mediaType1); - Assert.Same(mediaType0.MediaType, mediaType1.MediaType); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); @@ -97,14 +97,14 @@ namespace Microsoft.Net.Http.Headers mediaType0.Parameters.Add(new NameValueHeaderValue("name", "value")); var mediaType1 = mediaType0.Copy(); Assert.NotSame(mediaType0, mediaType1); - Assert.Same(mediaType0.MediaType, mediaType1.MediaType); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); Assert.Equal(mediaType0.Parameters.Count, mediaType1.Parameters.Count); var pair0 = mediaType0.Parameters.First(); var pair1 = mediaType1.Parameters.First(); Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name, pair1.Name); - Assert.Same(pair0.Value, pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); } [Fact] @@ -116,7 +116,7 @@ namespace Microsoft.Net.Http.Headers Assert.NotSame(mediaType0, mediaType1); Assert.False(mediaType0.IsReadOnly); Assert.True(mediaType1.IsReadOnly); - Assert.Same(mediaType0.MediaType, mediaType1.MediaType); + Assert.Same(mediaType0.MediaType.Value, mediaType1.MediaType.Value); Assert.NotSame(mediaType0.Parameters, mediaType1.Parameters); Assert.False(mediaType0.Parameters.IsReadOnly); @@ -131,8 +131,8 @@ namespace Microsoft.Net.Http.Headers Assert.NotSame(pair0, pair1); Assert.False(pair0.IsReadOnly); Assert.True(pair1.IsReadOnly); - Assert.Same(pair0.Name, pair1.Name); - Assert.Same(pair0.Value, pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); } [Fact] @@ -144,7 +144,7 @@ namespace Microsoft.Net.Http.Headers var mediaType2 = mediaType1.Copy(); Assert.NotSame(mediaType2, mediaType1); - Assert.Same(mediaType2.MediaType, mediaType1.MediaType); + Assert.Same(mediaType2.MediaType.Value, mediaType1.MediaType.Value); Assert.True(mediaType1.IsReadOnly); Assert.False(mediaType2.IsReadOnly); Assert.NotSame(mediaType2.Parameters, mediaType1.Parameters); @@ -154,8 +154,8 @@ namespace Microsoft.Net.Http.Headers Assert.NotSame(pair2, pair1); Assert.True(pair1.IsReadOnly); Assert.False(pair2.IsReadOnly); - Assert.Same(pair2.Name, pair1.Name); - Assert.Same(pair2.Value, pair1.Value); + Assert.Same(pair2.Name.Value, pair1.Name.Value); + Assert.Same(pair2.Value.Value, pair1.Value.Value); } [Fact] @@ -178,7 +178,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("charset", mediaType.Parameters.First().Name); mediaType.Charset = null; - Assert.Null(mediaType.Charset); + Assert.Null(mediaType.Charset.Value); Assert.Equal(0, mediaType.Parameters.Count); mediaType.Charset = null; // It's OK to set it again to null; no exception. } @@ -200,7 +200,7 @@ namespace Microsoft.Net.Http.Headers Assert.Equal("CHARSET", mediaType.Parameters.First().Name); mediaType.Parameters.Remove(charset); - Assert.Null(mediaType.Charset); + Assert.Null(mediaType.Charset.Value); } [Fact] diff --git a/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs b/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs index 9a8396f79d..8310fa2864 100644 --- a/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs +++ b/test/Microsoft.Net.Http.Headers.Tests/NameValueHeaderValueTest.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; @@ -65,14 +65,14 @@ namespace Microsoft.Net.Http.Headers var pair0 = new NameValueHeaderValue("name"); var pair1 = pair0.Copy(); Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name, pair1.Name); - Assert.Null(pair0.Value); - Assert.Null(pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Null(pair0.Value.Value); + Assert.Null(pair1.Value.Value); // Change one value and verify the other is unchanged. pair0.Value = "othervalue"; Assert.Equal("othervalue", pair0.Value); - Assert.Null(pair1.Value); + Assert.Null(pair1.Value.Value); } [Fact] @@ -81,16 +81,16 @@ namespace Microsoft.Net.Http.Headers var pair0 = new NameValueHeaderValue("name"); var pair1 = pair0.CopyAsReadOnly(); Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name, pair1.Name); - Assert.Null(pair0.Value); - Assert.Null(pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Null(pair0.Value.Value); + Assert.Null(pair1.Value.Value); Assert.False(pair0.IsReadOnly); Assert.True(pair1.IsReadOnly); // Change one value and verify the other is unchanged. pair0.Value = "othervalue"; Assert.Equal("othervalue", pair0.Value); - Assert.Null(pair1.Value); + Assert.Null(pair1.Value.Value); Assert.Throws(() => { pair1.Value = "othervalue"; }); } @@ -100,8 +100,8 @@ namespace Microsoft.Net.Http.Headers var pair0 = new NameValueHeaderValue("name", "value"); var pair1 = pair0.Copy(); Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name, pair1.Name); - Assert.Same(pair0.Value, pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); // Change one value and verify the other is unchanged. pair0.Value = "othervalue"; @@ -115,8 +115,8 @@ namespace Microsoft.Net.Http.Headers var pair0 = new NameValueHeaderValue("name", "value"); var pair1 = pair0.CopyAsReadOnly(); Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name, pair1.Name); - Assert.Same(pair0.Value, pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); Assert.False(pair0.IsReadOnly); Assert.True(pair1.IsReadOnly); @@ -134,8 +134,8 @@ namespace Microsoft.Net.Http.Headers var pair1 = pair0.CopyAsReadOnly(); var pair2 = pair1.Copy(); Assert.NotSame(pair0, pair1); - Assert.Same(pair0.Name, pair1.Name); - Assert.Same(pair0.Value, pair1.Value); + Assert.Same(pair0.Name.Value, pair1.Name.Value); + Assert.Same(pair0.Value.Value, pair1.Value.Value); // Change one value and verify the other is unchanged. pair2.Value = "othervalue";