NameValueHeaderValue Escaping/Unescaping quoted-strings and helpers (#913)
This commit is contained in:
parent
e5825641ce
commit
1e8a22dae3
|
|
@ -594,13 +594,133 @@ namespace Microsoft.Net.Http.Headers
|
|||
|
||||
public static StringSegment RemoveQuotes(StringSegment input)
|
||||
{
|
||||
if (!StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"')
|
||||
if (IsQuoted(input))
|
||||
{
|
||||
input = input.Subsegment(1, input.Length - 2);
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static bool IsQuoted(StringSegment input)
|
||||
{
|
||||
return !StringSegment.IsNullOrEmpty(input) && input.Length >= 2 && input[0] == '"' && input[input.Length - 1] == '"';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a quoted-string as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>,
|
||||
/// removes quotes and unescapes backslashes and quotes. This assumes that the input is a valid quoted-string.
|
||||
/// </summary>
|
||||
/// <param name="input">The quoted-string to be unescaped.</param>
|
||||
/// <returns>An unescaped version of the quoted-string.</returns>
|
||||
public static StringSegment UnescapeAsQuotedString(StringSegment input)
|
||||
{
|
||||
input = RemoveQuotes(input);
|
||||
|
||||
// First pass to calculate the size of the InplaceStringBuilder
|
||||
var backSlashCount = CountBackslashesForDecodingQuotedString(input);
|
||||
|
||||
if (backSlashCount == 0)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var stringBuilder = new InplaceStringBuilder(input.Length - backSlashCount);
|
||||
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
if (i < input.Length - 1 && input[i] == '\\')
|
||||
{
|
||||
// If there is an backslash character as the last character in the string,
|
||||
// we will assume that it should be included literally in the unescaped string
|
||||
// Ex: "hello\\" => "hello\\"
|
||||
// Also, if a sender adds a quoted pair like '\\''n',
|
||||
// we will assume it is over escaping and just add a n to the string.
|
||||
// Ex: "he\\llo" => "hello"
|
||||
stringBuilder.Append(input[i + 1]);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
stringBuilder.Append(input[i]);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static int CountBackslashesForDecodingQuotedString(StringSegment input)
|
||||
{
|
||||
var numberBackSlashes = 0;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
if (i < input.Length - 1 && input[i] == '\\')
|
||||
{
|
||||
// If there is an backslash character as the last character in the string,
|
||||
// we will assume that it should be included literally in the unescaped string
|
||||
// Ex: "hello\\" => "hello\\"
|
||||
// Also, if a sender adds a quoted pair like '\\''n',
|
||||
// we will assume it is over escaping and just add a n to the string.
|
||||
// Ex: "he\\llo" => "hello"
|
||||
if (input[i + 1] == '\\')
|
||||
{
|
||||
// Only count escaped backslashes once
|
||||
i++;
|
||||
}
|
||||
numberBackSlashes++;
|
||||
}
|
||||
}
|
||||
return numberBackSlashes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes a <see cref="StringSegment"/> as a quoted-string, which is defined by
|
||||
/// <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will add a backslash before each backslash and quote and add quotes
|
||||
/// around the input. Assumes that the input does not have quotes around it,
|
||||
/// as this method will add them. Throws if the input contains any invalid escape characters,
|
||||
/// as defined by rfc7230.
|
||||
/// </remarks>
|
||||
/// <param name="input">The input to be escaped.</param>
|
||||
/// <returns>An escaped version of the quoted-string.</returns>
|
||||
public static StringSegment EscapeAsQuotedString(StringSegment input)
|
||||
{
|
||||
// By calling this, we know that the string requires quotes around it to be a valid token.
|
||||
var backSlashCount = CountAndCheckCharactersNeedingBackslashesWhenEncoding(input);
|
||||
|
||||
var stringBuilder = new InplaceStringBuilder(input.Length + backSlashCount + 2); // 2 for quotes
|
||||
stringBuilder.Append('\"');
|
||||
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
if (input[i] == '\\' || input[i] == '\"')
|
||||
{
|
||||
stringBuilder.Append('\\');
|
||||
}
|
||||
else if ((input[i] <= 0x1F || input[i] == 0x7F) && input[i] != 0x09)
|
||||
{
|
||||
// Control characters are not allowed in a quoted-string, which include all characters
|
||||
// below 0x1F (except for 0x09 (TAB)) and 0x7F.
|
||||
throw new FormatException($"Invalid control character '{input[i]}' in input.");
|
||||
}
|
||||
stringBuilder.Append(input[i]);
|
||||
}
|
||||
stringBuilder.Append('\"');
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
|
||||
private static int CountAndCheckCharactersNeedingBackslashesWhenEncoding(StringSegment input)
|
||||
{
|
||||
var numberOfCharactersNeedingEscaping = 0;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
if (input[i] == '\\' || input[i] == '\"')
|
||||
{
|
||||
numberOfCharactersNeedingEscaping++;
|
||||
}
|
||||
}
|
||||
return numberOfCharactersNeedingEscaping;
|
||||
}
|
||||
|
||||
internal static void ThrowIfReadOnly(bool isReadOnly)
|
||||
{
|
||||
if (isReadOnly)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
|
|
@ -142,6 +143,28 @@ namespace Microsoft.Net.Http.Headers
|
|||
}
|
||||
}
|
||||
|
||||
public StringSegment GetUnescapedValue()
|
||||
{
|
||||
if (!HeaderUtilities.IsQuoted(_value))
|
||||
{
|
||||
return _value;
|
||||
}
|
||||
return HeaderUtilities.UnescapeAsQuotedString(_value);
|
||||
}
|
||||
|
||||
public void SetAndEscapeValue(StringSegment value)
|
||||
{
|
||||
HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
|
||||
if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Value = HeaderUtilities.EscapeAsQuotedString(value);
|
||||
}
|
||||
}
|
||||
|
||||
public static NameValueHeaderValue Parse(StringSegment input)
|
||||
{
|
||||
var index = 0;
|
||||
|
|
@ -390,7 +413,7 @@ namespace Microsoft.Net.Http.Headers
|
|||
// Either value is null/empty or a valid token/quoted string
|
||||
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));
|
||||
throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -210,5 +210,76 @@ namespace Microsoft.Net.Http.Headers
|
|||
Assert.True(HeaderUtilities.TryParseNonNegativeInt32(valueString, out value));
|
||||
Assert.Equal(expected, value);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\"hello\"", "hello")]
|
||||
[InlineData("\"hello", "\"hello")]
|
||||
[InlineData("hello\"", "hello\"")]
|
||||
[InlineData("\"\"hello\"\"", "\"hello\"")]
|
||||
public void RemoveQuotes_BehaviorCheck(string input, string expected)
|
||||
{
|
||||
var actual = HeaderUtilities.RemoveQuotes(input);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData("\"hello\"", true)]
|
||||
[InlineData("\"hello", false)]
|
||||
[InlineData("hello\"", false)]
|
||||
[InlineData("\"\"hello\"\"", true)]
|
||||
public void IsQuoted_BehaviorCheck(string input, bool expected)
|
||||
{
|
||||
var actual = HeaderUtilities.IsQuoted(input);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("value", "value")]
|
||||
[InlineData("\"value\"", "value")]
|
||||
[InlineData("\"hello\\\\\"", "hello\\")]
|
||||
[InlineData("\"hello\\\"\"", "hello\"")]
|
||||
[InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")]
|
||||
[InlineData("\"quoted value\"", "quoted value")]
|
||||
[InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")]
|
||||
[InlineData("\"hello\\\"", "hello\\")]
|
||||
public void UnescapeAsQuotedString_BehaviorCheck(string input, string expected)
|
||||
{
|
||||
var actual = HeaderUtilities.UnescapeAsQuotedString(input);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("value", "\"value\"")]
|
||||
[InlineData("23", "\"23\"")]
|
||||
[InlineData(";;;", "\";;;\"")]
|
||||
[InlineData("\"value\"", "\"\\\"value\\\"\"")]
|
||||
[InlineData("unquoted \"value", "\"unquoted \\\"value\"")]
|
||||
[InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")]
|
||||
// We have to assume that the input needs to be quoted here
|
||||
[InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")]
|
||||
[InlineData("\t", "\"\t\"")]
|
||||
public void SetAndEscapeValue_BehaviorCheck(string input, string expected)
|
||||
{
|
||||
var actual = HeaderUtilities.EscapeAsQuotedString(input);
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("\n")]
|
||||
[InlineData("\b")]
|
||||
[InlineData("\r")]
|
||||
public void SetAndEscapeValue_ControlCharactersThrowFormatException(string input)
|
||||
{
|
||||
Assert.Throws<FormatException>(() => { var actual = HeaderUtilities.EscapeAsQuotedString(input); });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetAndEscapeValue_ThrowsFormatExceptionOnDelCharacter()
|
||||
{
|
||||
Assert.Throws<FormatException>(() => { var actual = HeaderUtilities.EscapeAsQuotedString($"{(char)0x7F}"); });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -575,6 +575,87 @@ namespace Microsoft.Net.Http.Headers
|
|||
Assert.False(NameValueHeaderValue.TryParseStrictList(inputs, out results));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("value", "value")]
|
||||
[InlineData("\"value\"", "value")]
|
||||
[InlineData("\"hello\\\\\"", "hello\\")]
|
||||
[InlineData("\"hello\\\"\"", "hello\"")]
|
||||
[InlineData("\"hello\\\"foo\\\\bar\\\\baz\\\\\"", "hello\"foo\\bar\\baz\\")]
|
||||
[InlineData("\"quoted value\"", "quoted value")]
|
||||
[InlineData("\"quoted\\\"valuewithquote\"", "quoted\"valuewithquote")]
|
||||
[InlineData("\"hello\\\"", "hello\\")]
|
||||
public void GetUnescapedValue_ReturnsExpectedValue(string input, string expected)
|
||||
{
|
||||
var header = new NameValueHeaderValue("test", input);
|
||||
|
||||
var actual = header.GetUnescapedValue();
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("value", "value")]
|
||||
[InlineData("23", "23")]
|
||||
[InlineData(";;;", "\";;;\"")]
|
||||
[InlineData("\"value\"", "\"value\"")]
|
||||
[InlineData("\"assumes already encoded \\\"\"", "\"assumes already encoded \\\"\"")]
|
||||
[InlineData("unquoted \"value", "\"unquoted \\\"value\"")]
|
||||
[InlineData("value\\morevalues\\evenmorevalues", "\"value\\\\morevalues\\\\evenmorevalues\"")]
|
||||
// We have to assume that the input needs to be quoted here
|
||||
[InlineData("\"\"double quoted string\"\"", "\"\\\"\\\"double quoted string\\\"\\\"\"")]
|
||||
[InlineData("\t", "\"\t\"")]
|
||||
public void SetAndEscapeValue_ReturnsExpectedValue(string input, string expected)
|
||||
{
|
||||
var header = new NameValueHeaderValue("test");
|
||||
header.SetAndEscapeValue(input);
|
||||
|
||||
var actual = header.Value;
|
||||
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
|
||||
|
||||
[Theory]
|
||||
[InlineData("\n")]
|
||||
[InlineData("\b")]
|
||||
[InlineData("\r")]
|
||||
public void SetAndEscapeValue_ThrowsOnInvalidValues(string input)
|
||||
{
|
||||
var header = new NameValueHeaderValue("test");
|
||||
Assert.Throws<FormatException>(() => header.SetAndEscapeValue(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("value")]
|
||||
[InlineData("\"value\\\\morevalues\\\\evenmorevalues\"")]
|
||||
[InlineData("\"quoted \\\"value\"")]
|
||||
public void GetAndSetEncodeValueRoundTrip_ReturnsExpectedValue(string input)
|
||||
{
|
||||
var header = new NameValueHeaderValue("test");
|
||||
header.Value = input;
|
||||
var valueHeader = header.GetUnescapedValue();
|
||||
header.SetAndEscapeValue(valueHeader);
|
||||
|
||||
var actual = header.Value;
|
||||
|
||||
Assert.Equal(input, actual);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("val\\nue")]
|
||||
[InlineData("val\\bue")]
|
||||
public void OverescapingValuesDoNotRoundTrip(string input)
|
||||
{
|
||||
var header = new NameValueHeaderValue("test");
|
||||
header.SetAndEscapeValue(input);
|
||||
var valueHeader = header.GetUnescapedValue();
|
||||
|
||||
var actual = header.Value;
|
||||
|
||||
Assert.NotEqual(input, actual);
|
||||
}
|
||||
|
||||
|
||||
#region Helper methods
|
||||
|
||||
private void CheckValidParse(string input, NameValueHeaderValue expectedResult)
|
||||
|
|
|
|||
Loading…
Reference in New Issue