From 4103be8eb7f4ba327a7af5d4426ada24d8dff5c6 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 25 Mar 2021 15:02:22 -0700 Subject: [PATCH 1/6] reduce allocations for dictionary --- src/Http/Headers/src/CookieHeaderParser.cs | 99 ++++++++++++++++++- src/Http/Headers/src/CookieHeaderValue.cs | 22 ++++- src/Http/Headers/src/PublicAPI.Unshipped.txt | 1 + .../src/Features/RequestCookiesFeature.cs | 2 +- .../src/Internal/RequestCookieCollection.cs | 23 ++--- 5 files changed, 125 insertions(+), 22 deletions(-) diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs index b77263bc18f7..490eab9a88cf 100644 --- a/src/Http/Headers/src/CookieHeaderParser.cs +++ b/src/Http/Headers/src/CookieHeaderParser.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 System; +using System.Collections.Generic; using System.Diagnostics.Contracts; using Microsoft.Extensions.Primitives; @@ -8,12 +10,59 @@ namespace Microsoft.Net.Http.Headers { internal sealed class CookieHeaderParser : HttpHeaderParser { + internal CookieHeaderParser(bool supportsMultipleValues) : base(supportsMultipleValues) { } - public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue) + public bool TryParseValues(StringValues values, bool strict, Dictionary store, bool enableCookieNameEncoding) + { + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + if (values.Count == 0) + { + return false; + } + + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + var index = 0; + + while (!string.IsNullOrEmpty(value) && index < value.Length) + { + (StringSegment, StringSegment)? output; + if (TryParseValue(value, ref index, out output)) + { + // The entry may not contain an actual value, like " , " + if (output != null) + { + var cookie = output.Value; + var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Item1.Value) : cookie.Item1.Value; + var valueString = Uri.UnescapeDataString(cookie.Item2.Value); + store[name] = valueString; + } + } + else if (strict) + { + // TODO may need to revert here changes to the dictionary. + return false; + } + else + { + // Skip the invalid values and keep trying. + index++; + } + } + } + + // TODO currently parsing no values is the same as failing to parse. + return true; + } + + + public bool TryParseValue(StringSegment value, ref int index, out (StringSegment, StringSegment)? parsedValue) { parsedValue = null; @@ -61,6 +110,54 @@ public override bool TryParseValue(StringSegment value, ref int index, out Cooki return true; } + public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue) + { + parsedValue = null; + + // If multiple values are supported (i.e. list of values), then accept an empty string: The header may + // be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + { + return SupportsMultipleValues; + } + + var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out bool separatorFound); + + if (separatorFound && !SupportsMultipleValues) + { + return false; // leading separators not allowed if we don't support multiple values. + } + + if (current == value.Length) + { + if (SupportsMultipleValues) + { + index = current; + } + return SupportsMultipleValues; + } + + if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out var result)) + { + return false; + } + + current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound); + + // If we support multiple values and we've not reached the end of the string, then we must have a separator. + if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) + { + return false; + } + + index = current; + parsedValue = new CookieHeaderValue(result.Value.Item1, result.Value.Item2); + return true; + } + private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) { Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs index f566b0082d96..8e91673fe51f 100644 --- a/src/Http/Headers/src/CookieHeaderValue.cs +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -165,20 +165,31 @@ public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); } + /// + /// Attempts to parse the sequence of values into a dictionary of cookies using string parsing rules. + /// + /// The values to parse. + /// + /// + /// + public static bool TryParseIntoDictionary(StringValues inputs, Dictionary store, bool enableCookieNameEncoding) + { + return MultipleValueParser.TryParseValues(inputs, strict: false, store, enableCookieNameEncoding); + } + // name=value; name="value" - internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out CookieHeaderValue? parsedValue) + internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out (StringSegment, StringSegment)? parsedValue) { Contract.Requires(offset >= 0); parsedValue = null; + (StringSegment, StringSegment) result = default; if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) { return false; } - var result = new CookieHeaderValue(); - // The caller should have already consumed any leading whitespace, commas, etc.. // Name=value; @@ -189,7 +200,8 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No { return false; } - result._name = input.Subsegment(offset, itemLength); + + result.Item1 = input.Subsegment(offset, itemLength); offset += itemLength; // = (no spaces) @@ -200,7 +212,7 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No // value or "quoted value" // The value may be empty - result._value = GetCookieValue(input, ref offset); + result.Item2 = GetCookieValue(input, ref offset); parsedValue = result; return true; diff --git a/src/Http/Headers/src/PublicAPI.Unshipped.txt b/src/Http/Headers/src/PublicAPI.Unshipped.txt index be7e9574bc0a..14aa95cfdbc0 100644 --- a/src/Http/Headers/src/PublicAPI.Unshipped.txt +++ b/src/Http/Headers/src/PublicAPI.Unshipped.txt @@ -2,5 +2,6 @@ *REMOVED*Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag) -> void Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void +static Microsoft.Net.Http.Headers.CookieHeaderValue.TryParseIntoDictionary(Microsoft.Extensions.Primitives.StringValues inputs, System.Collections.Generic.Dictionary! store, bool enableCookieNameEncoding) -> bool static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string! static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string! diff --git a/src/Http/Http/src/Features/RequestCookiesFeature.cs b/src/Http/Http/src/Features/RequestCookiesFeature.cs index 5abdfc906375..16c0758fbaf8 100644 --- a/src/Http/Http/src/Features/RequestCookiesFeature.cs +++ b/src/Http/Http/src/Features/RequestCookiesFeature.cs @@ -75,7 +75,7 @@ public IRequestCookieCollection Cookies if (_parsedValues == null || _original != current) { _original = current; - _parsedValues = RequestCookieCollection.Parse(current.ToArray()); + _parsedValues = RequestCookieCollection.Parse(current); } return _parsedValues; diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs index d6542a23d76c..ad79cc9941af 100644 --- a/src/Http/Http/src/Internal/RequestCookieCollection.cs +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Http @@ -56,33 +57,25 @@ public string? this[string key] } } - public static RequestCookieCollection Parse(IList values) - => ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled); + public static RequestCookieCollection Parse(StringValues values) + => ParseInternal(values, AppContext.TryGetSwitch(ResponseCookies.EnableCookieNameEncoding, out var enabled) && enabled); - internal static RequestCookieCollection ParseInternal(IList values, bool enableCookieNameEncoding) + internal static RequestCookieCollection ParseInternal(StringValues values, bool enableCookieNameEncoding) { if (values.Count == 0) { return Empty; } + var collection = new RequestCookieCollection(new Dictionary()); + var store = collection.Store!; - if (CookieHeaderValue.TryParseList(values, out var cookies)) + if (CookieHeaderValue.TryParseIntoDictionary(values, store, enableCookieNameEncoding)) { - if (cookies.Count == 0) + if (store.Count == 0) { return Empty; } - var collection = new RequestCookieCollection(cookies.Count); - var store = collection.Store!; - for (var i = 0; i < cookies.Count; i++) - { - var cookie = cookies[i]; - var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Name.Value) : cookie.Name.Value; - var value = Uri.UnescapeDataString(cookie.Value.Value); - store[name] = value; - } - return collection; } return Empty; From a7cfa6ca37b0df3afab487127b0740ae643dcd4f Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 25 Mar 2021 15:19:54 -0700 Subject: [PATCH 2/6] Reducing duplication --- src/Http/Headers/src/CookieHeaderParser.cs | 113 +++++++-------------- src/Http/Headers/src/CookieHeaderValue.cs | 2 +- 2 files changed, 40 insertions(+), 75 deletions(-) diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs index 490eab9a88cf..2a4cc64f5a15 100644 --- a/src/Http/Headers/src/CookieHeaderParser.cs +++ b/src/Http/Headers/src/CookieHeaderParser.cs @@ -10,58 +10,11 @@ namespace Microsoft.Net.Http.Headers { internal sealed class CookieHeaderParser : HttpHeaderParser { - internal CookieHeaderParser(bool supportsMultipleValues) : base(supportsMultipleValues) { } - public bool TryParseValues(StringValues values, bool strict, Dictionary store, bool enableCookieNameEncoding) - { - // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller - // can ignore the value. - if (values.Count == 0) - { - return false; - } - - for (var i = 0; i < values.Count; i++) - { - var value = values[i]; - var index = 0; - - while (!string.IsNullOrEmpty(value) && index < value.Length) - { - (StringSegment, StringSegment)? output; - if (TryParseValue(value, ref index, out output)) - { - // The entry may not contain an actual value, like " , " - if (output != null) - { - var cookie = output.Value; - var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Item1.Value) : cookie.Item1.Value; - var valueString = Uri.UnescapeDataString(cookie.Item2.Value); - store[name] = valueString; - } - } - else if (strict) - { - // TODO may need to revert here changes to the dictionary. - return false; - } - else - { - // Skip the invalid values and keep trying. - index++; - } - } - } - - // TODO currently parsing no values is the same as failing to parse. - return true; - } - - public bool TryParseValue(StringSegment value, ref int index, out (StringSegment, StringSegment)? parsedValue) { parsedValue = null; @@ -110,51 +63,63 @@ public bool TryParseValue(StringSegment value, ref int index, out (StringSegment return true; } - public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue) + public bool TryParseValues(StringValues values, Dictionary store, bool enableCookieNameEncoding) { - parsedValue = null; - - // If multiple values are supported (i.e. list of values), then accept an empty string: The header may - // be added multiple times to the request/response message. E.g. - // Accept: text/xml; q=1 - // Accept: - // Accept: text/plain; q=0.2 - if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + if (values.Count == 0) { - return SupportsMultipleValues; + return false; } + bool hasFoundValue = false; - var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out bool separatorFound); - - if (separatorFound && !SupportsMultipleValues) + for (var i = 0; i < values.Count; i++) { - return false; // leading separators not allowed if we don't support multiple values. - } + var value = values[i]; + var index = 0; - if (current == value.Length) - { - if (SupportsMultipleValues) + while (!string.IsNullOrEmpty(value) && index < value.Length) { - index = current; + (StringSegment, StringSegment)? output; + if (TryParseValue(value, ref index, out output)) + { + // The entry may not contain an actual value, like " , " + if (output != null) + { + var cookie = output.Value; + var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Item1.Value) : cookie.Item1.Value; + var valueString = Uri.UnescapeDataString(cookie.Item2.Value); + store[name] = valueString; + hasFoundValue = true; + } + } + else + { + // Skip the invalid values and keep trying. + index++; + } } - return SupportsMultipleValues; } - if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out var result)) + return hasFoundValue; + } + + public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue) + { + parsedValue = null; + + if (!TryParseValue(value, ref index, out var stringSegments)) { return false; } - current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound); - - // If we support multiple values and we've not reached the end of the string, then we must have a separator. - if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) + if (stringSegments == null) { return false; } - index = current; - parsedValue = new CookieHeaderValue(result.Value.Item1, result.Value.Item2); + parsedValue = new CookieHeaderValue(stringSegments.Value.Item1, stringSegments.Value.Item2); + return true; } diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs index 8e91673fe51f..88c4fc2683be 100644 --- a/src/Http/Headers/src/CookieHeaderValue.cs +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -174,7 +174,7 @@ public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] /// public static bool TryParseIntoDictionary(StringValues inputs, Dictionary store, bool enableCookieNameEncoding) { - return MultipleValueParser.TryParseValues(inputs, strict: false, store, enableCookieNameEncoding); + return MultipleValueParser.TryParseValues(inputs, store, enableCookieNameEncoding); } // name=value; name="value" From 4480aecced6a3ca572d05621de7c1ed605f4b916 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 25 Mar 2021 17:45:27 -0700 Subject: [PATCH 3/6] Benchmark --- src/Http/Headers/src/CookieHeaderParser.cs | 32 +++++++++---------- src/Http/Headers/src/CookieHeaderValue.cs | 9 +++--- .../RequestCookieCollectionBenchmarks.cs | 25 +++++++++++++++ .../RouteValueDictionaryBenchmark.cs | 2 +- src/Http/Http/src/Properties/AssemblyInfo.cs | 1 + 5 files changed, 47 insertions(+), 22 deletions(-) create mode 100644 src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs index 2a4cc64f5a15..620d98ee5cae 100644 --- a/src/Http/Headers/src/CookieHeaderParser.cs +++ b/src/Http/Headers/src/CookieHeaderParser.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using Microsoft.Extensions.Primitives; @@ -15,8 +16,9 @@ internal CookieHeaderParser(bool supportsMultipleValues) { } - public bool TryParseValue(StringSegment value, ref int index, out (StringSegment, StringSegment)? parsedValue) + public bool TryParseValue(StringSegment value, ref int index, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) { + parsedName = null; parsedValue = null; // If multiple values are supported (i.e. list of values), then accept an empty string: The header may @@ -45,7 +47,7 @@ public bool TryParseValue(StringSegment value, ref int index, out (StringSegment return SupportsMultipleValues; } - if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out var result)) + if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) { return false; } @@ -59,7 +61,7 @@ public bool TryParseValue(StringSegment value, ref int index, out (StringSegment } index = current; - parsedValue = result; + return true; } @@ -80,16 +82,13 @@ public bool TryParseValues(StringValues values, Dictionary store while (!string.IsNullOrEmpty(value) && index < value.Length) { - (StringSegment, StringSegment)? output; - if (TryParseValue(value, ref index, out output)) + if (TryParseValue(value, ref index, out var parsedName, out var parsedValue)) { // The entry may not contain an actual value, like " , " - if (output != null) + if (parsedName != null && parsedValue != null) { - var cookie = output.Value; - var name = enableCookieNameEncoding ? Uri.UnescapeDataString(cookie.Item1.Value) : cookie.Item1.Value; - var valueString = Uri.UnescapeDataString(cookie.Item2.Value); - store[name] = valueString; + var name = enableCookieNameEncoding ? Uri.UnescapeDataString(parsedName.Value.Value) : parsedName.Value.Value; + store[name] = Uri.UnescapeDataString(parsedValue.Value.Value); hasFoundValue = true; } } @@ -104,21 +103,22 @@ public bool TryParseValues(StringValues values, Dictionary store return hasFoundValue; } - public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? parsedValue) + public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? cookieValue) { - parsedValue = null; + cookieValue = null; - if (!TryParseValue(value, ref index, out var stringSegments)) + if (!TryParseValue(value, ref index, out var parsedName, out var parsedValue)) { return false; } - if (stringSegments == null) + if (parsedName == null || parsedValue == null) { - return false; + // Successfully parsed, but no values. + return true; } - parsedValue = new CookieHeaderValue(stringSegments.Value.Item1, stringSegments.Value.Item2); + cookieValue = new CookieHeaderValue(parsedName.Value, parsedValue.Value); return true; } diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs index 88c4fc2683be..7aa478744c1e 100644 --- a/src/Http/Headers/src/CookieHeaderValue.cs +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -178,12 +178,12 @@ public static bool TryParseIntoDictionary(StringValues inputs, Dictionary= 0); + parsedName = null; parsedValue = null; - (StringSegment, StringSegment) result = default; if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) { @@ -201,7 +201,7 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No return false; } - result.Item1 = input.Subsegment(offset, itemLength); + parsedName = input.Subsegment(offset, itemLength); offset += itemLength; // = (no spaces) @@ -212,9 +212,8 @@ internal static bool TryGetCookieLength(StringSegment input, ref int offset, [No // value or "quoted value" // The value may be empty - result.Item2 = GetCookieValue(input, ref offset); + parsedValue = GetCookieValue(input, ref offset); - parsedValue = result; return true; } diff --git a/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs b/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs new file mode 100644 index 000000000000..035a367b1324 --- /dev/null +++ b/src/Http/Http/perf/Microbenchmarks/RequestCookieCollectionBenchmarks.cs @@ -0,0 +1,25 @@ +// 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 BenchmarkDotNet.Attributes; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Http +{ + public class RequestCookieCollectionBenchmarks + { + private StringValues _cookie; + + [IterationSetup] + public void Setup() + { + _cookie = ".AspNetCore.Cookies=CfDJ8BAklVa9EYREk8_ipRUUYJYhRsleKr485k18s_q5XD6vcRJ-DtowUuLCwwMiY728zRZ3rVFY3DEcXDAQUOTtg1e4tkSIVmYLX38Q6mqdFFyw-8dksclDywe9vnN84cEWvfV0wP3EgOsJGHaND7kTJ47gr7Pc1tLHWOm4Pe7Q1vrT9EkcTMr1Wts3aptBl3bdOLLqjmSdgk-OI7qG7uQGz1OGdnSer6-KLUPBcfXblzs4YCjvwu3bGnM42xLGtkZNIF8izPpyqKkIf7ec6O6LEHMp4gcq86PGHCXHn5NKuNSD"; + } + + [Benchmark] + public void Parse_TypicalCookie() + { + RequestCookieCollection.Parse(_cookie); + } + } +} diff --git a/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs b/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs index 2dfc36afa4cc..76a8659c2a48 100644 --- a/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.cs +++ b/src/Http/Http/perf/Microbenchmarks/RouteValueDictionaryBenchmark.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; diff --git a/src/Http/Http/src/Properties/AssemblyInfo.cs b/src/Http/Http/src/Properties/AssemblyInfo.cs index 2b8d94f4a543..21cadcc624da 100644 --- a/src/Http/Http/src/Properties/AssemblyInfo.cs +++ b/src/Http/Http/src/Properties/AssemblyInfo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Http.MicroBenchmarks, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] From 687a330d7968ed8fb1b4991b1c129d9f1c4cc5dd Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 25 Mar 2021 18:24:03 -0700 Subject: [PATCH 4/6] Nit --- src/Http/Http/src/Internal/RequestCookieCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs index ad79cc9941af..7dc266a9a137 100644 --- a/src/Http/Http/src/Internal/RequestCookieCollection.cs +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -66,7 +66,7 @@ internal static RequestCookieCollection ParseInternal(StringValues values, bool { return Empty; } - var collection = new RequestCookieCollection(new Dictionary()); + var collection = new RequestCookieCollection(values.Count); var store = collection.Store!; if (CookieHeaderValue.TryParseIntoDictionary(values, store, enableCookieNameEncoding)) From f5713fc55c47dfedd4992b7b2047f132a314f4e3 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Fri, 26 Mar 2021 12:03:21 -0700 Subject: [PATCH 5/6] Remove public --- src/Http/Headers/src/CookieHeaderParser.cs | 124 +--------------- src/Http/Headers/src/CookieHeaderValue.cs | 12 -- .../src/Microsoft.Net.Http.Headers.csproj | 1 + src/Http/Headers/src/PublicAPI.Unshipped.txt | 1 - .../src/Internal/RequestCookieCollection.cs | 2 +- .../Http/src/Microsoft.AspNetCore.Http.csproj | 3 +- src/Http/Shared/CookieHeaderParserShared.cs | 132 ++++++++++++++++++ 7 files changed, 137 insertions(+), 138 deletions(-) create mode 100644 src/Http/Shared/CookieHeaderParserShared.cs diff --git a/src/Http/Headers/src/CookieHeaderParser.cs b/src/Http/Headers/src/CookieHeaderParser.cs index 620d98ee5cae..cb05706921cc 100644 --- a/src/Http/Headers/src/CookieHeaderParser.cs +++ b/src/Http/Headers/src/CookieHeaderParser.cs @@ -1,10 +1,6 @@ // 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; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using Microsoft.Extensions.Primitives; namespace Microsoft.Net.Http.Headers @@ -16,98 +12,11 @@ internal CookieHeaderParser(bool supportsMultipleValues) { } - public bool TryParseValue(StringSegment value, ref int index, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) - { - parsedName = null; - parsedValue = null; - - // If multiple values are supported (i.e. list of values), then accept an empty string: The header may - // be added multiple times to the request/response message. E.g. - // Accept: text/xml; q=1 - // Accept: - // Accept: text/plain; q=0.2 - if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) - { - return SupportsMultipleValues; - } - - var current = GetNextNonEmptyOrWhitespaceIndex(value, index, SupportsMultipleValues, out bool separatorFound); - - if (separatorFound && !SupportsMultipleValues) - { - return false; // leading separators not allowed if we don't support multiple values. - } - - if (current == value.Length) - { - if (SupportsMultipleValues) - { - index = current; - } - return SupportsMultipleValues; - } - - if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) - { - return false; - } - - current = GetNextNonEmptyOrWhitespaceIndex(value, current, SupportsMultipleValues, out separatorFound); - - // If we support multiple values and we've not reached the end of the string, then we must have a separator. - if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length))) - { - return false; - } - - index = current; - - return true; - } - - public bool TryParseValues(StringValues values, Dictionary store, bool enableCookieNameEncoding) - { - // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller - // can ignore the value. - if (values.Count == 0) - { - return false; - } - bool hasFoundValue = false; - - for (var i = 0; i < values.Count; i++) - { - var value = values[i]; - var index = 0; - - while (!string.IsNullOrEmpty(value) && index < value.Length) - { - if (TryParseValue(value, ref index, out var parsedName, out var parsedValue)) - { - // The entry may not contain an actual value, like " , " - if (parsedName != null && parsedValue != null) - { - var name = enableCookieNameEncoding ? Uri.UnescapeDataString(parsedName.Value.Value) : parsedName.Value.Value; - store[name] = Uri.UnescapeDataString(parsedValue.Value.Value); - hasFoundValue = true; - } - } - else - { - // Skip the invalid values and keep trying. - index++; - } - } - } - - return hasFoundValue; - } - public override bool TryParseValue(StringSegment value, ref int index, out CookieHeaderValue? cookieValue) { cookieValue = null; - if (!TryParseValue(value, ref index, out var parsedName, out var parsedValue)) + if (!CookieHeaderParserShared.TryParseValue(value, ref index, SupportsMultipleValues, out var parsedName, out var parsedValue)) { return false; } @@ -122,36 +31,5 @@ public override bool TryParseValue(StringSegment value, ref int index, out Cooki return true; } - - private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) - { - Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. - - separatorFound = false; - var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); - - if ((current == input.Length) || (input[current] != ',' && input[current] != ';')) - { - return current; - } - - // If we have a separator, skip the separator and all following whitespaces. If we support - // empty values, continue until the current character is neither a separator nor a whitespace. - separatorFound = true; - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - - if (skipEmptyValues) - { - // Most headers only split on ',', but cookies primarily split on ';' - while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';'))) - { - current++; // skip delimiter. - current = current + HttpRuleParser.GetWhitespaceLength(input, current); - } - } - - return current; - } } } diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs index 7aa478744c1e..05f7ff7a2c5a 100644 --- a/src/Http/Headers/src/CookieHeaderValue.cs +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -165,18 +165,6 @@ public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); } - /// - /// Attempts to parse the sequence of values into a dictionary of cookies using string parsing rules. - /// - /// The values to parse. - /// - /// - /// - public static bool TryParseIntoDictionary(StringValues inputs, Dictionary store, bool enableCookieNameEncoding) - { - return MultipleValueParser.TryParseValues(inputs, store, enableCookieNameEncoding); - } - // name=value; name="value" internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) { diff --git a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj index 1bd100563caa..e0ab4a7fe7f1 100644 --- a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj +++ b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Http/Headers/src/PublicAPI.Unshipped.txt b/src/Http/Headers/src/PublicAPI.Unshipped.txt index 14aa95cfdbc0..be7e9574bc0a 100644 --- a/src/Http/Headers/src/PublicAPI.Unshipped.txt +++ b/src/Http/Headers/src/PublicAPI.Unshipped.txt @@ -2,6 +2,5 @@ *REMOVED*Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue? entityTag) -> void Microsoft.Net.Http.Headers.MediaTypeHeaderValue.MatchesMediaType(Microsoft.Extensions.Primitives.StringSegment otherMediaType) -> bool Microsoft.Net.Http.Headers.RangeConditionHeaderValue.RangeConditionHeaderValue(Microsoft.Net.Http.Headers.EntityTagHeaderValue! entityTag) -> void -static Microsoft.Net.Http.Headers.CookieHeaderValue.TryParseIntoDictionary(Microsoft.Extensions.Primitives.StringValues inputs, System.Collections.Generic.Dictionary! store, bool enableCookieNameEncoding) -> bool static readonly Microsoft.Net.Http.Headers.HeaderNames.Baggage -> string! static readonly Microsoft.Net.Http.Headers.HeaderNames.ProxyConnection -> string! diff --git a/src/Http/Http/src/Internal/RequestCookieCollection.cs b/src/Http/Http/src/Internal/RequestCookieCollection.cs index 7dc266a9a137..f7ae17212887 100644 --- a/src/Http/Http/src/Internal/RequestCookieCollection.cs +++ b/src/Http/Http/src/Internal/RequestCookieCollection.cs @@ -69,7 +69,7 @@ internal static RequestCookieCollection ParseInternal(StringValues values, bool var collection = new RequestCookieCollection(values.Count); var store = collection.Store!; - if (CookieHeaderValue.TryParseIntoDictionary(values, store, enableCookieNameEncoding)) + if (CookieHeaderParserShared.TryParseValues(values, store, enableCookieNameEncoding, supportsMultipleValues: true)) { if (store.Count == 0) { diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index 3a2eaae79951..d1d55362686a 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core default HTTP feature implementations. @@ -15,6 +15,7 @@ + diff --git a/src/Http/Shared/CookieHeaderParserShared.cs b/src/Http/Shared/CookieHeaderParserShared.cs new file mode 100644 index 000000000000..3a7a4b7fb66e --- /dev/null +++ b/src/Http/Shared/CookieHeaderParserShared.cs @@ -0,0 +1,132 @@ +// 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; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.Net.Http.Headers +{ + internal static class CookieHeaderParserShared + { + public static bool TryParseValues(StringValues values, Dictionary store, bool enableCookieNameEncoding, bool supportsMultipleValues) + { + // If a parser returns an empty list, it means there was no value, but that's valid (e.g. "Accept: "). The caller + // can ignore the value. + if (values.Count == 0) + { + return false; + } + bool hasFoundValue = false; + + for (var i = 0; i < values.Count; i++) + { + var value = values[i]; + var index = 0; + + while (!string.IsNullOrEmpty(value) && index < value.Length) + { + if (TryParseValue(value, ref index, supportsMultipleValues, out var parsedName, out var parsedValue)) + { + // The entry may not contain an actual value, like " , " + if (parsedName != null && parsedValue != null) + { + var name = enableCookieNameEncoding ? Uri.UnescapeDataString(parsedName.Value.Value) : parsedName.Value.Value; + store[name] = Uri.UnescapeDataString(parsedValue.Value.Value); + hasFoundValue = true; + } + } + else + { + // Skip the invalid values and keep trying. + index++; + } + } + } + + return hasFoundValue; + } + + public static bool TryParseValue(StringSegment value, ref int index, bool supportsMultipleValues, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) + { + parsedName = null; + parsedValue = null; + + // If multiple values are supported (i.e. list of values), then accept an empty string: The header may + // be added multiple times to the request/response message. E.g. + // Accept: text/xml; q=1 + // Accept: + // Accept: text/plain; q=0.2 + if (StringSegment.IsNullOrEmpty(value) || (index == value.Length)) + { + return supportsMultipleValues; + } + + var current = GetNextNonEmptyOrWhitespaceIndex(value, index, supportsMultipleValues, out bool separatorFound); + + if (separatorFound && !supportsMultipleValues) + { + return false; // leading separators not allowed if we don't support multiple values. + } + + if (current == value.Length) + { + if (supportsMultipleValues) + { + index = current; + } + return supportsMultipleValues; + } + + if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) + { + return false; + } + + current = GetNextNonEmptyOrWhitespaceIndex(value, current, supportsMultipleValues, out separatorFound); + + // If we support multiple values and we've not reached the end of the string, then we must have a separator. + if ((separatorFound && !supportsMultipleValues) || (!separatorFound && (current < value.Length))) + { + return false; + } + + index = current; + + return true; + } + + private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int startIndex, bool skipEmptyValues, out bool separatorFound) + { + Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. + + separatorFound = false; + var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); + + if ((current == input.Length) || (input[current] != ',' && input[current] != ';')) + { + return current; + } + + // If we have a separator, skip the separator and all following whitespaces. If we support + // empty values, continue until the current character is neither a separator nor a whitespace. + separatorFound = true; + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + + if (skipEmptyValues) + { + // Most headers only split on ',', but cookies primarily split on ';' + while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';'))) + { + current++; // skip delimiter. + current = current + HttpRuleParser.GetWhitespaceLength(input, current); + } + } + + return current; + } + } +} From f16ef53d2a31ad2a23c894ea19e74c1326ccd275 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Fri, 26 Mar 2021 12:13:05 -0700 Subject: [PATCH 6/6] Some missed files --- src/Http/Headers/src/CookieHeaderValue.cs | 116 +----------------- .../src/Microsoft.Net.Http.Headers.csproj | 2 + src/Http/Headers/src/SetCookieHeaderValue.cs | 2 +- .../Http/src/Microsoft.AspNetCore.Http.csproj | 2 + src/Http/Shared/CookieHeaderParserShared.cs | 116 +++++++++++++++++- .../src => Shared}/HttpParseResult.cs | 0 .../{Headers/src => Shared}/HttpRuleParser.cs | 0 7 files changed, 121 insertions(+), 117 deletions(-) rename src/Http/{Headers/src => Shared}/HttpParseResult.cs (100%) rename src/Http/{Headers/src => Shared}/HttpRuleParser.cs (100%) diff --git a/src/Http/Headers/src/CookieHeaderValue.cs b/src/Http/Headers/src/CookieHeaderValue.cs index 05f7ff7a2c5a..c8b6ab5505c0 100644 --- a/src/Http/Headers/src/CookieHeaderValue.cs +++ b/src/Http/Headers/src/CookieHeaderValue.cs @@ -165,120 +165,6 @@ public static bool TryParseStrictList(IList? inputs, [NotNullWhen(true)] return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues); } - // name=value; name="value" - internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) - { - Contract.Requires(offset >= 0); - - parsedName = null; - parsedValue = null; - - if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) - { - return false; - } - - // The caller should have already consumed any leading whitespace, commas, etc.. - - // Name=value; - - // Name - var itemLength = HttpRuleParser.GetTokenLength(input, offset); - if (itemLength == 0) - { - return false; - } - - parsedName = input.Subsegment(offset, itemLength); - offset += itemLength; - - // = (no spaces) - if (!ReadEqualsSign(input, ref offset)) - { - return false; - } - - // value or "quoted value" - // The value may be empty - parsedValue = GetCookieValue(input, ref offset); - - return true; - } - - // 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 StringSegment GetCookieValue(StringSegment input, ref int offset) - { - Contract.Requires(offset >= 0); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - offset))); - - var startIndex = offset; - - if (offset >= input.Length) - { - return StringSegment.Empty; - } - var inQuotes = false; - - if (input[offset] == '"') - { - inQuotes = true; - offset++; - } - - while (offset < input.Length) - { - var c = input[offset]; - if (!IsCookieValueChar(c)) - { - break; - } - - offset++; - } - - if (inQuotes) - { - if (offset == input.Length || input[offset] != '"') - { - // Missing final quote - return StringSegment.Empty; - } - offset++; - } - - int length = offset - startIndex; - if (offset > startIndex) - { - return input.Subsegment(startIndex, length); - } - - return StringSegment.Empty; - } - - private static bool ReadEqualsSign(StringSegment input, ref int offset) - { - // = (no spaces) - if (offset >= input.Length || input[offset] != '=') - { - return false; - } - offset++; - return true; - } - - // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E - // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash - private static bool IsCookieValueChar(char c) - { - if (c < 0x21 || c > 0x7E) - { - return false; - } - return !(c == '"' || c == ',' || c == ';' || c == '\\'); - } - internal static void CheckNameFormat(StringSegment name, string parameterName) { if (name == null) @@ -300,7 +186,7 @@ internal static void CheckValueFormat(StringSegment value, string parameterName) } var offset = 0; - var result = GetCookieValue(value, ref offset); + var result = CookieHeaderParserShared.GetCookieValue(value, ref offset); if (result.Length != value.Length) { throw new ArgumentException("Invalid cookie value: " + value, parameterName); diff --git a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj index e0ab4a7fe7f1..c5036c490f5a 100644 --- a/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj +++ b/src/Http/Headers/src/Microsoft.Net.Http.Headers.csproj @@ -14,6 +14,8 @@ + + diff --git a/src/Http/Headers/src/SetCookieHeaderValue.cs b/src/Http/Headers/src/SetCookieHeaderValue.cs index b0874a7bbb62..11a71634c0cd 100644 --- a/src/Http/Headers/src/SetCookieHeaderValue.cs +++ b/src/Http/Headers/src/SetCookieHeaderValue.cs @@ -494,7 +494,7 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S // value or "quoted value" // The value may be empty - result._value = CookieHeaderValue.GetCookieValue(input, ref offset); + result._value = CookieHeaderParserShared.GetCookieValue(input, ref offset); // *(';' SP cookie-av) while (offset < input.Length) diff --git a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj index d1d55362686a..18e06e184ca4 100644 --- a/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj +++ b/src/Http/Http/src/Microsoft.AspNetCore.Http.csproj @@ -16,6 +16,8 @@ + + diff --git a/src/Http/Shared/CookieHeaderParserShared.cs b/src/Http/Shared/CookieHeaderParserShared.cs index 3a7a4b7fb66e..061fe0b52874 100644 --- a/src/Http/Shared/CookieHeaderParserShared.cs +++ b/src/Http/Shared/CookieHeaderParserShared.cs @@ -80,7 +80,7 @@ public static bool TryParseValue(StringSegment value, ref int index, bool suppor return supportsMultipleValues; } - if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) + if (!TryGetCookieLength(value, ref current, out parsedName, out parsedValue)) { return false; } @@ -128,5 +128,119 @@ private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int sta return current; } + + // name=value; name="value" + internal static bool TryGetCookieLength(StringSegment input, ref int offset, [NotNullWhen(true)] out StringSegment? parsedName, [NotNullWhen(true)] out StringSegment? parsedValue) + { + Contract.Requires(offset >= 0); + + parsedName = null; + parsedValue = null; + + if (StringSegment.IsNullOrEmpty(input) || (offset >= input.Length)) + { + return false; + } + + // The caller should have already consumed any leading whitespace, commas, etc.. + + // Name=value; + + // Name + var itemLength = HttpRuleParser.GetTokenLength(input, offset); + if (itemLength == 0) + { + return false; + } + + parsedName = input.Subsegment(offset, itemLength); + offset += itemLength; + + // = (no spaces) + if (!ReadEqualsSign(input, ref offset)) + { + return false; + } + + // value or "quoted value" + // The value may be empty + parsedValue = GetCookieValue(input, ref offset); + + return true; + } + + // 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 StringSegment GetCookieValue(StringSegment input, ref int offset) + { + Contract.Requires(offset >= 0); + Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - offset))); + + var startIndex = offset; + + if (offset >= input.Length) + { + return StringSegment.Empty; + } + var inQuotes = false; + + if (input[offset] == '"') + { + inQuotes = true; + offset++; + } + + while (offset < input.Length) + { + var c = input[offset]; + if (!IsCookieValueChar(c)) + { + break; + } + + offset++; + } + + if (inQuotes) + { + if (offset == input.Length || input[offset] != '"') + { + // Missing final quote + return StringSegment.Empty; + } + offset++; + } + + int length = offset - startIndex; + if (offset > startIndex) + { + return input.Subsegment(startIndex, length); + } + + return StringSegment.Empty; + } + + private static bool ReadEqualsSign(StringSegment input, ref int offset) + { + // = (no spaces) + if (offset >= input.Length || input[offset] != '=') + { + return false; + } + offset++; + return true; + } + + // cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E + // ; US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash + private static bool IsCookieValueChar(char c) + { + if (c < 0x21 || c > 0x7E) + { + return false; + } + return !(c == '"' || c == ',' || c == ';' || c == '\\'); + } } } diff --git a/src/Http/Headers/src/HttpParseResult.cs b/src/Http/Shared/HttpParseResult.cs similarity index 100% rename from src/Http/Headers/src/HttpParseResult.cs rename to src/Http/Shared/HttpParseResult.cs diff --git a/src/Http/Headers/src/HttpRuleParser.cs b/src/Http/Shared/HttpRuleParser.cs similarity index 100% rename from src/Http/Headers/src/HttpRuleParser.cs rename to src/Http/Shared/HttpRuleParser.cs