Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deduplicate header Separator logic #100179

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,7 @@ private static bool EncodeLiteralHeaderNewNameCore(byte mask, string name, strin
}

/// <summary>Encodes a "Literal Header Field without Indexing - New Name".</summary>
public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, string separator, Span<byte> destination, out int bytesWritten)
{
return EncodeLiteralHeaderFieldWithoutIndexingNewName(name, values, separator, valueEncoding: null, destination, out bytesWritten);
}

public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, string separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
public static bool EncodeLiteralHeaderFieldWithoutIndexingNewName(string name, ReadOnlySpan<string> values, byte[] separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
{
// From https://tools.ietf.org/html/rfc7541#section-6.2.2
// ------------------------------------------------------
Expand Down Expand Up @@ -515,12 +510,7 @@ public static bool EncodeDynamicTableSizeUpdate(int value, Span<byte> destinatio
return false;
}

public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string? separator, Span<byte> destination, out int bytesWritten)
{
return EncodeStringLiterals(values, separator, valueEncoding: null, destination, out bytesWritten);
}

public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string? separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
public static bool EncodeStringLiterals(ReadOnlySpan<string> values, byte[]? separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
{
bytesWritten = 0;

Expand All @@ -536,23 +526,22 @@ public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string? sep
if (destination.Length != 0)
{
Debug.Assert(separator != null);
int valueLength;
Debug.Assert(Ascii.IsValid(separator));
int valueLength = checked((values.Length - 1) * separator.Length);

// Calculate length of all parts and separators.
// Calculate length of all values.
if (valueEncoding is null || ReferenceEquals(valueEncoding, Encoding.Latin1))
{
valueLength = checked((int)(values.Length - 1) * separator.Length);
foreach (string part in values)
{
valueLength = checked((int)(valueLength + part.Length));
valueLength = checked(valueLength + part.Length);
}
}
else
{
valueLength = checked((int)(values.Length - 1) * valueEncoding.GetByteCount(separator));
foreach (string part in values)
{
valueLength = checked((int)(valueLength + valueEncoding.GetByteCount(part)));
valueLength = checked(valueLength + valueEncoding.GetByteCount(part));
}
}

Expand All @@ -571,7 +560,7 @@ public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string? sep

for (int i = 1; i < values.Length; i++)
{
EncodeValueStringPart(separator, destination);
separator.CopyTo(destination);
destination = destination.Slice(separator.Length);

value = values[i];
Expand All @@ -586,8 +575,8 @@ public static bool EncodeStringLiterals(ReadOnlySpan<string> values, string? sep

for (int i = 1; i < values.Length; i++)
{
written = valueEncoding.GetBytes(separator, destination);
destination = destination.Slice(written);
separator.CopyTo(destination);
destination = destination.Slice(separator.Length);

written = valueEncoding.GetBytes(values[i], destination);
destination = destination.Slice(written);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,9 @@ public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, str
/// <summary>
/// Encodes a Literal Header Field Without Name Reference, building the value by concatenating a collection of strings with separators.
/// </summary>
public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, ReadOnlySpan<string> values, string valueSeparator, Span<byte> destination, out int bytesWritten)
public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, ReadOnlySpan<string> values, byte[] separator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
{
return EncodeLiteralHeaderFieldWithoutNameReference(name, values, valueSeparator, valueEncoding: null, destination, out bytesWritten);
}

public static bool EncodeLiteralHeaderFieldWithoutNameReference(string name, ReadOnlySpan<string> values, string valueSeparator, Encoding? valueEncoding, Span<byte> destination, out int bytesWritten)
{
if (EncodeNameString(name, destination, out int nameLength) && EncodeValueString(values, valueSeparator, valueEncoding, destination.Slice(nameLength), out int valueLength))
if (EncodeNameString(name, destination, out int nameLength) && EncodeValueString(values, separator, valueEncoding, destination.Slice(nameLength), out int valueLength))
{
bytesWritten = nameLength + valueLength;
return true;
Expand Down Expand Up @@ -222,12 +217,7 @@ private static bool EncodeValueString(string s, Encoding? valueEncoding, Span<by
/// <summary>
/// Encodes a value by concatenating a collection of strings, separated by a separator string.
/// </summary>
public static bool EncodeValueString(ReadOnlySpan<string> values, string? separator, Span<byte> buffer, out int length)
{
return EncodeValueString(values, separator, valueEncoding: null, buffer, out length);
}

public static bool EncodeValueString(ReadOnlySpan<string> values, string? separator, Encoding? valueEncoding, Span<byte> buffer, out int length)
public static bool EncodeValueString(ReadOnlySpan<string> values, byte[]? separator, Encoding? valueEncoding, Span<byte> buffer, out int length)
{
if (values.Length == 1)
{
Expand All @@ -243,18 +233,18 @@ public static bool EncodeValueString(ReadOnlySpan<string> values, string? separa
if (buffer.Length > 0)
{
Debug.Assert(separator != null);
int valueLength;
Debug.Assert(Ascii.IsValid(separator));
int valueLength = separator.Length * (values.Length - 1);

if (valueEncoding is null || ReferenceEquals(valueEncoding, Encoding.Latin1))
{
valueLength = separator.Length * (values.Length - 1);
foreach (string part in values)
{
valueLength += part.Length;
}
}
else
{
valueLength = valueEncoding.GetByteCount(separator) * (values.Length - 1);
foreach (string part in values)
{
valueLength += valueEncoding.GetByteCount(part);
Expand All @@ -275,7 +265,7 @@ public static bool EncodeValueString(ReadOnlySpan<string> values, string? separa

for (int i = 1; i < values.Length; i++)
{
EncodeValueStringPart(separator, buffer);
separator.CopyTo(buffer);
buffer = buffer.Slice(separator.Length);

value = values[i];
Expand All @@ -290,8 +280,8 @@ public static bool EncodeValueString(ReadOnlySpan<string> values, string? separa

for (int i = 1; i < values.Length; i++)
{
written = valueEncoding.GetBytes(separator, buffer);
buffer = buffer.Slice(written);
separator.CopyTo(buffer);
buffer = buffer.Slice(separator.Length);

written = valueEncoding.GetBytes(values[i], buffer);
buffer = buffer.Slice(written);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,5 +277,9 @@ private static bool TryDecodeUtf8(ReadOnlySpan<byte> input, [NotNullWhen(true)]
decoded = null;
return false;
}

public string Separator => Parser is { } parser ? parser.Separator : HttpHeaderParser.DefaultSeparator;

public byte[] SeparatorBytes => Parser is { } parser ? parser.SeparatorBytes : HttpHeaderParser.DefaultSeparatorBytes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal HeaderStringValues(HeaderDescriptor descriptor, string[] values)
public override string ToString() => _value switch
{
string value => value,
string[] values => string.Join(_header.Parser is HttpHeaderParser parser && parser.SupportsMultipleValues ? parser.Separator : HttpHeaderParser.DefaultSeparator, values),
string[] values => string.Join(_header.Separator, values),
_ => string.Empty,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,42 @@
using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;

namespace System.Net.Http.Headers
{
internal abstract class HttpHeaderParser
{
internal const string DefaultSeparator = ", ";
public const string DefaultSeparator = ", ";
public static readonly byte[] DefaultSeparatorBytes = ", "u8.ToArray();

private readonly bool _supportsMultipleValues;
private readonly string? _separator;
public bool SupportsMultipleValues { get; private set; }

public bool SupportsMultipleValues
{
get { return _supportsMultipleValues; }
}
public string Separator { get; private set; }

public string? Separator
{
get
{
Debug.Assert(_supportsMultipleValues);
return _separator;
}
}
public byte[] SeparatorBytes { get; private set; }

// If ValueType implements Equals() as required, there is no need to provide a comparer. A comparer is needed
// e.g. if we want to compare strings using case-insensitive comparison.
public virtual IEqualityComparer? Comparer
{
get { return null; }
}
public virtual IEqualityComparer? Comparer => null;

protected HttpHeaderParser(bool supportsMultipleValues)
{
_supportsMultipleValues = supportsMultipleValues;

if (supportsMultipleValues)
{
_separator = DefaultSeparator;
}
SupportsMultipleValues = supportsMultipleValues;
Separator = DefaultSeparator;
SeparatorBytes = DefaultSeparatorBytes;
}

protected HttpHeaderParser(bool supportsMultipleValues, string separator)
protected HttpHeaderParser(bool supportsMultipleValues, string separator) : this(supportsMultipleValues)
{
Debug.Assert(!string.IsNullOrEmpty(separator));
Debug.Assert(Ascii.IsValid(separator));

_supportsMultipleValues = supportsMultipleValues;
_separator = separator;
if (supportsMultipleValues)
{
Separator = separator;
SeparatorBytes = Encoding.ASCII.GetBytes(separator);
}
}

// If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ public override string ToString()
{
// Note that if we get multiple values for a header that doesn't support multiple values, we'll
// just separate the values using a comma (default separator).
string? separator = entry.Key.Parser is HttpHeaderParser parser && parser.SupportsMultipleValues ? parser.Separator : HttpHeaderParser.DefaultSeparator;
string separator = entry.Key.Separator;

Debug.Assert(multiValue is not null && multiValue.Length > 0);
vsb.Append(multiValue[0]);
Expand Down Expand Up @@ -289,8 +289,7 @@ internal string GetHeaderString(HeaderDescriptor descriptor)

// Note that if we get multiple values for a header that doesn't support multiple values, we'll
// just separate the values using a comma (default separator).
string? separator = descriptor.Parser != null && descriptor.Parser.SupportsMultipleValues ? descriptor.Parser.Separator : HttpHeaderParser.DefaultSeparator;
return string.Join(separator, multiValue!);
return string.Join(descriptor.Separator, multiValue!);
}

return string.Empty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1365,17 +1365,17 @@ private void WriteLiteralHeader(string name, ReadOnlySpan<string> values, Encodi
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(name)}={name}, {nameof(values)}={string.Join(", ", values.ToArray())}");

int bytesWritten;
while (!HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, values, HttpHeaderParser.DefaultSeparator, valueEncoding, headerBuffer.AvailableSpan, out bytesWritten))
while (!HPackEncoder.EncodeLiteralHeaderFieldWithoutIndexingNewName(name, values, HttpHeaderParser.DefaultSeparatorBytes, valueEncoding, headerBuffer.AvailableSpan, out bytesWritten))
{
headerBuffer.Grow();
}

headerBuffer.Commit(bytesWritten);
}

private void WriteLiteralHeaderValues(ReadOnlySpan<string> values, string? separator, Encoding? valueEncoding, ref ArrayBuffer headerBuffer)
private void WriteLiteralHeaderValues(ReadOnlySpan<string> values, byte[]? separator, Encoding? valueEncoding, ref ArrayBuffer headerBuffer)
{
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(values)}={string.Join(separator, values.ToArray())}");
if (NetEventSource.Log.IsEnabled()) Trace($"{nameof(values)}={string.Join(Encoding.ASCII.GetString(separator ?? []), values.ToArray())}");

int bytesWritten;
while (!HPackEncoder.EncodeStringLiterals(values, separator, valueEncoding, headerBuffer.AvailableSpan, out bytesWritten))
Expand Down Expand Up @@ -1464,19 +1464,8 @@ private int WriteHeaderCollection(HttpRequestMessage request, HttpHeaders header

// For all other known headers, send them via their pre-encoded name and the associated value.
WriteBytes(knownHeader.Http2EncodedName, ref headerBuffer);
string? separator = null;
if (headerValues.Length > 1)
{
HttpHeaderParser? parser = header.Key.Parser;
if (parser != null && parser.SupportsMultipleValues)
{
separator = parser.Separator;
}
else
{
separator = HttpHeaderParser.DefaultSeparator;
}
}

byte[]? separator = headerValues.Length > 1 ? header.Key.SeparatorBytes : null;

WriteLiteralHeaderValues(headerValues, separator, valueEncoding, ref headerBuffer);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,27 +708,16 @@ private int BufferHeaderCollection(HttpHeaders headers)

// For all other known headers, send them via their pre-encoded name and the associated value.
BufferBytes(knownHeader.Http3EncodedName);
string? separator = null;
if (headerValues.Length > 1)
{
HttpHeaderParser? parser = header.Key.Parser;
if (parser != null && parser.SupportsMultipleValues)
{
separator = parser.Separator;
}
else
{
separator = HttpHeaderParser.DefaultSeparator;
}
}

byte[]? separator = headerValues.Length > 1 ? header.Key.SeparatorBytes : null;

BufferLiteralHeaderValues(headerValues, separator, valueEncoding);
}
}
else
{
// The header is not known: fall back to just encoding the header name and value(s).
BufferLiteralHeaderWithoutNameReference(header.Key.Name, headerValues, HttpHeaderParser.DefaultSeparator, valueEncoding);
BufferLiteralHeaderWithoutNameReference(header.Key.Name, headerValues, HttpHeaderParser.DefaultSeparatorBytes, valueEncoding);
}
}

Expand All @@ -755,7 +744,7 @@ private void BufferLiteralHeaderWithStaticNameReference(int nameIndex, string va
_sendBuffer.Commit(bytesWritten);
}

private void BufferLiteralHeaderWithoutNameReference(string name, ReadOnlySpan<string> values, string separator, Encoding? valueEncoding)
private void BufferLiteralHeaderWithoutNameReference(string name, ReadOnlySpan<string> values, byte[] separator, Encoding? valueEncoding)
{
int bytesWritten;
while (!QPackEncoder.EncodeLiteralHeaderFieldWithoutNameReference(name, values, separator, valueEncoding, _sendBuffer.AvailableSpan, out bytesWritten))
Expand All @@ -775,7 +764,7 @@ private void BufferLiteralHeaderWithoutNameReference(string name, string value,
_sendBuffer.Commit(bytesWritten);
}

private void BufferLiteralHeaderValues(ReadOnlySpan<string> values, string? separator, Encoding? valueEncoding)
private void BufferLiteralHeaderValues(ReadOnlySpan<string> values, byte[]? separator, Encoding? valueEncoding)
{
int bytesWritten;
while (!QPackEncoder.EncodeValueString(values, separator, valueEncoding, _sendBuffer.AvailableSpan, out bytesWritten))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,16 +417,11 @@ private void WriteHeaderCollection(HttpHeaders headers, string? cookiesFromConta
// Some headers such as User-Agent and Server use space as a separator (see: ProductInfoHeaderParser)
if (headerValuesCount > 1)
{
HttpHeaderParser? parser = header.Key.Parser;
string separator = HttpHeaderParser.DefaultSeparator;
if (parser != null && parser.SupportsMultipleValues)
{
separator = parser.Separator!;
}
byte[] separator = header.Key.SeparatorBytes;

for (int i = 1; i < headerValuesCount; i++)
{
WriteAsciiString(separator);
WriteBytes(separator);
WriteString(headerValues[i], valueEncoding);
}
}
Expand Down
Loading