From 977a7a805210856303bfd32de7f7d52b0edcfdce Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Fri, 24 Oct 2014 15:04:25 -0400 Subject: [PATCH] Add HTTPBodyStream interface, three concrete implementations, and their tests. BUG=415544 R=mark@chromium.org Review URL: https://codereview.chromium.org/669153006 --- util/file/fd_io.cc | 6 + util/file/fd_io.h | 6 + util/net/http_body.cc | 116 ++++++++++++ util/net/http_body.h | 130 ++++++++++++++ util/net/http_body_test.cc | 237 +++++++++++++++++++++++++ util/net/testdata/ascii_http_body.txt | 1 + util/net/testdata/binary_http_body.dat | 1 + util/util.gyp | 3 + 8 files changed, 500 insertions(+) create mode 100644 util/net/http_body.cc create mode 100644 util/net/http_body.h create mode 100644 util/net/http_body_test.cc create mode 100644 util/net/testdata/ascii_http_body.txt create mode 100644 util/net/testdata/binary_http_body.dat diff --git a/util/file/fd_io.cc b/util/file/fd_io.cc index 46a3634f..95257d83 100644 --- a/util/file/fd_io.cc +++ b/util/file/fd_io.cc @@ -104,4 +104,10 @@ void CheckedReadFDAtEOF(int fd) { } } +bool LoggingCloseFD(int fd) { + int rv = IGNORE_EINTR(close(fd)); + PLOG_IF(ERROR, rv != 0) << "close"; + return rv == 0; +} + } // namespace crashpad diff --git a/util/file/fd_io.h b/util/file/fd_io.h index 14481c93..909e0d92 100644 --- a/util/file/fd_io.h +++ b/util/file/fd_io.h @@ -79,6 +79,12 @@ void CheckedWriteFD(int fd, const void* buffer, size_t size); //! \sa ReadFD void CheckedReadFDAtEOF(int fd); +//! \brief Wraps `close()`, logging an error if the operation fails. +//! +//! \return On success, `true` is returned. On failure, an error is logged and +//! `false` is returned. +bool LoggingCloseFD(int fd); + } // namespace crashpad #endif // CRASHPAD_UTIL_FILE_FD_IO_H_ diff --git a/util/net/http_body.cc b/util/net/http_body.cc new file mode 100644 index 00000000..65d21ebe --- /dev/null +++ b/util/net/http_body.cc @@ -0,0 +1,116 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/net/http_body.h" + +#include +#include +#include + +#include +#include + +#include "base/logging.h" +#include "base/posix/eintr_wrapper.h" +#include "base/stl_util.h" +#include "util/file/fd_io.h" + +namespace crashpad { + +StringHTTPBodyStream::StringHTTPBodyStream(const std::string& string) + : HTTPBodyStream(), string_(string), bytes_read_() { +} + +StringHTTPBodyStream::~StringHTTPBodyStream() { +} + +ssize_t StringHTTPBodyStream::GetBytesBuffer(uint8_t* buffer, size_t max_len) { + size_t num_bytes_remaining = string_.length() - bytes_read_; + if (num_bytes_remaining == 0) { + return num_bytes_remaining; + } + + size_t num_bytes_returned = + std::min(std::min(num_bytes_remaining, max_len), + static_cast(std::numeric_limits::max())); + memcpy(buffer, &string_[bytes_read_], num_bytes_returned); + bytes_read_ += num_bytes_returned; + return num_bytes_returned; +} + +FileHTTPBodyStream::FileHTTPBodyStream(const base::FilePath& path) + : HTTPBodyStream(), path_(path), fd_(kUnopenedFile) { +} + +FileHTTPBodyStream::~FileHTTPBodyStream() { + if (fd_ >= 0) { + LoggingCloseFD(fd_); + } +} + +ssize_t FileHTTPBodyStream::GetBytesBuffer(uint8_t* buffer, size_t max_len) { + switch (fd_) { + case kUnopenedFile: + fd_ = HANDLE_EINTR(open(path_.value().c_str(), O_RDONLY)); + if (fd_ < 0) { + fd_ = kFileOpenError; + PLOG(ERROR) << "Cannot open " << path_.value(); + return -1; + } + break; + case kFileOpenError: + return -1; + case kClosedAtEOF: + return 0; + default: + break; + } + + ssize_t rv = ReadFD(fd_, buffer, max_len); + if (rv == 0) { + LoggingCloseFD(fd_); + fd_ = kClosedAtEOF; + } else if (rv < 0) { + PLOG(ERROR) << "read"; + } + return rv; +} + +CompositeHTTPBodyStream::CompositeHTTPBodyStream( + const CompositeHTTPBodyStream::PartsList& parts) + : HTTPBodyStream(), parts_(parts), current_part_(parts_.begin()) { +} + +CompositeHTTPBodyStream::~CompositeHTTPBodyStream() { + STLDeleteContainerPointers(parts_.begin(), parts_.end()); +} + +ssize_t CompositeHTTPBodyStream::GetBytesBuffer(uint8_t* buffer, + size_t max_len) { + if (current_part_ == parts_.end()) + return 0; + + ssize_t rv = (*current_part_)->GetBytesBuffer(buffer, max_len); + + if (rv == 0) { + // If the current part has returned 0 indicating EOF, advance the current + // part and call recursively to try again. + ++current_part_; + return GetBytesBuffer(buffer, max_len); + } + + return rv; +} + +} // namespace crashpad diff --git a/util/net/http_body.h b/util/net/http_body.h new file mode 100644 index 00000000..0347e636 --- /dev/null +++ b/util/net/http_body.h @@ -0,0 +1,130 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef CRASHPAD_UTIL_NET_HTTP_BODY_H_ +#define CRASHPAD_UTIL_NET_HTTP_BODY_H_ + +#include +#include + +#include +#include + +#include "base/basictypes.h" +#include "base/files/file_path.h" + +namespace crashpad { + +//! \brief An interface to a stream that can be used for an HTTP request body. +class HTTPBodyStream { + public: + virtual ~HTTPBodyStream() {} + + //! \brief Copies up to \a max_len bytes into the user-supplied buffer. + //! + //! \param[out] buffer A user-supplied buffer into which this method will copy + //! bytes from the stream. + //! \param[in] max_len The length (or size) of \a buffer. At most this many + //! bytes will be copied. + //! + //! \return On success, a positive number indicating the number of bytes + //! actually copied to \a buffer. On failure, a negative number. When + //! the stream has no more data, returns `0`. + virtual ssize_t GetBytesBuffer(uint8_t* buffer, size_t max_len) = 0; + + protected: + HTTPBodyStream() {} +}; + +//! \brief An implementation of HTTPBodyStream that turns a fixed string into +//! a stream. +class StringHTTPBodyStream : public HTTPBodyStream { + public: + //! \brief Creates a stream with the specified string. + //! + //! \param[in] string The string to turn into a stream. + explicit StringHTTPBodyStream(const std::string& string); + + ~StringHTTPBodyStream() override; + + // HTTPBodyStream: + ssize_t GetBytesBuffer(uint8_t* buffer, size_t max_len) override; + + private: + std::string string_; + size_t bytes_read_; + + DISALLOW_COPY_AND_ASSIGN(StringHTTPBodyStream); +}; + +//! \brief An implementation of HTTPBodyStream that reads from the specified +//! file and provides its contents for an HTTP body. +class FileHTTPBodyStream : public HTTPBodyStream { + public: + //! \brief Creates a stream for reading the file at the specified \a path. + //! + //! \param[in] path The file from which this HTTPBodyStream will read. + explicit FileHTTPBodyStream(const base::FilePath& path); + + ~FileHTTPBodyStream() override; + + // HTTPBodyStream: + ssize_t GetBytesBuffer(uint8_t* buffer, size_t max_len) override; + + private: + enum InvalidFD { + kUnopenedFile = -1, + kFileOpenError = -2, + kClosedAtEOF = -3, + }; + + base::FilePath path_; + + // If |fd_| is greater than or equal to zero, it is an opened descriptor + // from which an instance of this class is reading. If |fd_| is less than + // zero, the value corresponds to an InvalidFD value. + int fd_; + + DISALLOW_COPY_AND_ASSIGN(FileHTTPBodyStream); +}; + +//! \brief An implementation of HTTPBodyStream that combines an array of +//! several other HTTPBodyStream objects into a single, unified stream. +class CompositeHTTPBodyStream : public HTTPBodyStream { + public: + using PartsList = std::vector; + + //! \brief Creates a stream from an array of other stream parts. + //! + //! \param[in] parts A vector of HTTPBodyStream objects, of which this object + //! takes ownership, that will be represented as a single unified stream. + //! Callers should not mutate the stream objects after passing them to + //! an instance of this class. + explicit CompositeHTTPBodyStream(const PartsList& parts); + + ~CompositeHTTPBodyStream() override; + + // HTTPBodyStream: + ssize_t GetBytesBuffer(uint8_t* buffer, size_t max_len) override; + + private: + PartsList parts_; + PartsList::iterator current_part_; + + DISALLOW_COPY_AND_ASSIGN(CompositeHTTPBodyStream); +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_NET_HTTP_BODY_H_ diff --git a/util/net/http_body_test.cc b/util/net/http_body_test.cc new file mode 100644 index 00000000..c15fe4f0 --- /dev/null +++ b/util/net/http_body_test.cc @@ -0,0 +1,237 @@ +// Copyright 2014 The Crashpad Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "util/net/http_body.h" + +#include "base/memory/scoped_ptr.h" +#include "gtest/gtest.h" + +namespace crashpad { +namespace test { +namespace { + +void ExpectBufferSet(const uint8_t* actual, + uint8_t expected_byte, + size_t num_expected_bytes) { + for (size_t i = 0; i < num_expected_bytes; ++i) { + EXPECT_EQ(expected_byte, actual[i]) << i; + } +} + +TEST(StringHTTPBodyStream, EmptyString) { + uint8_t buf[32]; + memset(buf, '!', sizeof(buf)); + + std::string empty_string; + StringHTTPBodyStream stream(empty_string); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + ExpectBufferSet(buf, '!', sizeof(buf)); +} + +TEST(StringHTTPBodyStream, SmallString) { + uint8_t buf[32]; + memset(buf, '!', sizeof(buf)); + + std::string string("Hello, world"); + StringHTTPBodyStream stream(string); + EXPECT_EQ(static_cast(string.length()), + stream.GetBytesBuffer(buf, sizeof(buf))); + + std::string actual(reinterpret_cast(buf), string.length()); + EXPECT_EQ(string, actual); + ExpectBufferSet(buf + string.length(), '!', sizeof(buf) - string.length()); + + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); +} + +TEST(StringHTTPBodyStream, MultipleReads) { + uint8_t buf[2]; + memset(buf, '!', sizeof(buf)); + + { + std::string string("test"); + SCOPED_TRACE("aligned buffer boundary"); + + StringHTTPBodyStream stream(string); + EXPECT_EQ(2, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ('t', buf[0]); + EXPECT_EQ('e', buf[1]); + EXPECT_EQ(2, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ('s', buf[0]); + EXPECT_EQ('t', buf[1]); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ('s', buf[0]); + EXPECT_EQ('t', buf[1]); + } + + { + std::string string("abc"); + SCOPED_TRACE("unaligned buffer boundary"); + + StringHTTPBodyStream stream(string); + EXPECT_EQ(2, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ('a', buf[0]); + EXPECT_EQ('b', buf[1]); + EXPECT_EQ(1, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ('c', buf[0]); + EXPECT_EQ('b', buf[1]); // Unmodified from last read. + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ('c', buf[0]); + EXPECT_EQ('b', buf[1]); + } +} + +std::string ReadStreamToString(HTTPBodyStream* stream, size_t buffer_size) { + scoped_ptr buf(new uint8_t[buffer_size]); + std::string result; + + ssize_t bytes_read; + while ((bytes_read = stream->GetBytesBuffer(buf.get(), buffer_size)) != 0) { + if (bytes_read < 0) { + ADD_FAILURE() << "Failed to read from stream: " << bytes_read; + return std::string(); + } + + result.append(reinterpret_cast(buf.get()), bytes_read); + } + + return result; +} + +TEST(FileHTTPBodyStream, ReadASCIIFile) { + // TODO(rsesek): Use a more robust mechanism to locate testdata + // . + base::FilePath path = base::FilePath("util/net/testdata/ascii_http_body.txt"); + FileHTTPBodyStream stream(path); + std::string contents = ReadStreamToString(&stream, 32); + EXPECT_EQ("This is a test.\n", contents); + + // Make sure that the file is not read again after it has been read to + // completion. + uint8_t buf[8]; + memset(buf, '!', sizeof(buf)); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + ExpectBufferSet(buf, '!', sizeof(buf)); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + ExpectBufferSet(buf, '!', sizeof(buf)); +} + +TEST(FileHTTPBodyStream, ReadBinaryFile) { + // HEX contents of file: |FEEDFACE A11A15|. + // TODO(rsesek): Use a more robust mechanism to locate testdata + // . + base::FilePath path = + base::FilePath("util/net/testdata/binary_http_body.dat"); + // This buffer size was chosen so that reading the file takes multiple reads. + uint8_t buf[4]; + + FileHTTPBodyStream stream(path); + + memset(buf, '!', sizeof(buf)); + EXPECT_EQ(4, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ(0xfe, buf[0]); + EXPECT_EQ(0xed, buf[1]); + EXPECT_EQ(0xfa, buf[2]); + EXPECT_EQ(0xce, buf[3]); + + memset(buf, '!', sizeof(buf)); + EXPECT_EQ(3, stream.GetBytesBuffer(buf, sizeof(buf))); + EXPECT_EQ(0xa1, buf[0]); + EXPECT_EQ(0x1a, buf[1]); + EXPECT_EQ(0x15, buf[2]); + EXPECT_EQ('!', buf[3]); + + memset(buf, '!', sizeof(buf)); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + ExpectBufferSet(buf, '!', sizeof(buf)); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + ExpectBufferSet(buf, '!', sizeof(buf)); +} + +TEST(FileHTTPBodyStream, NonExistentFile) { + base::FilePath path = + base::FilePath("/var/empty/crashpad/util/net/http_body/null"); + FileHTTPBodyStream stream(path); + + uint8_t buf = 0xff; + EXPECT_LT(stream.GetBytesBuffer(&buf, 1), 0); + EXPECT_EQ(0xff, buf); + EXPECT_LT(stream.GetBytesBuffer(&buf, 1), 0); + EXPECT_EQ(0xff, buf); +} + +TEST(CompositeHTTPBodyStream, TwoEmptyStrings) { + std::vector parts; + parts.push_back(new StringHTTPBodyStream(std::string())); + parts.push_back(new StringHTTPBodyStream(std::string())); + + CompositeHTTPBodyStream stream(parts); + + uint8_t buf[5]; + memset(buf, '!', sizeof(buf)); + EXPECT_EQ(0, stream.GetBytesBuffer(buf, sizeof(buf))); + ExpectBufferSet(buf, '!', sizeof(buf)); +} + +class CompositeHTTPBodyStreamBufferSize + : public testing::TestWithParam { +}; + +TEST_P(CompositeHTTPBodyStreamBufferSize, ThreeStringParts) { + std::string string1("crashpad"); + std::string string2("test"); + std::string string3("foobar"); + const size_t all_strings_length = string1.length() + string2.length() + + string3.length(); + uint8_t buf[all_strings_length + 3]; + memset(buf, '!', sizeof(buf)); + + std::vector parts; + parts.push_back(new StringHTTPBodyStream(string1)); + parts.push_back(new StringHTTPBodyStream(string2)); + parts.push_back(new StringHTTPBodyStream(string3)); + + CompositeHTTPBodyStream stream(parts); + + std::string actual_string = ReadStreamToString(&stream, GetParam()); + EXPECT_EQ(string1 + string2 + string3, actual_string); + + ExpectBufferSet(buf + all_strings_length, '!', + sizeof(buf) - all_strings_length); +} + +TEST_P(CompositeHTTPBodyStreamBufferSize, StringsAndFile) { + std::string string1("Hello! "); + std::string string2(" Goodbye :)"); + + std::vector parts; + parts.push_back(new StringHTTPBodyStream(string1)); + parts.push_back(new FileHTTPBodyStream( + base::FilePath("util/net/testdata/ascii_http_body.txt"))); + parts.push_back(new StringHTTPBodyStream(string2)); + + CompositeHTTPBodyStream stream(parts); + + std::string expected_string = string1 + "This is a test.\n" + string2; + std::string actual_string = ReadStreamToString(&stream, GetParam()); + EXPECT_EQ(expected_string, actual_string); +} + +INSTANTIATE_TEST_CASE_P(VariableBufferSize, + CompositeHTTPBodyStreamBufferSize, + testing::Values(1, 2, 9, 16, 31, 128)); + +} // namespace +} // namespace test +} // namespace crashpad diff --git a/util/net/testdata/ascii_http_body.txt b/util/net/testdata/ascii_http_body.txt new file mode 100644 index 00000000..484ba93e --- /dev/null +++ b/util/net/testdata/ascii_http_body.txt @@ -0,0 +1 @@ +This is a test. diff --git a/util/net/testdata/binary_http_body.dat b/util/net/testdata/binary_http_body.dat new file mode 100644 index 00000000..d88b81a2 --- /dev/null +++ b/util/net/testdata/binary_http_body.dat @@ -0,0 +1 @@ +þíúΡ \ No newline at end of file diff --git a/util/util.gyp b/util/util.gyp index f6c5dcff..255f2568 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -69,6 +69,8 @@ 'misc/symbolic_constants_common.h', 'misc/uuid.cc', 'misc/uuid.h', + 'net/http_body.cc', + 'net/http_body.h', 'numeric/checked_range.h', 'numeric/in_range_cast.h', 'numeric/int128.h', @@ -212,6 +214,7 @@ 'misc/initialization_state_test.cc', 'misc/scoped_forbid_return_test.cc', 'misc/uuid_test.cc', + 'net/http_body_test.cc', 'numeric/checked_range_test.cc', 'numeric/in_range_cast_test.cc', 'numeric/int128_test.cc',