Skip to content

Commit 27879c4

Browse files
authored
Fix #2157 (#2158)
* Fix #2157 * Fix Windows build error: wrap std::max in parentheses to avoid macro conflict - On Windows, max/min are often defined as macros by windows.h - This causes compilation errors with std::max/std::min - Solution: use (std::max) to prevent macro expansion - Fixes CI build error: error C2589: '(': illegal token on right side of '::' Fixes: error in coalesce_ranges function on line 5376
1 parent 08a0452 commit 27879c4

File tree

2 files changed

+142
-16
lines changed

2 files changed

+142
-16
lines changed

httplib.h

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5329,13 +5329,68 @@ serialize_multipart_formdata(const MultipartFormDataItems &items,
53295329
return body;
53305330
}
53315331

5332+
inline void coalesce_ranges(Ranges &ranges, size_t content_length) {
5333+
if (ranges.size() <= 1) return;
5334+
5335+
// Sort ranges by start position
5336+
std::sort(ranges.begin(), ranges.end(),
5337+
[](const Range &a, const Range &b) { return a.first < b.first; });
5338+
5339+
Ranges coalesced;
5340+
coalesced.reserve(ranges.size());
5341+
5342+
for (auto &r : ranges) {
5343+
auto first_pos = r.first;
5344+
auto last_pos = r.second;
5345+
5346+
// Handle special cases like in range_error
5347+
if (first_pos == -1 && last_pos == -1) {
5348+
first_pos = 0;
5349+
last_pos = static_cast<ssize_t>(content_length);
5350+
}
5351+
5352+
if (first_pos == -1) {
5353+
first_pos = static_cast<ssize_t>(content_length) - last_pos;
5354+
last_pos = static_cast<ssize_t>(content_length) - 1;
5355+
}
5356+
5357+
if (last_pos == -1 || last_pos >= static_cast<ssize_t>(content_length)) {
5358+
last_pos = static_cast<ssize_t>(content_length) - 1;
5359+
}
5360+
5361+
// Skip invalid ranges
5362+
if (!(0 <= first_pos && first_pos <= last_pos &&
5363+
last_pos < static_cast<ssize_t>(content_length))) {
5364+
continue;
5365+
}
5366+
5367+
// Coalesce with previous range if overlapping or adjacent (but not
5368+
// identical)
5369+
if (!coalesced.empty()) {
5370+
auto &prev = coalesced.back();
5371+
// Check if current range overlaps or is adjacent to previous range
5372+
// but don't coalesce identical ranges (allow duplicates)
5373+
if (first_pos <= prev.second + 1 &&
5374+
!(first_pos == prev.first && last_pos == prev.second)) {
5375+
// Extend the previous range
5376+
prev.second = (std::max)(prev.second, last_pos);
5377+
continue;
5378+
}
5379+
}
5380+
5381+
// Add new range
5382+
coalesced.emplace_back(first_pos, last_pos);
5383+
}
5384+
5385+
ranges = std::move(coalesced);
5386+
}
5387+
53325388
inline bool range_error(Request &req, Response &res) {
53335389
if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {
53345390
ssize_t content_len = static_cast<ssize_t>(
53355391
res.content_length_ ? res.content_length_ : res.body.size());
53365392

5337-
ssize_t prev_first_pos = -1;
5338-
ssize_t prev_last_pos = -1;
5393+
std::vector<std::pair<ssize_t, ssize_t>> processed_ranges;
53395394
size_t overwrapping_count = 0;
53405395

53415396
// NOTE: The following Range check is based on '14.2. Range' in RFC 9110
@@ -5378,18 +5433,21 @@ inline bool range_error(Request &req, Response &res) {
53785433
return true;
53795434
}
53805435

5381-
// Ranges must be in ascending order
5382-
if (first_pos <= prev_first_pos) { return true; }
5383-
53845436
// Request must not have more than two overlapping ranges
5385-
if (first_pos <= prev_last_pos) {
5386-
overwrapping_count++;
5387-
if (overwrapping_count > 2) { return true; }
5437+
for (const auto &processed_range : processed_ranges) {
5438+
if (!(last_pos < processed_range.first ||
5439+
first_pos > processed_range.second)) {
5440+
overwrapping_count++;
5441+
if (overwrapping_count > 2) { return true; }
5442+
break; // Only count once per range
5443+
}
53885444
}
53895445

5390-
prev_first_pos = (std::max)(prev_first_pos, first_pos);
5391-
prev_last_pos = (std::max)(prev_last_pos, last_pos);
5446+
processed_ranges.emplace_back(first_pos, last_pos);
53925447
}
5448+
5449+
// After validation, coalesce overlapping ranges as per RFC 9110
5450+
coalesce_ranges(req.ranges, static_cast<size_t>(content_len));
53935451
}
53945452

53955453
return false;

test/test.cc

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3992,6 +3992,16 @@ TEST_F(ServerTest, GetStreamedWithRangeMultipart) {
39923992
EXPECT_EQ("267", res->get_header_value("Content-Length"));
39933993
EXPECT_EQ(false, res->has_header("Content-Range"));
39943994
EXPECT_EQ(267U, res->body.size());
3995+
3996+
// Check that both range contents are present
3997+
EXPECT_TRUE(res->body.find("bc\r\n") != std::string::npos);
3998+
EXPECT_TRUE(res->body.find("ef\r\n") != std::string::npos);
3999+
4000+
// Check that Content-Range headers are present for both ranges
4001+
EXPECT_TRUE(res->body.find("Content-Range: bytes 1-2/7") !=
4002+
std::string::npos);
4003+
EXPECT_TRUE(res->body.find("Content-Range: bytes 4-5/7") !=
4004+
std::string::npos);
39954005
}
39964006

39974007
TEST_F(ServerTest, GetStreamedWithTooManyRanges) {
@@ -4009,14 +4019,59 @@ TEST_F(ServerTest, GetStreamedWithTooManyRanges) {
40094019
EXPECT_EQ(0U, res->body.size());
40104020
}
40114021

4022+
TEST_F(ServerTest, GetStreamedWithOverwrapping) {
4023+
auto res =
4024+
cli_.Get("/streamed-with-range", {{make_range_header({{1, 4}, {2, 5}})}});
4025+
ASSERT_TRUE(res);
4026+
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
4027+
EXPECT_EQ(5U, res->body.size());
4028+
4029+
// Check that overlapping ranges are coalesced into a single range
4030+
EXPECT_EQ("bcdef", res->body);
4031+
EXPECT_EQ("bytes 1-5/7", res->get_header_value("Content-Range"));
4032+
4033+
// Should be single range, not multipart
4034+
EXPECT_TRUE(res->has_header("Content-Range"));
4035+
EXPECT_EQ("text/plain", res->get_header_value("Content-Type"));
4036+
}
4037+
40124038
TEST_F(ServerTest, GetStreamedWithNonAscendingRanges) {
4013-
auto res = cli_.Get("/streamed-with-range?error",
4014-
{{make_range_header({{0, -1}, {0, -1}})}});
4039+
auto res =
4040+
cli_.Get("/streamed-with-range", {{make_range_header({{4, 5}, {0, 2}})}});
40154041
ASSERT_TRUE(res);
4016-
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
4017-
EXPECT_EQ("0", res->get_header_value("Content-Length"));
4018-
EXPECT_EQ(false, res->has_header("Content-Range"));
4019-
EXPECT_EQ(0U, res->body.size());
4042+
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
4043+
EXPECT_EQ(268U, res->body.size());
4044+
4045+
// Check that both range contents are present
4046+
EXPECT_TRUE(res->body.find("ef\r\n") != std::string::npos);
4047+
EXPECT_TRUE(res->body.find("abc\r\n") != std::string::npos);
4048+
4049+
// Check that Content-Range headers are present for both ranges
4050+
EXPECT_TRUE(res->body.find("Content-Range: bytes 4-5/7") !=
4051+
std::string::npos);
4052+
EXPECT_TRUE(res->body.find("Content-Range: bytes 0-2/7") !=
4053+
std::string::npos);
4054+
}
4055+
4056+
TEST_F(ServerTest, GetStreamedWithDuplicateRanges) {
4057+
auto res =
4058+
cli_.Get("/streamed-with-range", {{make_range_header({{0, 2}, {0, 2}})}});
4059+
ASSERT_TRUE(res);
4060+
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
4061+
EXPECT_EQ(269U, res->body.size());
4062+
4063+
// Check that both duplicate range contents are present
4064+
size_t first_abc = res->body.find("abc\r\n");
4065+
EXPECT_TRUE(first_abc != std::string::npos);
4066+
size_t second_abc = res->body.find("abc\r\n", first_abc + 1);
4067+
EXPECT_TRUE(second_abc != std::string::npos);
4068+
4069+
// Check that Content-Range headers are present for both ranges
4070+
size_t first_range = res->body.find("Content-Range: bytes 0-2/7");
4071+
EXPECT_TRUE(first_range != std::string::npos);
4072+
size_t second_range =
4073+
res->body.find("Content-Range: bytes 0-2/7", first_range + 1);
4074+
EXPECT_TRUE(second_range != std::string::npos);
40204075
}
40214076

40224077
TEST_F(ServerTest, GetStreamedWithRangesMoreThanTwoOverwrapping) {
@@ -4122,6 +4177,19 @@ TEST_F(ServerTest, GetWithRange4) {
41224177
EXPECT_EQ(std::string("fg"), res->body);
41234178
}
41244179

4180+
TEST_F(ServerTest, GetWithRange5) {
4181+
auto res = cli_.Get("/with-range", {
4182+
make_range_header({{0, 5}}),
4183+
{"Accept-Encoding", ""},
4184+
});
4185+
ASSERT_TRUE(res);
4186+
EXPECT_EQ(StatusCode::PartialContent_206, res->status);
4187+
EXPECT_EQ("6", res->get_header_value("Content-Length"));
4188+
EXPECT_EQ(true, res->has_header("Content-Range"));
4189+
EXPECT_EQ("bytes 0-5/7", res->get_header_value("Content-Range"));
4190+
EXPECT_EQ(std::string("abcdef"), res->body);
4191+
}
4192+
41254193
TEST_F(ServerTest, GetWithRangeOffsetGreaterThanContent) {
41264194
auto res = cli_.Get("/with-range", {{make_range_header({{10000, 20000}})}});
41274195
ASSERT_TRUE(res);

0 commit comments

Comments
 (0)