Make accepts a real parser
We somewhat copped out a little with regards to this originally, because
writing parsers is hard, and we don't have to implement the full field
of what the Accepts header allows.
We should aim to be correct where we can, so implement a real parser
that parses values, including the floats.
Tested: Unit tests pass, good coverage.
Change-Id: I1b4232929367d230641be9f41f5af6e6dbcea037
Signed-off-by: Ed Tanous <etanous@nvidia.com>
diff --git a/include/http_utility.hpp b/include/http_utility.hpp
index 3eef889..5b8996c 100644
--- a/include/http_utility.hpp
+++ b/include/http_utility.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include <boost/spirit/home/x3.hpp>
+
#include <algorithm>
#include <cctype>
#include <iomanip>
@@ -24,71 +26,50 @@
EventStream,
};
-struct ContentTypePair
-{
- std::string_view contentTypeString;
- ContentType contentTypeEnum;
-};
-
-constexpr std::array<ContentTypePair, 5> contentTypes{{
- {"application/cbor", ContentType::CBOR},
- {"application/json", ContentType::JSON},
- {"application/octet-stream", ContentType::OctetStream},
- {"text/html", ContentType::HTML},
- {"text/event-stream", ContentType::EventStream},
-}};
-
inline ContentType getPreferredContentType(
std::string_view header, std::span<const ContentType> preferedOrder)
{
- size_t lastIndex = 0;
- while (lastIndex < header.size() + 1)
+ using boost::spirit::x3::char_;
+ using boost::spirit::x3::lit;
+ using boost::spirit::x3::omit;
+ using boost::spirit::x3::parse;
+ using boost::spirit::x3::space;
+ using boost::spirit::x3::symbols;
+ using boost::spirit::x3::uint_;
+
+ const symbols<ContentType> knownMimeType{
+ {"application/cbor", ContentType::CBOR},
+ {"application/json", ContentType::JSON},
+ {"application/octet-stream", ContentType::OctetStream},
+ {"text/html", ContentType::HTML},
+ {"text/event-stream", ContentType::EventStream},
+ {"*/*", ContentType::ANY}};
+
+ std::vector<ContentType> ct;
+
+ auto parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_));
+ auto typeCharset = char_("a-zA-Z.+-");
+ auto mimeType = knownMimeType |
+ omit[+typeCharset >> lit('/') >> +typeCharset];
+ auto parser = +(mimeType >> omit[parameters >> -char_(',') >> *space]);
+ if (!parse(header.begin(), header.end(), parser, ct))
{
- size_t index = header.find(',', lastIndex);
- if (index == std::string_view::npos)
- {
- index = header.size();
- }
- std::string_view encoding = header.substr(lastIndex, index);
-
- if (!header.empty())
- {
- header.remove_prefix(1);
- }
- lastIndex = index + 1;
- // ignore any q-factor weighting (;q=)
- std::size_t separator = encoding.find(";q=");
-
- if (separator != std::string_view::npos)
- {
- encoding = encoding.substr(0, separator);
- }
- // If the client allows any encoding, given them the first one on the
- // servers list
- if (encoding == "*/*")
- {
- return ContentType::ANY;
- }
- const auto* knownContentType = std::ranges::find_if(
- contentTypes, [encoding](const ContentTypePair& pair) {
- return pair.contentTypeString == encoding;
- });
-
- if (knownContentType == contentTypes.end())
- {
- // not able to find content type in list
- continue;
- }
-
- // Not one of the types requested
- if (std::ranges::find(preferedOrder,
- knownContentType->contentTypeEnum) ==
- preferedOrder.end())
- {
- continue;
- }
- return knownContentType->contentTypeEnum;
+ return ContentType::NoMatch;
}
+
+ for (const ContentType parsedType : ct)
+ {
+ if (parsedType == ContentType::ANY)
+ {
+ return parsedType;
+ }
+ auto it = std::ranges::find(preferedOrder, parsedType);
+ if (it != preferedOrder.end())
+ {
+ return *it;
+ }
+ }
+
return ContentType::NoMatch;
}
diff --git a/test/include/http_utility_test.cpp b/test/include/http_utility_test.cpp
index d4823da..72c4759 100644
--- a/test/include/http_utility_test.cpp
+++ b/test/include/http_utility_test.cpp
@@ -60,6 +60,18 @@
EXPECT_EQ(getPreferredContentType("text/html, application/json", htmlJson),
ContentType::HTML);
+ // String the chrome gives
+ EXPECT_EQ(getPreferredContentType(
+ "text/html,"
+ "application/xhtml+xml,"
+ "application/xml;q=0.9,"
+ "image/avif,"
+ "image/webp,"
+ "image/apng,*/*;q=0.8,"
+ "application/signed-exchange;v=b3;q=0.7",
+ htmlJson),
+ ContentType::HTML);
+
std::array<ContentType, 2> jsonHtml{ContentType::JSON, ContentType::HTML};
EXPECT_EQ(getPreferredContentType("text/html, application/json", jsonHtml),
ContentType::HTML);
@@ -72,6 +84,27 @@
EXPECT_EQ(getPreferredContentType("application/json", cborJson),
ContentType::JSON);
EXPECT_EQ(getPreferredContentType("*/*", cborJson), ContentType::ANY);
+
+ // Application types with odd characters
+ EXPECT_EQ(getPreferredContentType(
+ "application/prs.nprend, application/json", cborJson),
+ ContentType::JSON);
+
+ EXPECT_EQ(getPreferredContentType("application/rdf+xml, application/json",
+ cborJson),
+ ContentType::JSON);
+
+ // Q values are ignored, but should parse
+ EXPECT_EQ(getPreferredContentType(
+ "application/rdf+xml;q=0.9, application/json", cborJson),
+ ContentType::JSON);
+ EXPECT_EQ(getPreferredContentType(
+ "application/rdf+xml;q=1, application/json", cborJson),
+ ContentType::JSON);
+ EXPECT_EQ(getPreferredContentType("application/json;q=0.9", cborJson),
+ ContentType::JSON);
+ EXPECT_EQ(getPreferredContentType("application/json;q=1", cborJson),
+ ContentType::JSON);
}
TEST(getPreferredContentType, NegativeTest)