diff --git a/httplib.h b/httplib.h index 89403dae..3887b228 100644 --- a/httplib.h +++ b/httplib.h @@ -3988,6 +3988,10 @@ public: bool is_open() const; size_t size() const; const char *data() const; + // Copies up to len bytes from offset into buf and returns how many. Unlike + // reading data(), it gives fewer, down to 0, for a file that has shrunk + // since it was opened, rather than a SIGBUS. + size_t read(size_t offset, char *buf, size_t len) const; private: #if defined(_WIN32) @@ -6520,6 +6524,22 @@ inline const char *mmap::data() const { return is_open_empty_file ? "" : static_cast(addr_); } +inline size_t mmap::read(size_t offset, char *buf, size_t len) const { + if (offset >= size_) { return 0; } + len = (std::min)(len, size_ - offset); +#if defined(_WIN32) + // Windows does not let a mapped file be truncated. + std::memcpy(buf, static_cast(addr_) + offset, len); + return len; +#else + ssize_t n; + do { + n = ::pread(fd_, buf, len, static_cast(offset)); + } while (n < 0 && errno == EINTR); + return n > 0 ? static_cast(n) : 0; +#endif +} + inline void mmap::close() { #if defined(_WIN32) if (addr_) { @@ -8991,9 +9011,12 @@ inline void set_file_content_provider(Response &res, res.set_content_provider( m->size(), content_type, [m](size_t offset, size_t length, DataSink &sink) -> bool { - // Never read past the mapping. - if (offset >= m->size()) { return false; } - sink.write(m->data() + offset, (std::min)(length, m->size() - offset)); + // Never read past the file, which ends the response early if it has + // shrunk since it was opened. + char buf[CPPHTTPLIB_SEND_BUFSIZ]; + auto n = m->read(offset, buf, (std::min)(length, sizeof(buf))); + if (n == 0) { return false; } + sink.write(buf, n); return true; }); diff --git a/test/test.cc b/test/test.cc index 870e0633..a1fbfbcb 100644 --- a/test/test.cc +++ b/test/test.cc @@ -12797,6 +12797,45 @@ TEST(ResponseContentSourceTest, LastContentSetterWins) { } } +#if defined(CPPHTTPLIB_SSL_ENABLED) && !defined(_WIN32) +TEST(FileContentTest, TruncatedWhileServedEndsTheResponse) { + const char *path = "./truncated_while_served.txt"; + const size_t size = 32 * 1024 * 1024; + { + std::ofstream f(path, std::ios::binary); + f << std::string(size, 'x'); + } + auto se_file = detail::scope_exit([&] { std::remove(path); }); + + // Over TLS, where the bytes are encrypted in this process rather than + // copied by send(). + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + svr.Get("/file", [&](const Request &, Response &res) { + res.set_file_content(path, "text/plain"); + }); + auto thread = std::thread([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + thread.join(); + ASSERT_FALSE(svr.is_running()); + }); + svr.wait_until_ready(); + + // The file shrinks once the first bytes are in, as a rewrite in place + // would, and reading the mapping past its new end used to raise SIGBUS. + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(false); + size_t received = 0; + auto res = cli.Get("/file", [&](const char *, size_t len) { + if (received == 0) { EXPECT_EQ(0, ::truncate(path, 0)); } + received += len; + return true; + }); + EXPECT_FALSE(res); + EXPECT_LT(received, size); +} +#endif + // Forward declaration (see the base64_encode note). namespace httplib { namespace detail {