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 @@
namespace blink {
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.
......@@ -20,15 +41,12 @@ TEST(StructuredHeaderTest, ParseItem) {
const base::Optional<Item> expected; // nullopt if parse error is expected
} cases[] = {
// Item
{"basic token - item", "a_b-c.d3:f%00/*",
Item("a_b-c.d3:f%00/*", Item::kTokenType)},
{"token with capitals - item", "fooBar",
Item("fooBar", Item::kTokenType)},
{"token starting with capitals - item", "FooBar",
Item("FooBar", Item::kTokenType)},
{"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", Item("foo", Item::kTokenType)},
{"trailing whitespace", "foo ", Item("foo", Item::kTokenType)},
{"leading whitespace", " foo", Token("foo")},
{"trailing whitespace", "foo ", Token("foo")},
// Number
{"basic integer", "42", Item(42)},
{"zero integer", "0", Item(0)},
......@@ -73,16 +91,74 @@ TEST(StructuredHeaderTest, ParseItem) {
{"abruptly ending string quote", "\"foo \\", base::nullopt},
};
for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<Item> result = ParseItem(c.raw);
if (c.expected) {
EXPECT_TRUE(result.has_value()) << c.name;
EXPECT_EQ(*result, c.expected) << c.name;
} else {
EXPECT_FALSE(result.has_value()) << c.name;
}
EXPECT_EQ(result, c.expected);
}
}
// 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) {
struct TestCase {
const char* name;
......@@ -110,21 +186,18 @@ TEST(StructuredHeaderTest, ParseListOfLists) {
{"empty inner item list of lists", "1;;2,42", {}},
};
for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<ListOfLists> result = ParseListOfLists(c.raw);
if (!c.expected.empty()) {
EXPECT_TRUE(result.has_value()) << c.name;
EXPECT_EQ(*result, c.expected) << c.name;
EXPECT_TRUE(result.has_value());
EXPECT_EQ(*result, c.expected);
} else {
EXPECT_FALSE(result.has_value()) << c.name;
EXPECT_FALSE(result.has_value());
}
}
}
inline bool operator==(const ParameterisedIdentifier& lhs,
const ParameterisedIdentifier& rhs) {
return lhs.identifier == rhs.identifier && lhs.params == rhs.params;
}
// For Structured Headers Draft 9
TEST(StructuredHeaderTest, ParseParameterisedList) {
struct TestCase {
const char* name;
......@@ -134,26 +207,23 @@ TEST(StructuredHeaderTest, ParseParameterisedList) {
{"basic param-list",
"abc_123;a=1;b=2; cdef_456, ghi;q=\"9\";r=\"w\"",
{
{Item("abc_123", Item::kTokenType),
{{"a", Item(1)}, {"b", Item(2)}, {"cdef_456", {}}}},
{Item("ghi", Item::kTokenType),
{{"q", Item("9")}, {"r", Item("w")}}},
{Token("abc_123"),
{Param("a", 1), Param("b", 2), Param("cdef_456")}},
{Token("ghi"), {Param("q", "9"), Param("r", "w")}},
}},
{"empty param-list", "", {}},
{"single item param-list",
"text/html;q=1",
{{Item("text/html", Item::kTokenType), {{"q", Item(1)}}}}},
{{Token("text/html"), {Param("q", 1)}}}},
{"empty param-list", "", {}},
{"no whitespace param-list",
"text/html,text/plain;q=1",
{{Item("text/html", Item::kTokenType), {}},
{Item("text/plain", Item::kTokenType), {{"q", Item(1)}}}}},
{{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}},
{"whitespace before = param-list", "text/html, text/plain;q =1", {}},
{"whitespace after = param-list", "text/html, text/plain;q= 1", {}},
{"extra whitespace param-list",
"text/html , text/plain ; q=1",
{{Item("text/html", Item::kTokenType), {}},
{Item("text/plain", Item::kTokenType), {{"q", Item(1)}}}}},
{{Token("text/html"), {}}, {Token("text/plain"), {Param("q", 1)}}}},
{"duplicate key", "abc;a=1;b=2;a=1", {}},
{"numeric key", "abc;a=1;1b=2;c=1", {}},
{"uppercase key", "abc;a=1;B=2;c=1", {}},
......@@ -168,16 +238,17 @@ TEST(StructuredHeaderTest, ParseParameterisedList) {
{"leading comma", ",abc;a=1", {}},
};
for (const auto& c : cases) {
SCOPED_TRACE(c.name);
base::Optional<ParameterisedList> result = ParseParameterisedList(c.raw);
if (c.expected.empty()) {
EXPECT_FALSE(result.has_value()) << c.name;
EXPECT_FALSE(result.has_value());
continue;
}
EXPECT_TRUE(result.has_value()) << c.name;
EXPECT_EQ(result->size(), c.expected.size()) << c.name;
EXPECT_TRUE(result.has_value());
EXPECT_EQ(result->size(), c.expected.size());
if (result->size() == c.expected.size()) {
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 {
// This file implements parsing of HTTP structured headers, as defined in
// 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:
// Item:
// integer: 123
......@@ -28,6 +35,9 @@ namespace http_structured_header {
// byte sequence: *YWJj*
// Parameterised list: abc_123;a=1;b=2; cdef_456, ghi;q="9";r="w"
// 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
// called with the complete value of an HTTP header (that is, any
......@@ -87,6 +97,9 @@ class BLINK_COMMON_EXPORT Item {
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 {
using Parameters = std::map<std::string, Item>;
......@@ -99,8 +112,45 @@ struct BLINK_COMMON_EXPORT 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>;
// Structured Headers Draft 09 List of Lists.
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
// parsed as one, or nullopt if it cannot.
......@@ -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
// be parsed as one, or nullopt if it cannot. Inner list items will be returned
// as Items.
// Structured-Headers Draft 09 only.
BLINK_COMMON_EXPORT base::Optional<ListOfLists> ParseListOfLists(
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 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