Commit 6ac3b3d9 authored by Ian Clelland's avatar Ian Clelland Committed by Commit Bot

Add support for Structured Headers draft 13 list syntax.

This adds support for the generalized 'List' syntax in
https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13.
Since some types have changed slightly between revisions, the parser now
takes a version parameter on construction, which can be used to switch
between draft 9 (for compatibility with existing Web Packaging
deployments) and draft 13 (for new headers).

Bug: 1011101
Change-Id: Ie8a76384430f5319e1d564e51f4f62af7c0ada7f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1872764
Commit-Queue: Ian Clelland <iclelland@chromium.org>
Reviewed-by: default avatarJeremy Roman <jbroman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#713847}
parent 839a53b9
...@@ -10,6 +10,27 @@ ...@@ -10,6 +10,27 @@
namespace blink { namespace blink {
namespace http_structured_header { namespace http_structured_header {
namespace {
// Helpers to make test cases clearer
Item Token(std::string value) {
return Item(value, Item::kTokenType);
}
std::pair<std::string, Item> Param(std::string key) {
return std::make_pair(key, Item());
}
std::pair<std::string, Item> Param(std::string key, int64_t value) {
return std::make_pair(key, Item(value));
}
std::pair<std::string, Item> Param(std::string key, std::string value) {
return std::make_pair(key, Item(value));
}
} // namespace
// Test cases are taken from https://github.com/httpwg/structured-header-tests. // Test cases are taken from https://github.com/httpwg/structured-header-tests.
...@@ -20,15 +41,12 @@ TEST(StructuredHeaderTest, ParseItem) { ...@@ -20,15 +41,12 @@ TEST(StructuredHeaderTest, ParseItem) {
const base::Optional<Item> expected; // nullopt if parse error is expected const base::Optional<Item> expected; // nullopt if parse error is expected
} cases[] = { } cases[] = {
// Item // Item
{"basic token - item", "a_b-c.d3:f%00/*", {"basic token - item", "a_b-c.d3:f%00/*", Token("a_b-c.d3:f%00/*")},
Item("a_b-c.d3:f%00/*", Item::kTokenType)}, {"token with capitals - item", "fooBar", Token("fooBar")},
{"token with capitals - item", "fooBar", {"token starting with capitals - item", "FooBar", Token("FooBar")},
Item("fooBar", Item::kTokenType)},
{"token starting with capitals - item", "FooBar",
Item("FooBar", Item::kTokenType)},
{"bad token - item", "abc$%!", base::nullopt}, {"bad token - item", "abc$%!", base::nullopt},
{"leading whitespace", " foo", Item("foo", Item::kTokenType)}, {"leading whitespace", " foo", Token("foo")},
{"trailing whitespace", "foo ", Item("foo", Item::kTokenType)}, {"trailing whitespace", "foo ", Token("foo")},
// Number // Number
{"basic integer", "42", Item(42)}, {"basic integer", "42", Item(42)},
{"zero integer", "0", Item(0)}, {"zero integer", "0", Item(0)},
...@@ -73,16 +91,74 @@ TEST(StructuredHeaderTest, ParseItem) { ...@@ -73,16 +91,74 @@ TEST(StructuredHeaderTest, ParseItem) {
{"abruptly ending string quote", "\"foo \\", base::nullopt}, {"abruptly ending string quote", "\"foo \\", base::nullopt},
}; };
for (const auto& c : cases) { for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<Item> result = ParseItem(c.raw); base::Optional<Item> result = ParseItem(c.raw);
if (c.expected) { EXPECT_EQ(result, c.expected);
EXPECT_TRUE(result.has_value()) << c.name;
EXPECT_EQ(*result, c.expected) << c.name;
} else {
EXPECT_FALSE(result.has_value()) << c.name;
} }
}
// For Structured Headers Draft 13
TEST(StructuredHeaderTest, ParseList) {
struct ListTestCase {
const char* name;
const char* raw;
const base::Optional<List> expected; // nullopt if parse error is expected.
} 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), {}}}}},
{"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
TEST(StructuredHeaderTest, ParseListOfLists) { TEST(StructuredHeaderTest, ParseListOfLists) {
struct TestCase { struct TestCase {
const char* name; const char* name;
...@@ -110,21 +186,18 @@ TEST(StructuredHeaderTest, ParseListOfLists) { ...@@ -110,21 +186,18 @@ TEST(StructuredHeaderTest, ParseListOfLists) {
{"empty inner item list of lists", "1;;2,42", {}}, {"empty inner item list of lists", "1;;2,42", {}},
}; };
for (const auto& c : cases) { for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<ListOfLists> result = ParseListOfLists(c.raw); base::Optional<ListOfLists> result = ParseListOfLists(c.raw);
if (!c.expected.empty()) { if (!c.expected.empty()) {
EXPECT_TRUE(result.has_value()) << c.name; EXPECT_TRUE(result.has_value());
EXPECT_EQ(*result, c.expected) << c.name; EXPECT_EQ(*result, c.expected);
} else { } else {
EXPECT_FALSE(result.has_value()) << c.name; EXPECT_FALSE(result.has_value());
} }
} }
} }
inline bool operator==(const ParameterisedIdentifier& lhs, // For Structured Headers Draft 9
const ParameterisedIdentifier& rhs) {
return lhs.identifier == rhs.identifier && lhs.params == rhs.params;
}
TEST(StructuredHeaderTest, ParseParameterisedList) { TEST(StructuredHeaderTest, ParseParameterisedList) {
struct TestCase { struct TestCase {
const char* name; const char* name;
...@@ -134,26 +207,23 @@ TEST(StructuredHeaderTest, ParseParameterisedList) { ...@@ -134,26 +207,23 @@ TEST(StructuredHeaderTest, ParseParameterisedList) {
{"basic param-list", {"basic param-list",
"abc_123;a=1;b=2; cdef_456, ghi;q=\"9\";r=\"w\"", "abc_123;a=1;b=2; cdef_456, ghi;q=\"9\";r=\"w\"",
{ {
{Item("abc_123", Item::kTokenType), {Token("abc_123"),
{{"a", Item(1)}, {"b", Item(2)}, {"cdef_456", {}}}}, {Param("a", 1), Param("b", 2), Param("cdef_456")}},
{Item("ghi", Item::kTokenType), {Token("ghi"), {Param("q", "9"), Param("r", "w")}},
{{"q", Item("9")}, {"r", Item("w")}}},
}}, }},
{"empty param-list", "", {}}, {"empty param-list", "", {}},
{"single item param-list", {"single item param-list",
"text/html;q=1", "text/html;q=1",
{{Item("text/html", Item::kTokenType), {{"q", Item(1)}}}}}, {{Token("text/html"), {Param("q", 1)}}}},
{"empty param-list", "", {}}, {"empty param-list", "", {}},
{"no whitespace param-list", {"no whitespace param-list",
"text/html,text/plain;q=1", "text/html,text/plain;q=1",
{{Item("text/html", Item::kTokenType), {}}, {{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}},
{Item("text/plain", Item::kTokenType), {{"q", Item(1)}}}}},
{"whitespace before = param-list", "text/html, text/plain;q =1", {}}, {"whitespace before = param-list", "text/html, text/plain;q =1", {}},
{"whitespace after = param-list", "text/html, text/plain;q= 1", {}}, {"whitespace after = param-list", "text/html, text/plain;q= 1", {}},
{"extra whitespace param-list", {"extra whitespace param-list",
"text/html , text/plain ; q=1", "text/html , text/plain ; q=1",
{{Item("text/html", Item::kTokenType), {}}, {{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}},
{Item("text/plain", Item::kTokenType), {{"q", Item(1)}}}}},
{"duplicate key", "abc;a=1;b=2;a=1", {}}, {"duplicate key", "abc;a=1;b=2;a=1", {}},
{"numeric key", "abc;a=1;1b=2;c=1", {}}, {"numeric key", "abc;a=1;1b=2;c=1", {}},
{"uppercase key", "abc;a=1;B=2;c=1", {}}, {"uppercase key", "abc;a=1;B=2;c=1", {}},
...@@ -168,16 +238,17 @@ TEST(StructuredHeaderTest, ParseParameterisedList) { ...@@ -168,16 +238,17 @@ TEST(StructuredHeaderTest, ParseParameterisedList) {
{"leading comma", ",abc;a=1", {}}, {"leading comma", ",abc;a=1", {}},
}; };
for (const auto& c : cases) { for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<ParameterisedList> result = ParseParameterisedList(c.raw); base::Optional<ParameterisedList> result = ParseParameterisedList(c.raw);
if (c.expected.empty()) { if (c.expected.empty()) {
EXPECT_FALSE(result.has_value()) << c.name; EXPECT_FALSE(result.has_value());
continue; continue;
} }
EXPECT_TRUE(result.has_value()) << c.name; EXPECT_TRUE(result.has_value());
EXPECT_EQ(result->size(), c.expected.size()) << c.name; EXPECT_EQ(result->size(), c.expected.size());
if (result->size() == c.expected.size()) { if (result->size() == c.expected.size()) {
for (size_t i = 0; i < c.expected.size(); ++i) for (size_t i = 0; i < c.expected.size(); ++i)
EXPECT_EQ((*result)[i], c.expected[i]) << c.name; EXPECT_EQ((*result)[i], c.expected[i]);
} }
} }
} }
......
...@@ -20,6 +20,13 @@ namespace http_structured_header { ...@@ -20,6 +20,13 @@ namespace http_structured_header {
// This file implements parsing of HTTP structured headers, as defined in // This file implements parsing of HTTP structured headers, as defined in
// https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html. // https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html.
// //
// Both drafts 9 and 13 are currently supported. The major difference
// between the two drafts is in the various list formats: Draft 9 describes
// Parameterised lists and lists-of-lists, while draft 13 uses a single List
// syntax, whose members may be inner lists. There should be no ambiguity,
// however, as the code which calls this parser should be expecting only a
// single type for a given header.
//
// Currently supported data types are: // Currently supported data types are:
// Item: // Item:
// integer: 123 // integer: 123
...@@ -28,6 +35,9 @@ namespace http_structured_header { ...@@ -28,6 +35,9 @@ namespace http_structured_header {
// byte sequence: *YWJj* // byte sequence: *YWJj*
// Parameterised list: abc_123;a=1;b=2; cdef_456, ghi;q="9";r="w" // Parameterised list: abc_123;a=1;b=2; cdef_456, ghi;q="9";r="w"
// List-of-lists: "foo";"bar", "baz", "bat"; "one" // List-of-lists: "foo";"bar", "baz", "bat"; "one"
// List: "foo", "bar", "It was the best of times."
// ("foo" "bar"), ("baz"), ("bat" "one"), ()
// abc;a=1;b=2; cde_456, (ghi jkl);q="9";r=w
// //
// Functions are provided to parse each of these, which are intended to be // Functions are provided to parse each of these, which are intended to be
// called with the complete value of an HTTP header (that is, any // called with the complete value of an HTTP header (that is, any
...@@ -87,6 +97,9 @@ class BLINK_COMMON_EXPORT Item { ...@@ -87,6 +97,9 @@ class BLINK_COMMON_EXPORT Item {
std::string string_value_; std::string string_value_;
}; };
// Holds a ParameterizedIdentifier (draft 9 only). The contained Item must be a
// Token, and there may be any number of parameters. Parameter ordering is not
// significant.
struct BLINK_COMMON_EXPORT ParameterisedIdentifier { struct BLINK_COMMON_EXPORT ParameterisedIdentifier {
using Parameters = std::map<std::string, Item>; using Parameters = std::map<std::string, Item>;
...@@ -99,8 +112,45 @@ struct BLINK_COMMON_EXPORT ParameterisedIdentifier { ...@@ -99,8 +112,45 @@ struct BLINK_COMMON_EXPORT ParameterisedIdentifier {
~ParameterisedIdentifier(); ~ParameterisedIdentifier();
}; };
inline bool operator==(const ParameterisedIdentifier& lhs,
const ParameterisedIdentifier& rhs) {
return lhs.identifier == rhs.identifier && lhs.params == rhs.params;
}
// Holds a ParameterizedMember, which may be either an Inner List, or a single
// Item, with any number of parameters. Parameter ordering is significant.
struct BLINK_COMMON_EXPORT ParameterizedMember {
using Parameters = std::vector<std::pair<std::string, Item>>;
std::vector<Item> member;
// If false, then |member| should only hold one Item.
bool member_is_inner_list;
Parameters params;
ParameterizedMember(const ParameterizedMember&);
ParameterizedMember& operator=(const ParameterizedMember&);
ParameterizedMember(std::vector<Item>, bool, const Parameters&);
// Shorthand constructor for a member which is an inner list.
ParameterizedMember(std::vector<Item>, const Parameters&);
// Shorthand constructor for a member which is a single Item.
ParameterizedMember(Item, const Parameters&);
~ParameterizedMember();
};
inline bool operator==(const ParameterizedMember& lhs,
const ParameterizedMember& rhs) {
return lhs.member == rhs.member &&
lhs.member_is_inner_list == rhs.member_is_inner_list &&
lhs.params == rhs.params;
}
// Structured Headers Draft 09 Parameterised List.
using ParameterisedList = std::vector<ParameterisedIdentifier>; using ParameterisedList = std::vector<ParameterisedIdentifier>;
// Structured Headers Draft 09 List of Lists.
using ListOfLists = std::vector<std::vector<Item>>; using ListOfLists = std::vector<std::vector<Item>>;
// Structured Headers Draft 13 List.
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.
...@@ -117,9 +167,16 @@ BLINK_COMMON_EXPORT base::Optional<ParameterisedList> ParseParameterisedList( ...@@ -117,9 +167,16 @@ BLINK_COMMON_EXPORT base::Optional<ParameterisedList> ParseParameterisedList(
// 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.
// 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);
// Returns the result of parsing the header value as a general List, if it can
// be parsed as one, or nullopt if it cannot.
// Structured-Headers Draft 13 only.
BLINK_COMMON_EXPORT base::Optional<List> ParseList(
const base::StringPiece& str);
} // 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