diff --git a/httplib.h b/httplib.h index 89d15af4..c63d4561 100644 --- a/httplib.h +++ b/httplib.h @@ -1523,7 +1523,10 @@ make_file_body(const std::string &filepath) { auto to_read = (std::min)(sizeof(buf), length); f.read(buf, static_cast(to_read)); auto n = static_cast(f.gcount()); - if (n == 0) { break; } + // The file is shorter than the size make_file_body() measured, which the + // caller has already committed to as Content-Length. The body cannot be + // completed, so fail as every other error here does. + if (n == 0) { return false; } if (!sink.write(buf, n)) { return false; } length -= n; } diff --git a/test/test.cc b/test/test.cc index 23da37bc..84dda3a5 100644 --- a/test/test.cc +++ b/test/test.cc @@ -16142,6 +16142,66 @@ TEST(MakeFileBodyTest, Basic) { EXPECT_EQ(StatusCode::OK_200, res->status); } +TEST(MakeFileBodyTest, TruncatedFileMakesTheProviderFail) { + const std::string path = "./httplib_test_make_file_body_truncated.bin"; + { + std::ofstream ofs(path, std::ios::binary); + const std::string content(100, 'A'); + ofs.write(content.data(), static_cast(content.size())); + } + auto cleanup = detail::scope_exit([&] { std::remove(path.c_str()); }); + + auto body = make_file_body(path); + ASSERT_EQ(100u, body.first); + ASSERT_TRUE(static_cast(body.second)); + + // Rewritten shorter after its length was measured - and, on a server, after + // that length has already gone out as Content-Length. + { + std::ofstream ofs(path, std::ios::binary | std::ios::trunc); + const std::string content(10, 'A'); + ofs.write(content.data(), static_cast(content.size())); + } + + std::string written; + DataSink sink; + sink.write = [&](const char *d, size_t l) { + written.append(d, l); + return true; + }; + + // The provider has to report failure. write_content_with_progress() advances + // its offset only by what was written, so a provider that returns true + // without completing the length it promised is called again straight away, + // and again. + EXPECT_FALSE(body.second(0, body.first, sink)); + EXPECT_EQ(std::string(10, 'A'), written); +} + +TEST(MakeFileBodyTest, WholeFileIsSent) { + const std::string path = "./httplib_test_make_file_body_whole.bin"; + const std::string content(9000, 'Z'); // spans more than one 8 KiB read + { + std::ofstream ofs(path, std::ios::binary); + ofs.write(content.data(), static_cast(content.size())); + } + auto cleanup = detail::scope_exit([&] { std::remove(path.c_str()); }); + + auto body = make_file_body(path); + ASSERT_EQ(content.size(), body.first); + ASSERT_TRUE(static_cast(body.second)); + + std::string written; + DataSink sink; + sink.write = [&](const char *d, size_t l) { + written.append(d, l); + return true; + }; + + EXPECT_TRUE(body.second(0, body.first, sink)); + EXPECT_EQ(content, written); +} + TEST(TaskQueueTest, IncreaseAtomicInteger) { static constexpr unsigned int number_of_tasks{1000000}; std::atomic_uint count{0};