Commit 27a645c7 authored by Ian Clelland's avatar Ian Clelland Committed by Commit Bot

Add serializer for HTTP Structured Headers.

This serializes the values returned from parsing structured headers,
according to https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13.

Bug: 1011101
Change-Id: I33c0a80bd7cbe17c96bf9ef9803af32a1a7506ef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1872940
Commit-Queue: Ian Clelland <iclelland@chromium.org>
Reviewed-by: default avatarJeremy Roman <jbroman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#714677}
parent 86604615
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
#include "base/base64.h" #include "base/base64.h"
#include "base/containers/flat_set.h" #include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h" #include "base/strings/string_util.h"
...@@ -31,6 +32,11 @@ constexpr char kKeyChars13[] = DIGIT LCALPHA "_-*"; ...@@ -31,6 +32,11 @@ constexpr char kKeyChars13[] = DIGIT LCALPHA "_-*";
#undef LCALPHA #undef LCALPHA
#undef UCALPHA #undef UCALPHA
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13#section-3.4
// TODO(1011101): Add support for negative integers.
constexpr int64_t kMaxInteger = 999999999999999L;
constexpr int64_t kMinInteger = 0L;
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-3.8 // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-3.8
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13#section-3.6 // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13#section-3.6
bool IsPrintableASCII(char c) { bool IsPrintableASCII(char c) {
...@@ -306,6 +312,9 @@ class StructuredHeaderParser { ...@@ -306,6 +312,9 @@ class StructuredHeaderParser {
int64_t n; int64_t n;
if (!base::StringToInt64(output_number_string, &n)) if (!base::StringToInt64(output_number_string, &n))
return base::nullopt; return base::nullopt;
// [SH13] restricts the range of integers further.
if (version_ == kDraft13 && n > kMaxInteger)
return base::nullopt;
return Item(n); return Item(n);
} }
...@@ -397,6 +406,128 @@ class StructuredHeaderParser { ...@@ -397,6 +406,128 @@ class StructuredHeaderParser {
DISALLOW_COPY_AND_ASSIGN(StructuredHeaderParser); DISALLOW_COPY_AND_ASSIGN(StructuredHeaderParser);
}; };
// Serializer for (a subset of) Structured Headers for HTTP defined in [SH13].
// [SH13] https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13
class StructuredHeaderSerializer {
public:
StructuredHeaderSerializer() = default;
~StructuredHeaderSerializer() = default;
StructuredHeaderSerializer(const StructuredHeaderSerializer&) = delete;
StructuredHeaderSerializer& operator=(const StructuredHeaderSerializer&) =
delete;
std::string Output() { return output_.str(); }
bool WriteList(const List& value) {
// Serializes a List ([SH13] 4.1.1).
bool first = true;
for (const auto& member : value) {
if (!first)
output_ << ", ";
if (!WriteParameterizedMember(member))
return false;
first = false;
}
return true;
}
bool WriteItem(const Item& value) {
// Serializes an Item ([SH13] 4.1.6).
if (value.is_string()) {
output_ << "\"";
for (const char& c : value.string()) {
if (!IsPrintableASCII(c))
return false;
if (c == '\\' || c == '\"')
output_ << "\\";
output_ << c;
}
output_ << "\"";
return true;
}
if (value.is_token()) {
if (!value.string().size() || !base::IsAsciiAlpha(value.string().front()))
return false;
if (value.string().find_first_not_of(kTokenChars) != std::string::npos)
return false;
output_ << value.string();
return true;
}
if (value.is_byte_sequence()) {
output_ << "*";
output_ << base::Base64Encode(
base::as_bytes(base::make_span(value.string())));
output_ << "*";
return true;
}
if (value.is_integer()) {
if (value.integer() > kMaxInteger || value.integer() < kMinInteger)
return false;
output_ << value.integer();
return true;
}
return false;
}
private:
bool WriteParameterizedMember(const ParameterizedMember& value) {
// Serializes a parameterized member ([SH13] 4.1.1).
if (value.member_is_inner_list) {
if (!WriteInnerList(value.member))
return false;
} else {
DCHECK_EQ(value.member.size(), 1UL);
if (!WriteItem(value.member[0]))
return false;
}
return WriteParameters(value.params);
}
bool WriteInnerList(const std::vector<Item>& value) {
// Serializes an inner list ([SH13] 4.1.1.1).
output_ << "(";
bool first = true;
for (const Item& member : value) {
if (!first)
output_ << " ";
if (!WriteItem(member))
return false;
first = false;
}
output_ << ")";
return true;
}
bool WriteParameters(const ParameterizedMember::Parameters& value) {
// Serializes a parameter list ([SH13] 4.1.1.2).
for (const auto& param_name_and_value : value) {
const std::string& param_name = param_name_and_value.first;
const Item& param_value = param_name_and_value.second;
output_ << ";";
if (!WriteKey(param_name))
return false;
if (!param_value.is_null()) {
output_ << "=";
if (!WriteItem(param_value))
return false;
}
}
return true;
}
bool WriteKey(const std::string& value) {
// Serializes a Key ([SH13] 4.1.1.3).
if (!value.size())
return false;
if (value.find_first_not_of(kKeyChars13) != std::string::npos)
return false;
output_ << value;
return true;
}
std::ostringstream output_;
};
} // namespace } // namespace
Item::Item() {} Item::Item() {}
...@@ -452,7 +583,7 @@ ParameterisedIdentifier::ParameterisedIdentifier(Item id, const Parameters& ps) ...@@ -452,7 +583,7 @@ ParameterisedIdentifier::ParameterisedIdentifier(Item id, const Parameters& ps)
ParameterisedIdentifier::~ParameterisedIdentifier() = default; ParameterisedIdentifier::~ParameterisedIdentifier() = default;
base::Optional<Item> ParseItem(const base::StringPiece& str) { base::Optional<Item> ParseItem(const base::StringPiece& str) {
StructuredHeaderParser parser(str, StructuredHeaderParser::kDraft09); StructuredHeaderParser parser(str, StructuredHeaderParser::kDraft13);
base::Optional<Item> item = parser.ReadItem(); base::Optional<Item> item = parser.ReadItem();
if (item && parser.FinishParsing()) if (item && parser.FinishParsing())
return item; return item;
...@@ -484,5 +615,19 @@ base::Optional<List> ParseList(const base::StringPiece& str) { ...@@ -484,5 +615,19 @@ base::Optional<List> ParseList(const base::StringPiece& str) {
return base::nullopt; return base::nullopt;
} }
base::Optional<std::string> SerializeItem(const Item& value) {
StructuredHeaderSerializer s;
if (s.WriteItem(value))
return s.Output();
return base::nullopt;
}
base::Optional<std::string> SerializeList(const List& value) {
StructuredHeaderSerializer s;
if (s.WriteList(value))
return s.Output();
return base::nullopt;
}
} // namespace http_structured_header } // namespace http_structured_header
} // namespace blink } // namespace blink
...@@ -30,137 +30,162 @@ std::pair<std::string, Item> Param(std::string key, std::string value) { ...@@ -30,137 +30,162 @@ std::pair<std::string, Item> Param(std::string key, std::string value) {
return std::make_pair(key, Item(value)); return std::make_pair(key, Item(value));
} }
} // namespace // Most test cases are taken from
// https://github.com/httpwg/structured-header-tests.
const struct ItemTestCase {
const char* name;
const char* raw;
const base::Optional<Item> expected; // nullopt if parse error is expected.
const char* canonical; // nullptr if parse error is expected, or if canonical
// format is identical to raw.
} item_test_cases[] = {
// Token
{"basic token - item", "a_b-c.d3:f%00/*", Token("a_b-c.d3:f%00/*")},
{"token with capitals - item", "fooBar", Token("fooBar")},
{"token starting with capitals - item", "FooBar", Token("FooBar")},
{"bad token - item", "abc$%!", base::nullopt},
{"leading whitespace", " foo", Token("foo"), "foo"},
{"trailing whitespace", "foo ", Token("foo"), "foo"},
// Number
{"basic integer", "42", Item(42)},
{"zero integer", "0", Item(0)},
{"comma", "2,3", base::nullopt},
{"long integer", "999999999999999", Item(999999999999999L)},
{"too long integer", "1000000000000000", base::nullopt},
// Byte Sequence
{"basic binary", "*aGVsbG8=*", Item("hello", Item::kByteSequenceType)},
{"empty binary", "**", Item("", Item::kByteSequenceType)},
{"bad paddding", "*aGVsbG8*", Item("hello", Item::kByteSequenceType),
"*aGVsbG8=*"},
{"bad end delimiter", "*aGVsbG8=", base::nullopt},
{"extra whitespace", "*aGVsb G8=*", base::nullopt},
{"extra chars", "*aGVsbG!8=*", base::nullopt},
{"suffix chars", "*aGVsbG8=!*", base::nullopt},
{"non-zero pad bits", "*iZ==*", Item("\x89", Item::kByteSequenceType),
"*iQ==*"},
{"non-ASCII binary", "*/+Ah*", Item("\xFF\xE0!", Item::kByteSequenceType)},
{"base64url binary", "*_-Ah*", base::nullopt},
// String
{"basic string", "\"foo\"", Item("foo")},
{"empty string", "\"\"", Item("")},
{"long string",
"\"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo \"",
Item("foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo ")},
{"whitespace string", "\" \"", Item(" ")},
{"non-ascii string", "\"f\xC3\xBC\xC3\xBC\"", base::nullopt},
{"tab in string", "\"\t\"", base::nullopt},
{"newline in string", "\" \n \"", base::nullopt},
{"single quoted string", "'foo'", base::nullopt},
{"unbalanced string", "\"foo", base::nullopt},
{"string quoting", "\"foo \\\"bar\\\" \\\\ baz\"",
Item("foo \"bar\" \\ baz")},
{"bad string quoting", "\"foo \\,\"", base::nullopt},
{"ending string quote", "\"foo \\\"", base::nullopt},
{"abruptly ending string quote", "\"foo \\", base::nullopt},
// Additional tests
{"valid quoting containing \\n", "\"\\\\n\"", Item("\\n")},
{"valid quoting containing \\t", "\"\\\\t\"", Item("\\t")},
{"valid quoting containing \\x", "\"\\\\x61\"", Item("\\x61")},
{"c-style hex escape in string", "\"\\x61\"", base::nullopt},
{"valid quoting containing \\u", "\"\\\\u0061\"", Item("\\u0061")},
{"c-style unicode escape in string", "\"\\u0061\"", base::nullopt},
};
// Test cases are taken from https://github.com/httpwg/structured-header-tests. // For Structured Headers Draft 13
const struct ListTestCase {
const char* name;
const char* raw;
const base::Optional<List> expected; // nullopt if parse error is expected.
const char* canonical; // nullptr if parse error is expected, or if canonical
// format is identical to raw.
} list_test_cases[] = {
// Basic lists
{"basic list", "1, 42", {{{Item(1), {}}, {Item(42), {}}}}},
{"empty list", "", List()},
{"single item list", "42", {{{Item(42), {}}}}},
{"no whitespace list", "1,42", {{{Item(1), {}}, {Item(42), {}}}}, "1, 42"},
{"trailing comma list", "1, 42,", base::nullopt},
{"empty item list", "1,,42", base::nullopt},
// Lists of lists
{"basic list of lists",
"(1 2), (42 43)",
{{{{Item(1), Item(2)}, {}}, {{Item(42), Item(43)}, {}}}}},
{"single item list of lists",
"(42)",
{{{std::vector<Item>{Item(42)}, {}}}}},
{"empty item list of lists", "()", {{{std::vector<Item>(), {}}}}},
{"empty middle item list of lists",
"(1),(),(42)",
{{{std::vector<Item>{Item(1)}, {}},
{std::vector<Item>(), {}},
{std::vector<Item>{Item(42)}, {}}}},
"(1), (), (42)"},
{"extra whitespace list of lists",
"(1 42)",
{{{{Item(1), Item(42)}, {}}}},
"(1 42)"},
{"no trailing parenthesis list of lists", "(1 42", base::nullopt},
{"no trailing parenthesis middle list of lists", "(1 2, (42 43)",
base::nullopt},
// Parameterized Lists
{"basic parameterised list",
"abc_123;a=1;b=2; cdef_456, ghi;q=\"9\";r=\"w\"",
{{{Token("abc_123"), {Param("a", 1), Param("b", 2), Param("cdef_456")}},
{Token("ghi"), {Param("q", "9"), Param("r", "w")}}}},
"abc_123;a=1;b=2;cdef_456, ghi;q=\"9\";r=\"w\""},
{"single item parameterised list",
"text/html;q=1",
{{{Token("text/html"), {Param("q", 1)}}}}},
{"no whitespace parameterised list",
"text/html,text/plain;q=1",
{{{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}},
"text/html, text/plain;q=1"},
{"whitespace before = parameterised list", "text/html, text/plain;q =1",
base::nullopt},
{"whitespace after = parameterised list", "text/html, text/plain;q= 1",
base::nullopt},
{"extra whitespace param-list",
"text/html , text/plain ; q=1",
{{{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}},
"text/html, text/plain;q=1"},
{"empty item parameterised list", "text/html,,text/plain;q=1",
base::nullopt},
};
} // namespace
TEST(StructuredHeaderTest, ParseItem) { TEST(StructuredHeaderTest, ParseItem) {
struct TestCase { for (const auto& c : item_test_cases) {
const char* name;
const char* raw;
const base::Optional<Item> expected; // nullopt if parse error is expected
} cases[] = {
// Item
{"basic token - item", "a_b-c.d3:f%00/*", Token("a_b-c.d3:f%00/*")},
{"token with capitals - item", "fooBar", Token("fooBar")},
{"token starting with capitals - item", "FooBar", Token("FooBar")},
{"bad token - item", "abc$%!", base::nullopt},
{"leading whitespace", " foo", Token("foo")},
{"trailing whitespace", "foo ", Token("foo")},
// Number
{"basic integer", "42", Item(42)},
{"zero integer", "0", Item(0)},
{"comma", "2,3", base::nullopt},
{"long integer", "9223372036854775807", Item(9223372036854775807L)},
{"too long integer", "9223372036854775808", base::nullopt},
// Byte Sequence
{"basic binary", "*aGVsbG8=*", Item("hello", Item::kByteSequenceType)},
{"empty binary", "**", Item("", Item::kByteSequenceType)},
{"bad paddding", "*aGVsbG8*", Item("hello", Item::kByteSequenceType)},
{"bad end delimiter", "*aGVsbG8=", base::nullopt},
{"extra whitespace", "*aGVsb G8=*", base::nullopt},
{"extra chars", "*aGVsbG!8=*", base::nullopt},
{"suffix chars", "*aGVsbG8=!*", base::nullopt},
{"non-zero pad bits", "*iZ==*", Item("\x89", Item::kByteSequenceType)},
{"non-ASCII binary", "*/+Ah*",
Item("\xFF\xE0!", Item::kByteSequenceType)},
{"base64url binary", "*_-Ah*", base::nullopt},
// String
{"basic string", "\"foo\"", Item("foo")},
{"empty string", "\"\"", Item("")},
{"long string",
"\"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo \"",
Item("foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo foo "
"foo ")},
{"whitespace string", "\" \"", Item(" ")},
{"non-ascii string", "\"f\xC3\xBC\xC3\xBC\"", base::nullopt},
{"tab in string", "\"\t\"", base::nullopt},
{"newline in string", "\" \n \"", base::nullopt},
{"single quoted string", "'foo'", base::nullopt},
{"unbalanced string", "\"foo", base::nullopt},
{"string quoting", "\"foo \\\"bar\\\" \\\\ baz\"",
Item("foo \"bar\" \\ baz")},
{"bad string quoting", "\"foo \\,\"", base::nullopt},
{"ending string quote", "\"foo \\\"", base::nullopt},
{"abruptly ending string quote", "\"foo \\", base::nullopt},
};
for (const auto& c : cases) {
SCOPED_TRACE(c.name); SCOPED_TRACE(c.name);
base::Optional<Item> result = ParseItem(c.raw); base::Optional<Item> result = ParseItem(c.raw);
EXPECT_EQ(result, c.expected); EXPECT_EQ(result, c.expected);
} }
} }
// For Structured Headers Draft 13 // In Structured Headers Draft 9, integers can be larger than 1e15. This
TEST(StructuredHeaderTest, ParseList) { // behaviour is exposed in the parser for SH09-specific lists, so test it
struct ListTestCase { // through that interface.
const char* name; TEST(StructuredHeaderTest, SH09LargeInteger) {
const char* raw; base::Optional<ListOfLists> result = ParseListOfLists("9223372036854775807");
const base::Optional<List> expected; // nullopt if parse error is expected. EXPECT_TRUE(result.has_value());
} cases[] = { EXPECT_EQ(result->size(), 1UL);
// Basic lists EXPECT_EQ((*result)[0].size(), 1UL);
{"basic list", "1, 42", {{{Item(1), {}}, {Item(42), {}}}}}, EXPECT_EQ((*result)[0][0], Item(9223372036854775807L));
{"empty list", "", List()},
{"single item list", "42", {{{Item(42), {}}}}}, result = ParseListOfLists("9223372036854775808");
{"no whitespace list", "1, 42", {{{Item(1), {}}, {Item(42), {}}}}}, EXPECT_FALSE(result.has_value());
{"trailing comma list", "1, 42,", base::nullopt},
{"empty item list", "1,,42", base::nullopt},
// Lists of lists
{"basic list of lists",
"(1 2), (42 43)",
{{{{Item(1), Item(2)}, {}}, {{Item(42), Item(43)}, {}}}}},
{"single item list of lists",
"(42)",
{{{std::vector<Item>{Item(42)}, {}}}}},
{"empty item list of lists", "()", {{{std::vector<Item>(), {}}}}},
{"empty middle item list of lists",
"(1),(),(42)",
{{{std::vector<Item>{Item(1)}, {}},
{std::vector<Item>(), {}},
{std::vector<Item>{Item(42)}, {}}}}},
{"extra whitespace list of lists",
"(1 42)",
{{{{Item(1), Item(42)}, {}}}}},
{"no trailing parenthesis list of lists", "(1 42", base::nullopt},
{"no trailing parenthesis middle list of lists", "(1 2, (42 43)",
base::nullopt},
// Parameterized Lists
{"basic parameterised list",
"abc_123;a=1;b=2; cdef_456, ghi;q=\"9\";r=\"w\"",
{{{Token("abc_123"), {Param("a", 1), Param("b", 2), Param("cdef_456")}},
{Token("ghi"), {Param("q", "9"), Param("r", "w")}}}}},
{"single item parameterised list",
"text/html;q=1",
{{{Token("text/html"), {Param("q", 1)}}}}},
{"no whitespace parameterised list",
"text/html,text/plain;q=1",
{{{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}}},
{"whitespace before = parameterised list", "text/html, text/plain;q =1",
base::nullopt},
{"whitespace after = parameterised list", "text/html, text/plain;q= 1",
base::nullopt},
{"extra whitespace param-list",
"text/html , text/plain ; q=1",
{{{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}}},
{"empty item parameterised list", "text/html,,text/plain;q=1",
base::nullopt},
};
for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<List> result = ParseList(c.raw);
EXPECT_EQ(result, c.expected);
}
} }
// For Structured Headers Draft 9 // For Structured Headers Draft 9
TEST(StructuredHeaderTest, ParseListOfLists) { TEST(StructuredHeaderTest, ParseListOfLists) {
struct TestCase { static const struct TestCase {
const char* name; const char* name;
const char* raw; const char* raw;
ListOfLists expected; // empty if parse error is expected ListOfLists expected; // empty if parse error is expected
...@@ -199,7 +224,7 @@ TEST(StructuredHeaderTest, ParseListOfLists) { ...@@ -199,7 +224,7 @@ TEST(StructuredHeaderTest, ParseListOfLists) {
// For Structured Headers Draft 9 // For Structured Headers Draft 9
TEST(StructuredHeaderTest, ParseParameterisedList) { TEST(StructuredHeaderTest, ParseParameterisedList) {
struct TestCase { static const struct TestCase {
const char* name; const char* name;
const char* raw; const char* raw;
ParameterisedList expected; // empty if parse error is expected ParameterisedList expected; // empty if parse error is expected
...@@ -253,5 +278,125 @@ TEST(StructuredHeaderTest, ParseParameterisedList) { ...@@ -253,5 +278,125 @@ TEST(StructuredHeaderTest, ParseParameterisedList) {
} }
} }
// For Structured Headers Draft 13
TEST(StructuredHeaderTest, ParseList) {
for (const auto& c : list_test_cases) {
SCOPED_TRACE(c.name);
base::Optional<List> result = ParseList(c.raw);
EXPECT_EQ(result, c.expected);
}
}
// Serializer tests are all exclusively for Structured Headers Draft 13
TEST(StructuredHeaderTest, SerializeItem) {
for (const auto& c : item_test_cases) {
SCOPED_TRACE(c.name);
if (c.expected) {
base::Optional<std::string> result = SerializeItem(*c.expected);
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result.value(), std::string(c.canonical ? c.canonical : c.raw));
}
}
}
TEST(StructuredHeaderTest, UnserializableItems) {
// Test that items with unknown type are not serialized.
EXPECT_FALSE(SerializeItem(Item()).has_value());
}
TEST(StructuredHeaderTest, UnserializableTokens) {
static const struct UnserializableString {
const char* name;
const char* value;
} bad_tokens[] = {
{"empty token", ""},
{"contains high ascii", "a\xff"},
{"contains nonprintable character", "a\x7f"},
{"contains C0", "a\x01"},
{"UTF-8 encoded", "a\xc3\xa9"},
{"contains TAB", "a\t"},
{"contains LF", "a\n"},
{"contains CR", "a\r"},
{"contains SP", "a "},
{"begins with digit", "9token"},
{"begins with hyphen", "-token"},
{"begins with LF", "\ntoken"},
{"begins with SP", " token"},
{"begins with colon", ":token"},
{"begins with percent", "%token"},
{"begins with period", ".token"},
{"begins with asterisk", "*token"},
{"begins with slash", "/token"},
};
for (const auto& bad_token : bad_tokens) {
SCOPED_TRACE(bad_token.name);
base::Optional<std::string> serialization =
SerializeItem(Token(bad_token.value));
EXPECT_FALSE(serialization.has_value()) << *serialization;
}
}
TEST(StructuredHeaderTest, UnserializableStrings) {
static const struct UnserializableString {
const char* name;
const char* value;
} bad_strings[] = {
{"contains high ascii", "a\xff"},
{"contains nonprintable character", "a\x7f"},
{"UTF-8 encoded", "a\xc3\xa9"},
{"contains TAB", "a\t"},
{"contains LF", "a\n"},
{"contains CR", "a\r"},
{"contains C0", "a\x01"},
};
for (const auto& bad_string : bad_strings) {
SCOPED_TRACE(bad_string.name);
base::Optional<std::string> serialization =
SerializeItem(Item(bad_string.value));
EXPECT_FALSE(serialization.has_value()) << *serialization;
}
}
TEST(StructuredHeaderTest, UnserializableIntegers) {
EXPECT_FALSE(SerializeItem(Item(1e15L)).has_value());
}
TEST(StructuredHeaderTest, SerializeList) {
for (const auto& c : list_test_cases) {
SCOPED_TRACE(c.name);
if (c.expected) {
base::Optional<std::string> result = SerializeList(*c.expected);
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result.value(), std::string(c.canonical ? c.canonical : c.raw));
}
}
}
TEST(StructuredHeaderTest, UnserializableLists) {
static const struct UnserializableList {
const char* name;
const List value;
} bad_lists[] = {
{"Null item as member", {{Item(), {}}}},
{"Unserializable item as member", {{Token("\n"), {}}}},
{"Key is empty", {{Token("abc"), {Param("", 1)}}}},
{"Key contains whitespace", {{Token("abc"), {Param("a\n", 1)}}}},
{"Key contains UTF8", {{Token("abc"), {Param("a\xc3\xa9", 1)}}}},
{"Key contains unprintable characters",
{{Token("abc"), {Param("a\x7f", 1)}}}},
{"Key contains disallowed characters",
{{Token("abc"), {Param("a:", 1)}}}},
{"Param value is unserializable", {{Token("abc"), {{"a", Token("\n")}}}}},
{"Inner list contains unserializable item",
{{std::vector<Item>{Token("\n")}, {}}}},
};
for (const auto& bad_list : bad_lists) {
SCOPED_TRACE(bad_list.name);
base::Optional<std::string> serialization = SerializeList(bad_list.value);
EXPECT_FALSE(serialization.has_value()) << *serialization;
}
}
} // namespace http_structured_header } // namespace http_structured_header
} // namespace blink } // namespace blink
...@@ -153,20 +153,24 @@ using ListOfLists = std::vector<std::vector<Item>>; ...@@ -153,20 +153,24 @@ using ListOfLists = std::vector<std::vector<Item>>;
using List = std::vector<ParameterizedMember>; using List = std::vector<ParameterizedMember>;
// Returns the result of parsing the header value as an Item, if it can be // Returns the result of parsing the header value as an Item, if it can be
// parsed as one, or nullopt if it cannot. // parsed as one, or nullopt if it cannot. Note that this uses the Draft 13
// parsing rules, and so applies tighter range limits to integers.
BLINK_COMMON_EXPORT base::Optional<Item> ParseItem( BLINK_COMMON_EXPORT base::Optional<Item> ParseItem(
const base::StringPiece& str); const base::StringPiece& str);
// Returns the result of parsing the header value as a Parameterised List, if it // Returns the result of parsing the header value as a Parameterised List, if it
// can be parsed as one, or nullopt if it cannot. Note that parameter keys will // can be parsed as one, or nullopt if it cannot. Note that parameter keys will
// be returned as strings, which are guaranteed to be ASCII-encoded. List items, // be returned as strings, which are guaranteed to be ASCII-encoded. List items,
// as well as parameter values, will be returned as Items. // as well as parameter values, will be returned as Items. This method uses the
// Draft 09 parsing rules for Items, so integers have the 64-bit int range.
// Structured-Headers Draft 09 only.
BLINK_COMMON_EXPORT base::Optional<ParameterisedList> ParseParameterisedList( BLINK_COMMON_EXPORT base::Optional<ParameterisedList> ParseParameterisedList(
const base::StringPiece& str); const base::StringPiece& str);
// Returns the result of parsing the header value as a List of Lists, if it can // Returns the result of parsing the header value as a List of Lists, if it can
// be parsed as one, or nullopt if it cannot. Inner list items will be returned // be parsed as one, or nullopt if it cannot. Inner list items will be returned
// as Items. // as Items. This method uses the Draft 09 parsing rules for Items, so integers
// have the 64-bit int range.
// Structured-Headers Draft 09 only. // Structured-Headers Draft 09 only.
BLINK_COMMON_EXPORT base::Optional<ListOfLists> ParseListOfLists( BLINK_COMMON_EXPORT base::Optional<ListOfLists> ParseListOfLists(
const base::StringPiece& str); const base::StringPiece& str);
...@@ -177,6 +181,12 @@ BLINK_COMMON_EXPORT base::Optional<ListOfLists> ParseListOfLists( ...@@ -177,6 +181,12 @@ BLINK_COMMON_EXPORT base::Optional<ListOfLists> ParseListOfLists(
BLINK_COMMON_EXPORT base::Optional<List> ParseList( BLINK_COMMON_EXPORT base::Optional<List> ParseList(
const base::StringPiece& str); const base::StringPiece& str);
// Serialization is implemented for Structured-Headers Draft 13 only.
BLINK_COMMON_EXPORT base::Optional<std::string> SerializeItem(
const Item& value);
BLINK_COMMON_EXPORT base::Optional<std::string> SerializeList(
const List& value);
} // namespace http_structured_header } // namespace http_structured_header
} // namespace blink } // namespace blink
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment