mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Add quality parameter parsing for Accept-Encoding header and enhance encoding type selection logic
This commit is contained in:
@@ -6471,33 +6471,114 @@ inline bool can_compress_content_type(const std::string &content_type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline bool parse_quality(const char *b, const char *e, std::string &token,
|
||||||
|
double &quality) {
|
||||||
|
quality = 1.0;
|
||||||
|
token.clear();
|
||||||
|
|
||||||
|
// Split on first ';': left = token name, right = parameters
|
||||||
|
const char *params_b = nullptr;
|
||||||
|
std::size_t params_len = 0;
|
||||||
|
|
||||||
|
divide(
|
||||||
|
b, static_cast<std::size_t>(e - b), ';',
|
||||||
|
[&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) {
|
||||||
|
auto r = trim(lb, lb + llen, 0, llen);
|
||||||
|
if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); }
|
||||||
|
params_b = rb;
|
||||||
|
params_len = rlen;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (token.empty()) { return false; }
|
||||||
|
if (params_len == 0) { return true; }
|
||||||
|
|
||||||
|
// Scan parameters for q= (stops on first match)
|
||||||
|
bool invalid = false;
|
||||||
|
split_find(params_b, params_b + params_len, ';',
|
||||||
|
(std::numeric_limits<size_t>::max)(),
|
||||||
|
[&](const char *pb, const char *pe) -> bool {
|
||||||
|
// Match exactly "q=" or "Q=" (not "query=" etc.)
|
||||||
|
auto len = static_cast<size_t>(pe - pb);
|
||||||
|
if (len < 2) { return false; }
|
||||||
|
if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim the value portion
|
||||||
|
auto r = trim(pb, pe, 2, len);
|
||||||
|
if (r.first >= r.second) {
|
||||||
|
invalid = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double v = 0.0;
|
||||||
|
auto res = from_chars(pb + r.first, pb + r.second, v);
|
||||||
|
if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
|
||||||
|
invalid = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
quality = v;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return !invalid;
|
||||||
|
}
|
||||||
|
|
||||||
inline EncodingType encoding_type(const Request &req, const Response &res) {
|
inline EncodingType encoding_type(const Request &req, const Response &res) {
|
||||||
auto ret =
|
if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
|
||||||
detail::can_compress_content_type(res.get_header_value("Content-Type"));
|
return EncodingType::None;
|
||||||
if (!ret) { return EncodingType::None; }
|
}
|
||||||
|
|
||||||
const auto &s = req.get_header_value("Accept-Encoding");
|
const auto &s = req.get_header_value("Accept-Encoding");
|
||||||
(void)(s);
|
if (s.empty()) { return EncodingType::None; }
|
||||||
|
|
||||||
|
// Single-pass: iterate tokens and track the best supported encoding.
|
||||||
|
// Server preference breaks ties (br > gzip > zstd).
|
||||||
|
EncodingType best = EncodingType::None;
|
||||||
|
double best_q = 0.0; // q=0 means "not acceptable"
|
||||||
|
|
||||||
|
// Server preference: Brotli > Gzip > Zstd (lower = more preferred)
|
||||||
|
auto priority = [](EncodingType t) -> int {
|
||||||
|
switch (t) {
|
||||||
|
case EncodingType::Brotli: return 0;
|
||||||
|
case EncodingType::Gzip: return 1;
|
||||||
|
case EncodingType::Zstd: return 2;
|
||||||
|
default: return 3;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
|
||||||
|
double quality = 1.0;
|
||||||
|
if (!parse_quality(b, e, name, quality)) { return; }
|
||||||
|
if (quality <= 0.0) { return; }
|
||||||
|
|
||||||
|
EncodingType type = EncodingType::None;
|
||||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||||
// TODO: 'Accept-Encoding' has br, not br;q=0
|
if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; }
|
||||||
ret = s.find("br") != std::string::npos;
|
|
||||||
if (ret) { return EncodingType::Brotli; }
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||||
// TODO: 'Accept-Encoding' has gzip, not gzip;q=0
|
if (type == EncodingType::None && case_ignore::equal(name, "gzip")) {
|
||||||
ret = s.find("gzip") != std::string::npos;
|
type = EncodingType::Gzip;
|
||||||
if (ret) { return EncodingType::Gzip; }
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
|
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
|
||||||
// TODO: 'Accept-Encoding' has zstd, not zstd;q=0
|
if (type == EncodingType::None && case_ignore::equal(name, "zstd")) {
|
||||||
ret = s.find("zstd") != std::string::npos;
|
type = EncodingType::Zstd;
|
||||||
if (ret) { return EncodingType::Zstd; }
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return EncodingType::None;
|
if (type == EncodingType::None) { return; }
|
||||||
|
|
||||||
|
// Higher q-value wins; for equal q, server preference breaks ties
|
||||||
|
if (quality > best_q ||
|
||||||
|
(quality == best_q && priority(type) < priority(best))) {
|
||||||
|
best_q = quality;
|
||||||
|
best = type;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool nocompressor::compress(const char *data, size_t data_length,
|
inline bool nocompressor::compress(const char *data, size_t data_length,
|
||||||
@@ -7613,7 +7694,7 @@ inline bool parse_accept_header(const std::string &s,
|
|||||||
struct AcceptEntry {
|
struct AcceptEntry {
|
||||||
std::string media_type;
|
std::string media_type;
|
||||||
double quality;
|
double quality;
|
||||||
int order; // Original order in header
|
int order;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<AcceptEntry> entries;
|
std::vector<AcceptEntry> entries;
|
||||||
@@ -7631,48 +7712,12 @@ inline bool parse_accept_header(const std::string &s,
|
|||||||
}
|
}
|
||||||
|
|
||||||
AcceptEntry accept_entry;
|
AcceptEntry accept_entry;
|
||||||
accept_entry.quality = 1.0; // Default quality
|
|
||||||
accept_entry.order = order++;
|
accept_entry.order = order++;
|
||||||
|
|
||||||
// Find q= parameter
|
if (!parse_quality(entry.data(), entry.data() + entry.size(),
|
||||||
auto q_pos = entry.find(";q=");
|
accept_entry.media_type, accept_entry.quality)) {
|
||||||
if (q_pos == std::string::npos) { q_pos = entry.find("; q="); }
|
has_invalid_entry = true;
|
||||||
|
return;
|
||||||
if (q_pos != std::string::npos) {
|
|
||||||
// Extract media type (before q parameter)
|
|
||||||
accept_entry.media_type = trim_copy(entry.substr(0, q_pos));
|
|
||||||
|
|
||||||
// Extract quality value
|
|
||||||
auto q_start = entry.find('=', q_pos) + 1;
|
|
||||||
auto q_end = entry.find(';', q_start);
|
|
||||||
if (q_end == std::string::npos) { q_end = entry.length(); }
|
|
||||||
|
|
||||||
std::string quality_str =
|
|
||||||
trim_copy(entry.substr(q_start, q_end - q_start));
|
|
||||||
if (quality_str.empty()) {
|
|
||||||
has_invalid_entry = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
double v = 0.0;
|
|
||||||
auto res = detail::from_chars(
|
|
||||||
quality_str.data(), quality_str.data() + quality_str.size(), v);
|
|
||||||
if (res.ec == std::errc{}) {
|
|
||||||
accept_entry.quality = v;
|
|
||||||
} else {
|
|
||||||
has_invalid_entry = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check if quality is in valid range [0.0, 1.0]
|
|
||||||
if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {
|
|
||||||
has_invalid_entry = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No quality parameter, use entire entry as media type
|
|
||||||
accept_entry.media_type = entry;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove additional parameters from media type
|
// Remove additional parameters from media type
|
||||||
|
|||||||
@@ -1267,6 +1267,89 @@ TEST(ParseAcceptEncoding3, AcceptEncoding) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ParseAcceptEncoding4, AcceptEncodingQZero) {
|
||||||
|
// All supported encodings rejected with q=0 should return None
|
||||||
|
Request req;
|
||||||
|
req.set_header("Accept-Encoding", "gzip;q=0, br;q=0, zstd;q=0, deflate");
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
res.set_header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
auto ret = detail::encoding_type(req, res);
|
||||||
|
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ParseAcceptEncoding5, AcceptEncodingQZeroVariants) {
|
||||||
|
// q=0.0, q=0.00, q=0.000 should also be treated as rejected
|
||||||
|
Request req;
|
||||||
|
req.set_header("Accept-Encoding", "gzip;q=0.000, br;q=0.0, zstd;q=0.00");
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
res.set_header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
auto ret = detail::encoding_type(req, res);
|
||||||
|
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ParseAcceptEncoding6, AcceptEncodingXGzipQZero) {
|
||||||
|
// x-gzip;q=0 should not cause "gzip" to be incorrectly detected
|
||||||
|
Request req;
|
||||||
|
req.set_header("Accept-Encoding", "x-gzip;q=0");
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
res.set_header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
auto ret = detail::encoding_type(req, res);
|
||||||
|
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ParseAcceptEncoding7, AcceptEncodingCaseInsensitive) {
|
||||||
|
// RFC 7231: Accept-Encoding values are case-insensitive
|
||||||
|
Request req;
|
||||||
|
req.set_header("Accept-Encoding", "GZIP, BR, ZSTD");
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
res.set_header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
auto ret = detail::encoding_type(req, res);
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::Brotli);
|
||||||
|
#elif CPPHTTPLIB_ZLIB_SUPPORT
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::Gzip);
|
||||||
|
#elif CPPHTTPLIB_ZSTD_SUPPORT
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::Zstd);
|
||||||
|
#else
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ParseAcceptEncoding8, AcceptEncodingQValuePriority) {
|
||||||
|
// q value should determine priority, not hardcoded order
|
||||||
|
Request req;
|
||||||
|
req.set_header("Accept-Encoding", "br;q=0.5, gzip;q=1.0, zstd;q=0.8");
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
res.set_header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
auto ret = detail::encoding_type(req, res);
|
||||||
|
|
||||||
|
// gzip has highest q=1.0, so it should be selected even though
|
||||||
|
// br and zstd are also supported
|
||||||
|
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::Gzip);
|
||||||
|
#elif CPPHTTPLIB_ZSTD_SUPPORT
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::Zstd);
|
||||||
|
#elif CPPHTTPLIB_BROTLI_SUPPORT
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::Brotli);
|
||||||
|
#else
|
||||||
|
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
TEST(BufferStreamTest, read) {
|
TEST(BufferStreamTest, read) {
|
||||||
detail::BufferStream strm1;
|
detail::BufferStream strm1;
|
||||||
Stream &strm = strm1;
|
Stream &strm = strm1;
|
||||||
|
|||||||
Reference in New Issue
Block a user