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
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include <utility> #include <utility>
#include "base/base64.h" #include "base/base64.h"
#include "base/containers/flat_set.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"
...@@ -20,38 +21,56 @@ namespace { ...@@ -20,38 +21,56 @@ namespace {
#define LCALPHA "abcdefghijklmnopqrstuvwxyz" #define LCALPHA "abcdefghijklmnopqrstuvwxyz"
#define UCALPHA "ABCDEFGHIJKLMNOPQRSTUVWXYZ" #define UCALPHA "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-3.9 // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-3.9
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13#section-3.7
constexpr char kTokenChars[] = DIGIT UCALPHA LCALPHA "_-.:%*/"; constexpr char kTokenChars[] = DIGIT UCALPHA LCALPHA "_-.:%*/";
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-3.1 // https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09#section-3.1
constexpr char kKeyChars[] = DIGIT LCALPHA "_-"; constexpr char kKeyChars09[] = DIGIT LCALPHA "_-";
// https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13#section-3.1
constexpr char kKeyChars13[] = DIGIT LCALPHA "_-*";
#undef DIGIT #undef DIGIT
#undef LCALPHA #undef LCALPHA
#undef UCALPHA #undef UCALPHA
// 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
bool IsPrintableASCII(char c) { bool IsPrintableASCII(char c) {
return ' ' <= c && c <= '~'; // 0x20 (' ') to 0x7E ('~') return ' ' <= c && c <= '~'; // 0x20 (' ') to 0x7E ('~')
} }
// Parser for (a subset of) Structured Headers for HTTP defined in [SH]. // Parser for (a subset of) Structured Headers for HTTP defined in [SH09] and
// [SH] https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09 // [SH13]. [SH09] compatibility is retained for use by Web Packaging, and can be
// removed once that spec is updated, and users have migrated to new headers.
// [SH09] https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-09
// [SH13] https://tools.ietf.org/html/draft-ietf-httpbis-header-structure-13
class StructuredHeaderParser { class StructuredHeaderParser {
public: public:
explicit StructuredHeaderParser(const base::StringPiece& str) : input_(str) { enum DraftVersion {
// [SH] 4.2. Step 1. Discard any leading OWS from input_string. kDraft09,
kDraft13,
};
explicit StructuredHeaderParser(const base::StringPiece& str,
DraftVersion version)
: input_(str), version_(version) {
// [SH09] 4.2 Step 1.
// [SH13] 4.2 Step 2.
// Discard any leading OWS from input_string.
SkipWhitespaces(); SkipWhitespaces();
} }
// Callers should call this after ReadSomething(), to check if parser has // Callers should call this after ReadSomething(), to check if parser has
// consumed all the input successfully. // consumed all the input successfully.
bool FinishParsing() { bool FinishParsing() {
// [SH] 4.2 Step 7. Discard any leading OWS from input_string. // [SH09] 4.2 Step 7. [SH13] 4.2 Step 6.
// Discard any leading OWS from input_string.
SkipWhitespaces(); SkipWhitespaces();
// [SH] 4.2 Step 8. If input_string is not empty, fail parsing. // [SH09] 4.2 Step 8. [SH13] 4.2 Step 7.
// If input_string is not empty, fail parsing.
return input_.empty(); return input_.empty();
} }
// Parses a List of Lists ([SH] 4.2.4). // Parses a List of Lists ([SH09] 4.2.4).
base::Optional<ListOfLists> ReadListOfLists() { base::Optional<ListOfLists> ReadListOfLists() {
DCHECK_EQ(version_, kDraft09);
ListOfLists result; ListOfLists result;
while (true) { while (true) {
std::vector<Item> inner_list; std::vector<Item> inner_list;
...@@ -74,7 +93,26 @@ class StructuredHeaderParser { ...@@ -74,7 +93,26 @@ class StructuredHeaderParser {
return result; return result;
} }
// Parses an Item ([SH] 4.2.7). // Parses a List ([SH13] 4.2.1).
base::Optional<List> ReadList() {
DCHECK_EQ(version_, kDraft13);
List members;
while (!input_.empty()) {
base::Optional<ParameterizedMember> member(ReadParameterizedMember());
if (!member)
return base::nullopt;
members.push_back(std::move(*member));
SkipWhitespaces();
if (!ConsumeChar(','))
break;
SkipWhitespaces();
if (input_.empty())
return base::nullopt;
}
return members;
}
// Parses an Item ([SH09] 4.2.7, [SH13] 4.2.3).
// Currently only limited types (non-negative integers, strings, tokens and // Currently only limited types (non-negative integers, strings, tokens and
// byte sequences) are supported. // byte sequences) are supported.
// TODO(1011101): Add support for other types. // TODO(1011101): Add support for other types.
...@@ -96,8 +134,9 @@ class StructuredHeaderParser { ...@@ -96,8 +134,9 @@ class StructuredHeaderParser {
} }
} }
// Parses a Parameterised List ([SH] 4.2.5). // Parses a Parameterised List ([SH09] 4.2.5).
base::Optional<ParameterisedList> ReadParameterisedList() { base::Optional<ParameterisedList> ReadParameterisedList() {
DCHECK_EQ(version_, kDraft09);
ParameterisedList items; ParameterisedList items;
while (true) { while (true) {
base::Optional<ParameterisedIdentifier> item = base::Optional<ParameterisedIdentifier> item =
...@@ -113,8 +152,9 @@ class StructuredHeaderParser { ...@@ -113,8 +152,9 @@ class StructuredHeaderParser {
} }
private: private:
// Parses a Parameterised Identifier ([SH] 4.2.6). // Parses a Parameterised Identifier ([SH09] 4.2.6).
base::Optional<ParameterisedIdentifier> ReadParameterisedIdentifier() { base::Optional<ParameterisedIdentifier> ReadParameterisedIdentifier() {
DCHECK_EQ(version_, kDraft09);
base::Optional<Item> primary_identifier = ReadToken(); base::Optional<Item> primary_identifier = ReadToken();
if (!primary_identifier) if (!primary_identifier)
return base::nullopt; return base::nullopt;
...@@ -147,13 +187,84 @@ class StructuredHeaderParser { ...@@ -147,13 +187,84 @@ class StructuredHeaderParser {
std::move(parameters)); std::move(parameters));
} }
// Parses a Key ([SH] 4.2.2). // Parses a Parameterized Member ([SH13] 4.2.1.1).
base::Optional<ParameterizedMember> ReadParameterizedMember() {
DCHECK_EQ(version_, kDraft13);
std::vector<Item> member;
bool member_is_inner_list = ConsumeChar('(');
if (member_is_inner_list) {
base::Optional<std::vector<Item>> inner_list = ReadInnerList();
if (!inner_list)
return base::nullopt;
member = std::move(*inner_list);
} else {
base::Optional<Item> item = ReadItem();
if (!item)
return base::nullopt;
member.push_back(std::move(*item));
}
ParameterizedMember::Parameters parameters;
base::flat_set<std::string> keys;
SkipWhitespaces();
while (ConsumeChar(';')) {
SkipWhitespaces();
base::Optional<std::string> name = ReadKey();
if (!name)
return base::nullopt;
bool is_duplicate_key = !keys.insert(*name).second;
if (is_duplicate_key) {
DVLOG(1) << "ReadParameterizedMember: duplicated parameter: " << *name;
return base::nullopt;
}
Item value;
if (ConsumeChar('=')) {
auto item = ReadItem();
if (!item)
return base::nullopt;
value = std::move(*item);
}
parameters.emplace_back(std::move(*name), std::move(value));
SkipWhitespaces();
}
return ParameterizedMember(std::move(member), member_is_inner_list,
std::move(parameters));
}
// Parses an Inner List ([SH13] 4.2.1.2).
// Note that the initial '(' character should already have been consumed by
// the caller to determine that this is in fact an inner list.
base::Optional<std::vector<Item>> ReadInnerList() {
DCHECK_EQ(version_, kDraft13);
std::vector<Item> inner_list;
while (true) {
SkipWhitespaces();
if (ConsumeChar(')')) {
return inner_list;
}
auto item = ReadItem();
if (!item)
return base::nullopt;
inner_list.push_back(std::move(*item));
if (input_.empty() || (input_.front() != ' ' && input_.front() != ')'))
return base::nullopt;
}
NOTREACHED();
return base::nullopt;
}
// Parses a Key ([SH09] 4.2.2, [SH13] 4.2.1.3).
base::Optional<std::string> ReadKey() { base::Optional<std::string> ReadKey() {
if (input_.empty() || !base::IsAsciiLower(input_.front())) { if (input_.empty() || !base::IsAsciiLower(input_.front())) {
LogParseError("ReadKey", "lcalpha"); LogParseError("ReadKey", "lcalpha");
return base::nullopt; return base::nullopt;
} }
size_t len = input_.find_first_not_of(kKeyChars); const char* allowed_chars =
(version_ == kDraft09 ? kKeyChars09 : kKeyChars13);
size_t len = input_.find_first_not_of(allowed_chars);
if (len == base::StringPiece::npos) if (len == base::StringPiece::npos)
len = input_.size(); len = input_.size();
std::string key(input_.substr(0, len)); std::string key(input_.substr(0, len));
...@@ -161,7 +272,7 @@ class StructuredHeaderParser { ...@@ -161,7 +272,7 @@ class StructuredHeaderParser {
return key; return key;
} }
// Parses a Token ([SH] 4.2.10). // Parses a Token ([SH09] 4.2.10, [SH13] 4.2.6).
base::Optional<Item> ReadToken() { base::Optional<Item> ReadToken() {
if (input_.empty() || !base::IsAsciiAlpha(input_.front())) { if (input_.empty() || !base::IsAsciiAlpha(input_.front())) {
LogParseError("ReadToken", "ALPHA"); LogParseError("ReadToken", "ALPHA");
...@@ -175,7 +286,7 @@ class StructuredHeaderParser { ...@@ -175,7 +286,7 @@ class StructuredHeaderParser {
return Item(std::move(token), Item::kTokenType); return Item(std::move(token), Item::kTokenType);
} }
// Parses a Number ([SH] 4.2.8). // Parses a Number ([SH09] 4.2.8, [SH13] 4.2.4).
// Currently only supports non-negative integers. // Currently only supports non-negative integers.
base::Optional<Item> ReadNumber() { base::Optional<Item> ReadNumber() {
size_t i = 0; size_t i = 0;
...@@ -198,7 +309,7 @@ class StructuredHeaderParser { ...@@ -198,7 +309,7 @@ class StructuredHeaderParser {
return Item(n); return Item(n);
} }
// Parses a String ([SH] 4.2.9). // Parses a String ([SH09] 4.2.9, [SH13] 4.2.5).
base::Optional<Item> ReadString() { base::Optional<Item> ReadString() {
std::string s; std::string s;
if (!ConsumeChar('"')) { if (!ConsumeChar('"')) {
...@@ -237,7 +348,7 @@ class StructuredHeaderParser { ...@@ -237,7 +348,7 @@ class StructuredHeaderParser {
return s; return s;
} }
// Parses a Byte Sequence ([SH] 4.2.11). // Parses a Byte Sequence ([SH09] 4.2.11, [SH13] 4.2.7).
base::Optional<Item> ReadByteSequence() { base::Optional<Item> ReadByteSequence() {
if (!ConsumeChar('*')) { if (!ConsumeChar('*')) {
LogParseError("ReadByteSequence", "'*'"); LogParseError("ReadByteSequence", "'*'");
...@@ -281,6 +392,8 @@ class StructuredHeaderParser { ...@@ -281,6 +392,8 @@ class StructuredHeaderParser {
} }
base::StringPiece input_; base::StringPiece input_;
DraftVersion version_;
DISALLOW_COPY_AND_ASSIGN(StructuredHeaderParser); DISALLOW_COPY_AND_ASSIGN(StructuredHeaderParser);
}; };
...@@ -314,6 +427,22 @@ bool operator==(const Item& lhs, const Item& rhs) { ...@@ -314,6 +427,22 @@ bool operator==(const Item& lhs, const Item& rhs) {
} }
} }
ParameterizedMember::ParameterizedMember(const ParameterizedMember&) = default;
ParameterizedMember& ParameterizedMember::operator=(
const ParameterizedMember&) = default;
ParameterizedMember::ParameterizedMember(std::vector<Item> id,
bool member_is_inner_list,
const Parameters& ps)
: member(std::move(id)),
member_is_inner_list(member_is_inner_list),
params(ps) {}
ParameterizedMember::ParameterizedMember(std::vector<Item> id,
const Parameters& ps)
: member(std::move(id)), member_is_inner_list(true), params(ps) {}
ParameterizedMember::ParameterizedMember(Item id, const Parameters& ps)
: member({std::move(id)}), member_is_inner_list(false), params(ps) {}
ParameterizedMember::~ParameterizedMember() = default;
ParameterisedIdentifier::ParameterisedIdentifier( ParameterisedIdentifier::ParameterisedIdentifier(
const ParameterisedIdentifier&) = default; const ParameterisedIdentifier&) = default;
ParameterisedIdentifier& ParameterisedIdentifier::operator=( ParameterisedIdentifier& ParameterisedIdentifier::operator=(
...@@ -323,7 +452,7 @@ ParameterisedIdentifier::ParameterisedIdentifier(Item id, const Parameters& ps) ...@@ -323,7 +452,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 parser(str, StructuredHeaderParser::kDraft09);
base::Optional<Item> item = parser.ReadItem(); base::Optional<Item> item = parser.ReadItem();
if (item && parser.FinishParsing()) if (item && parser.FinishParsing())
return item; return item;
...@@ -332,7 +461,7 @@ base::Optional<Item> ParseItem(const base::StringPiece& str) { ...@@ -332,7 +461,7 @@ base::Optional<Item> ParseItem(const base::StringPiece& str) {
base::Optional<ParameterisedList> ParseParameterisedList( base::Optional<ParameterisedList> ParseParameterisedList(
const base::StringPiece& str) { const base::StringPiece& str) {
StructuredHeaderParser parser(str); StructuredHeaderParser parser(str, StructuredHeaderParser::kDraft09);
base::Optional<ParameterisedList> param_list = parser.ReadParameterisedList(); base::Optional<ParameterisedList> param_list = parser.ReadParameterisedList();
if (param_list && parser.FinishParsing()) if (param_list && parser.FinishParsing())
return param_list; return param_list;
...@@ -340,12 +469,20 @@ base::Optional<ParameterisedList> ParseParameterisedList( ...@@ -340,12 +469,20 @@ base::Optional<ParameterisedList> ParseParameterisedList(
} }
base::Optional<ListOfLists> ParseListOfLists(const base::StringPiece& str) { base::Optional<ListOfLists> ParseListOfLists(const base::StringPiece& str) {
StructuredHeaderParser parser(str); StructuredHeaderParser parser(str, StructuredHeaderParser::kDraft09);
base::Optional<ListOfLists> list_of_lists = parser.ReadListOfLists(); base::Optional<ListOfLists> list_of_lists = parser.ReadListOfLists();
if (list_of_lists && parser.FinishParsing()) if (list_of_lists && parser.FinishParsing())
return list_of_lists; return list_of_lists;
return base::nullopt; return base::nullopt;
} }
base::Optional<List> ParseList(const base::StringPiece& str) {
StructuredHeaderParser parser(str, StructuredHeaderParser::kDraft13);
base::Optional<List> list = parser.ReadList();
if (list && parser.FinishParsing())
return list;
return base::nullopt;
}
} // namespace http_structured_header } // namespace http_structured_header
} // namespace blink } // namespace blink
...@@ -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