NameValueHeaderValue Escaping/Unescaping quoted-strings and helpers (#913)

This commit is contained in:
Justin Kotalik 2017-08-30 14:03:12 -07:00 committed by GitHub
parent e5825641ce
commit 1e8a22dae3
4 changed files with 297 additions and 2 deletions

View File

@ -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)

View File

@ -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));
}
}

View File

@ -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}"); });
}
}
}

View File

@ -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)