From bb7fb95b57eb00e8b31ab1b0326cadf024ccc204 Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Sat, 9 Jan 2016 22:15:22 -0800 Subject: [PATCH] Helper for composing a VirtualFile from "slices". --- src/http-test.cc | 78 ++++++++++++++++++++++++++++++++++++++++++++++++ src/http.cc | 22 ++++++++++++++ src/http.h | 37 +++++++++++++++++++++-- 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/src/http-test.cc b/src/http-test.cc index 541c990..d56741b 100644 --- a/src/http-test.cc +++ b/src/http-test.cc @@ -49,9 +49,21 @@ DECLARE_bool(alsologtostderr); using moonfire_nvr::internal::ParseRangeHeader; using moonfire_nvr::internal::RangeHeaderType; +using testing::_; +using testing::AnyNumber; +using testing::DoAll; +using testing::Return; +using testing::SetArgPointee; + namespace moonfire_nvr { namespace { +class MockFileSlice : public FileSlice { + public: + MOCK_CONST_METHOD0(size, int64_t()); + MOCK_CONST_METHOD3(AddRange, bool(ByteRange, EvBuffer *, std::string *)); +}; + TEST(EvBufferTest, AddFileTest) { std::string dir = PrepareTempDirOrDie("http"); std::string foo_filename = StrCat(dir, "/foo"); @@ -72,6 +84,72 @@ TEST(EvBufferTest, AddFileTest) { EXPECT_EQ(0u, evbuffer_get_length(buf2.get())); } +class FileSlicesTest : public testing::Test { + protected: + FileSlicesTest() { + EXPECT_CALL(a_, size()).Times(AnyNumber()).WillRepeatedly(Return(5)); + EXPECT_CALL(b_, size()).Times(AnyNumber()).WillRepeatedly(Return(13)); + EXPECT_CALL(c_, size()).Times(AnyNumber()).WillRepeatedly(Return(7)); + EXPECT_CALL(d_, size()).Times(AnyNumber()).WillRepeatedly(Return(17)); + EXPECT_CALL(e_, size()).Times(AnyNumber()).WillRepeatedly(Return(19)); + + slices_.Append(&a_); + slices_.Append(&b_); + slices_.Append(&c_); + slices_.Append(&d_); + slices_.Append(&e_); + } + + FileSlices slices_; + testing::StrictMock a_; + testing::StrictMock b_; + testing::StrictMock c_; + testing::StrictMock d_; + testing::StrictMock e_; +}; + +TEST_F(FileSlicesTest, Size) { + EXPECT_EQ(5 + 13 + 7 + 17 + 19, slices_.size()); +} + +TEST_F(FileSlicesTest, ExactSlice) { + // Exactly slice b. + std::string error_message; + EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)).WillOnce(Return(true)); + EXPECT_TRUE(slices_.AddRange(ByteRange(5, 18), nullptr, &error_message)) + << error_message; +} + +TEST_F(FileSlicesTest, Offset) { + // Part of slice b, all of slice c, and part of slice d. + std::string error_message; + EXPECT_CALL(b_, AddRange(ByteRange(12, 13), _, _)).WillOnce(Return(true)); + EXPECT_CALL(c_, AddRange(ByteRange(0, 7), _, _)).WillOnce(Return(true)); + EXPECT_CALL(d_, AddRange(ByteRange(0, 1), _, _)).WillOnce(Return(true)); + EXPECT_TRUE(slices_.AddRange(ByteRange(17, 26), nullptr, &error_message)) + << error_message; +} + +TEST_F(FileSlicesTest, Everything) { + std::string error_message; + EXPECT_CALL(a_, AddRange(ByteRange(0, 5), _, _)).WillOnce(Return(true)); + EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)).WillOnce(Return(true)); + EXPECT_CALL(c_, AddRange(ByteRange(0, 7), _, _)).WillOnce(Return(true)); + EXPECT_CALL(d_, AddRange(ByteRange(0, 17), _, _)).WillOnce(Return(true)); + EXPECT_CALL(e_, AddRange(ByteRange(0, 19), _, _)).WillOnce(Return(true)); + EXPECT_TRUE(slices_.AddRange(ByteRange(0, 61), nullptr, &error_message)) + << error_message; +} + +TEST_F(FileSlicesTest, PropagateError) { + std::string error_message; + EXPECT_CALL(a_, AddRange(ByteRange(0, 5), _, _)).WillOnce(Return(true)); + EXPECT_CALL(b_, AddRange(ByteRange(0, 13), _, _)) + .WillOnce(DoAll(SetArgPointee<2>("asdf"), Return(false))); + EXPECT_FALSE(slices_.AddRange(ByteRange(0, 61), nullptr, &error_message)); + EXPECT_EQ("asdf", error_message); +} + // Test the specific examples enumerated in RFC 2616 section 14.35.1. TEST(RangeHeaderTest, Rfc_2616_Section_14_35_1) { std::vector ranges; diff --git a/src/http.cc b/src/http.cc index 231f403..6d9d354 100644 --- a/src/http.cc +++ b/src/http.cc @@ -205,6 +205,28 @@ bool FillerFileSlice::AddRange(ByteRange range, EvBuffer *buf, return true; } +bool FileSlices::AddRange(ByteRange range, EvBuffer *buf, + std::string *error_message) const { + if (range.begin < 0 || range.begin > range.end || range.end > size_) { + *error_message = StrCat("Range ", range.DebugString(), + " not valid for file of size ", size_); + return false; + } + auto it = std::upper_bound(slices_.begin(), slices_.end(), range.begin, + [](int64_t begin, const SliceInfo &info) { + return begin < info.range.end; + }); + for (; it != slices_.end() && range.end > it->range.begin; ++it) { + ByteRange mapped( + std::max(INT64_C(0), range.begin - it->range.begin), + std::min(range.end - it->range.begin, it->range.end - it->range.begin)); + if (!it->slice->AddRange(mapped, buf, error_message)) { + return false; + } + } + return true; +} + void HttpSendError(evhttp_request *req, int http_err, const std::string &prefix, int posix_err) { evhttp_send_error(req, http_err, diff --git a/src/http.h b/src/http.h index b9fa38c..3088f7f 100644 --- a/src/http.h +++ b/src/http.h @@ -48,6 +48,8 @@ #include #include +#include "string.h" + namespace moonfire_nvr { // Wrapped version of libevent's "struct evbuffer" which uses RAII and simply @@ -102,11 +104,11 @@ struct ByteRange { bool operator==(const ByteRange &o) const { return begin == o.begin && end == o.end; } + std::string DebugString() const { return StrCat("[", begin, ", ", end, ")"); } }; inline std::ostream &operator<<(std::ostream &out, const ByteRange &range) { - out << "[" << range.begin << ", " << range.end << ")"; - return out; + return out << range.DebugString(); } // Helper for sending HTTP errors based on POSIX error returns. @@ -154,6 +156,37 @@ class FillerFileSlice : public FileSlice { size_t size_; }; +// A slice composed of other slices. +class FileSlices : public FileSlice { + public: + FileSlices() {} + FileSlices(const FileSlices &) = delete; + FileSlices &operator=(const FileSlices &) = delete; + + // |slice| must outlive the FileSlices. + // |slice->size()| should not change after this call. + void Append(const FileSlice *slice) { + int64_t new_size = size_ + slice->size(); + slices_.emplace_back(ByteRange(size_, new_size), slice); + size_ = new_size; + } + + int64_t size() const final { return size_; } + bool AddRange(ByteRange range, EvBuffer *buf, + std::string *error_message) const final; + + private: + struct SliceInfo { + SliceInfo(ByteRange range, const FileSlice *slice) + : range(range), slice(slice) {} + ByteRange range; + const FileSlice *slice = nullptr; + }; + int64_t size_ = 0; + + std::vector slices_; +}; + // Serve an HTTP request |req| from |file|, handling byte range and // conditional serving. (Similar to golang's http.ServeContent.) //