Add content-encoding parser

Similar to content-type, add an http content-encoding parser.

Tested: Unit tests pass.

Change-Id: Ic62809934f84804c910458184de19ca9a4207ce5
Signed-off-by: Ed Tanous <etanous@nvidia.com>
diff --git a/include/http_utility.hpp b/include/http_utility.hpp
index 5b8996c..158228f 100644
--- a/include/http_utility.hpp
+++ b/include/http_utility.hpp
@@ -27,7 +27,7 @@
 };
 
 inline ContentType getPreferredContentType(
-    std::string_view header, std::span<const ContentType> preferedOrder)
+    std::string_view header, std::span<const ContentType> preferredOrder)
 {
     using boost::spirit::x3::char_;
     using boost::spirit::x3::lit;
@@ -63,8 +63,8 @@
         {
             return parsedType;
         }
-        auto it = std::ranges::find(preferedOrder, parsedType);
-        if (it != preferedOrder.end())
+        auto it = std::ranges::find(preferredOrder, parsedType);
+        if (it != preferredOrder.end())
         {
             return *it;
         }
@@ -86,4 +86,72 @@
     return type == allowed;
 }
 
+enum class Encoding
+{
+    ParseError,
+    NoMatch,
+    UnencodedBytes,
+    GZIP,
+    ZSTD,
+    ANY, // represents *. Never returned.  Only used for string matching
+};
+
+inline Encoding
+    getPreferredEncoding(std::string_view acceptEncoding,
+                         const std::span<const Encoding> availableEncodings)
+{
+    if (acceptEncoding.empty())
+    {
+        return Encoding::UnencodedBytes;
+    }
+
+    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<Encoding> knownAcceptEncoding{{"gzip", Encoding::GZIP},
+                                                {"zstd", Encoding::ZSTD},
+                                                {"*", Encoding::ANY}};
+
+    std::vector<Encoding> ct;
+
+    auto parameters = *(lit(';') >> lit("q=") >> uint_ >> -(lit('.') >> uint_));
+    auto typeCharset = char_("a-zA-Z.+-");
+    auto encodeType = knownAcceptEncoding | omit[+typeCharset];
+    auto parser = +(encodeType >> omit[parameters >> -char_(',') >> *space]);
+    if (!parse(acceptEncoding.begin(), acceptEncoding.end(), parser, ct))
+    {
+        return Encoding::ParseError;
+    }
+
+    for (const Encoding parsedType : ct)
+    {
+        if (parsedType == Encoding::ANY)
+        {
+            if (!availableEncodings.empty())
+            {
+                return *availableEncodings.begin();
+            }
+        }
+        auto it = std::ranges::find(availableEncodings, parsedType);
+        if (it != availableEncodings.end())
+        {
+            return *it;
+        }
+    }
+
+    // Fall back to raw bytes if it was allowed
+    auto it = std::ranges::find(availableEncodings, Encoding::UnencodedBytes);
+    if (it != availableEncodings.end())
+    {
+        return *it;
+    }
+
+    return Encoding::NoMatch;
+}
+
 } // namespace http_helpers
diff --git a/test/include/http_utility_test.cpp b/test/include/http_utility_test.cpp
index 72c4759..2a5e8a7 100644
--- a/test/include/http_utility_test.cpp
+++ b/test/include/http_utility_test.cpp
@@ -114,5 +114,32 @@
         getPreferredContentType("text/html, application/json", contentType),
         ContentType::NoMatch);
 }
+
+TEST(getPreferredEncoding, PositiveTest)
+{
+    std::array<Encoding, 1> encodingsGzip{Encoding::GZIP};
+    EXPECT_EQ(getPreferredEncoding("gzip", encodingsGzip), Encoding::GZIP);
+
+    std::array<Encoding, 2> encodingsGzipZstd{Encoding::GZIP, Encoding::ZSTD};
+    EXPECT_EQ(getPreferredEncoding("gzip", encodingsGzipZstd), Encoding::GZIP);
+    EXPECT_EQ(getPreferredEncoding("zstd", encodingsGzipZstd), Encoding::ZSTD);
+
+    EXPECT_EQ(getPreferredEncoding("*", encodingsGzipZstd), Encoding::GZIP);
+
+    EXPECT_EQ(getPreferredEncoding("zstd, gzip;q=1.0", encodingsGzipZstd),
+              Encoding::ZSTD);
+}
+
+TEST(getPreferredEncoding, NegativeTest)
+{
+    std::array<Encoding, 2> contentType{Encoding::GZIP,
+                                        Encoding::UnencodedBytes};
+    EXPECT_EQ(getPreferredEncoding("noexist", contentType),
+              Encoding::UnencodedBytes);
+
+    std::array<Encoding, 1> contentType2{Encoding::GZIP};
+    EXPECT_EQ(getPreferredEncoding("zstd", contentType2), Encoding::NoMatch);
+}
+
 } // namespace
 } // namespace http_helpers