| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/url_request/url_request_file_job.h" |
| |
| #include <memory> |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/logging.h" |
| #include "base/run_loop.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/net_errors.h" |
| #include "net/test/test_with_scoped_task_environment.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "net/url_request/url_request.h" |
| #include "net/url_request/url_request_test_util.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| // A URLRequestFileJob for testing values passed to OnSeekComplete and |
| // OnReadComplete. |
| class TestURLRequestFileJob : public URLRequestFileJob { |
| public: |
| // |seek_position| will be set to the value passed in to OnSeekComplete. |
| // |observed_content| will be set to the concatenated data from all calls to |
| // OnReadComplete. |
| TestURLRequestFileJob(URLRequest* request, |
| NetworkDelegate* network_delegate, |
| const base::FilePath& file_path, |
| const scoped_refptr<base::TaskRunner>& file_task_runner, |
| int* open_result, |
| int64_t* seek_position, |
| bool* done_reading, |
| std::string* observed_content) |
| : URLRequestFileJob(request, |
| network_delegate, |
| file_path, |
| file_task_runner), |
| open_result_(open_result), |
| seek_position_(seek_position), |
| done_reading_(done_reading), |
| observed_content_(observed_content) { |
| *open_result_ = ERR_IO_PENDING; |
| *seek_position_ = ERR_IO_PENDING; |
| *done_reading_ = false; |
| observed_content_->clear(); |
| } |
| |
| ~TestURLRequestFileJob() override = default; |
| |
| protected: |
| void OnOpenComplete(int result) override { |
| // Should only be called once. |
| ASSERT_EQ(ERR_IO_PENDING, *open_result_); |
| *open_result_ = result; |
| } |
| |
| void OnSeekComplete(int64_t result) override { |
| // Should only call this if open succeeded. |
| EXPECT_EQ(OK, *open_result_); |
| // Should only be called once. |
| ASSERT_EQ(ERR_IO_PENDING, *seek_position_); |
| *seek_position_ = result; |
| } |
| |
| void OnReadComplete(IOBuffer* buf, int result) override { |
| // Should only call this if seek succeeded. |
| EXPECT_GE(*seek_position_, 0); |
| observed_content_->append(std::string(buf->data(), result)); |
| } |
| |
| void DoneReading() override { *done_reading_ = true; } |
| |
| int* const open_result_; |
| int64_t* const seek_position_; |
| bool* done_reading_; |
| std::string* const observed_content_; |
| }; |
| |
| // A URLRequestJobFactory that will return TestURLRequestFileJob instances for |
| // file:// scheme URLs. Can only be used to create a single job. |
| class TestJobFactory : public URLRequestJobFactory { |
| public: |
| TestJobFactory(const base::FilePath& path, |
| int* open_result, |
| int64_t* seek_position, |
| bool* done_reading, |
| std::string* observed_content) |
| : path_(path), |
| open_result_(open_result), |
| seek_position_(seek_position), |
| done_reading_(done_reading), |
| observed_content_(observed_content) { |
| CHECK(open_result_); |
| CHECK(seek_position_); |
| CHECK(done_reading_); |
| CHECK(observed_content_); |
| } |
| |
| ~TestJobFactory() override = default; |
| |
| URLRequestJob* MaybeCreateJobWithProtocolHandler( |
| const std::string& scheme, |
| URLRequest* request, |
| NetworkDelegate* network_delegate) const override { |
| CHECK(open_result_); |
| CHECK(seek_position_); |
| CHECK(done_reading_); |
| CHECK(observed_content_); |
| URLRequestJob* job = new TestURLRequestFileJob( |
| request, network_delegate, path_, base::ThreadTaskRunnerHandle::Get(), |
| open_result_, seek_position_, done_reading_, observed_content_); |
| open_result_ = nullptr; |
| seek_position_ = nullptr; |
| done_reading_ = nullptr; |
| observed_content_ = nullptr; |
| return job; |
| } |
| |
| URLRequestJob* MaybeInterceptRedirect(URLRequest* request, |
| NetworkDelegate* network_delegate, |
| const GURL& location) const override { |
| return nullptr; |
| } |
| |
| URLRequestJob* MaybeInterceptResponse( |
| URLRequest* request, |
| NetworkDelegate* network_delegate) const override { |
| return nullptr; |
| } |
| |
| bool IsHandledProtocol(const std::string& scheme) const override { |
| return scheme == "file"; |
| } |
| |
| bool IsSafeRedirectTarget(const GURL& location) const override { |
| return false; |
| } |
| |
| private: |
| const base::FilePath path_; |
| |
| // These are mutable because MaybeCreateJobWithProtocolHandler is const. |
| mutable int* open_result_; |
| mutable int64_t* seek_position_; |
| mutable bool* done_reading_; |
| mutable std::string* observed_content_; |
| }; |
| |
| // Helper function to create a file at |path| filled with |content|. |
| // Returns true on success. |
| bool CreateFileWithContent(const std::string& content, |
| const base::FilePath& path) { |
| return base::WriteFile(path, content.c_str(), content.length()) != -1; |
| } |
| |
| // A simple holder for start/end used in http range requests. |
| struct Range { |
| int start; |
| int end; |
| |
| Range() { |
| start = 0; |
| end = 0; |
| } |
| |
| Range(int start, int end) { |
| this->start = start; |
| this->end = end; |
| } |
| }; |
| |
| // A superclass for tests of the OnReadComplete / OnSeekComplete / |
| // OnReadComplete functions of URLRequestFileJob. |
| class URLRequestFileJobEventsTest : public TestWithScopedTaskEnvironment { |
| public: |
| URLRequestFileJobEventsTest(); |
| |
| protected: |
| void TearDown() override; |
| |
| // This creates a file with |content| as the contents, and then creates and |
| // runs a TestURLRequestFileJob job to get the contents out of it, |
| // and makes sure that the callbacks observed the correct bytes. If a Range |
| // is provided, this function will add the appropriate Range http header to |
| // the request and verify that only the bytes in that range (inclusive) were |
| // observed. |
| void RunSuccessfulRequestWithString(const std::string& content, |
| const Range* range); |
| |
| // This is the same as the method above it, except that it will make sure |
| // the content matches |expected_content| and allow caller to specify the |
| // extension of the filename in |file_extension|. |
| void RunSuccessfulRequestWithString( |
| const std::string& content, |
| const std::string& expected_content, |
| const base::FilePath::StringPieceType& file_extension, |
| const Range* range); |
| |
| // Creates and runs a TestURLRequestFileJob job to read from file provided by |
| // |path|. If |range| value is provided, it will be passed in the range |
| // header. |
| void RunRequestWithPath(const base::FilePath& path, |
| const std::string& range, |
| int* open_result, |
| int64_t* seek_position, |
| bool* done_reading, |
| std::string* observed_content); |
| |
| TestURLRequestContext context_; |
| TestDelegate delegate_; |
| }; |
| |
| URLRequestFileJobEventsTest::URLRequestFileJobEventsTest() = default; |
| |
| void URLRequestFileJobEventsTest::TearDown() { |
| // Gives a chance to close the opening file. |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void URLRequestFileJobEventsTest::RunSuccessfulRequestWithString( |
| const std::string& content, |
| const Range* range) { |
| RunSuccessfulRequestWithString(content, content, FILE_PATH_LITERAL(""), |
| range); |
| } |
| |
| void URLRequestFileJobEventsTest::RunSuccessfulRequestWithString( |
| const std::string& raw_content, |
| const std::string& expected_content, |
| const base::FilePath::StringPieceType& file_extension, |
| const Range* range) { |
| base::ScopedTempDir directory; |
| ASSERT_TRUE(directory.CreateUniqueTempDir()); |
| base::FilePath path = directory.GetPath().Append(FILE_PATH_LITERAL("test")); |
| if (!file_extension.empty()) |
| path = path.AddExtension(file_extension); |
| ASSERT_TRUE(CreateFileWithContent(raw_content, path)); |
| |
| std::string range_value; |
| if (range) { |
| ASSERT_GE(range->start, 0); |
| ASSERT_GE(range->end, 0); |
| ASSERT_LE(range->start, range->end); |
| ASSERT_LT(static_cast<unsigned int>(range->end), expected_content.length()); |
| range_value = base::StringPrintf("bytes=%d-%d", range->start, range->end); |
| } |
| |
| { |
| int open_result; |
| int64_t seek_position; |
| bool done_reading; |
| std::string observed_content; |
| RunRequestWithPath(path, range_value, &open_result, &seek_position, |
| &done_reading, &observed_content); |
| |
| EXPECT_EQ(OK, open_result); |
| EXPECT_FALSE(delegate_.request_failed()); |
| int expected_length = |
| range ? (range->end - range->start + 1) : expected_content.length(); |
| EXPECT_EQ(delegate_.bytes_received(), expected_length); |
| |
| std::string expected_data_received; |
| if (range) { |
| expected_data_received.insert(0, expected_content, range->start, |
| expected_length); |
| EXPECT_EQ(expected_data_received, observed_content); |
| } else { |
| expected_data_received = expected_content; |
| EXPECT_EQ(raw_content, observed_content); |
| } |
| |
| EXPECT_EQ(expected_data_received, delegate_.data_received()); |
| EXPECT_EQ(seek_position, range ? range->start : 0); |
| EXPECT_TRUE(done_reading); |
| } |
| } |
| |
| void URLRequestFileJobEventsTest::RunRequestWithPath( |
| const base::FilePath& path, |
| const std::string& range, |
| int* open_result, |
| int64_t* seek_position, |
| bool* done_reading, |
| std::string* observed_content) { |
| TestJobFactory factory(path, open_result, seek_position, done_reading, |
| observed_content); |
| context_.set_job_factory(&factory); |
| |
| std::unique_ptr<URLRequest> request( |
| context_.CreateRequest(FilePathToFileURL(path), DEFAULT_PRIORITY, |
| &delegate_, TRAFFIC_ANNOTATION_FOR_TESTS)); |
| if (!range.empty()) { |
| request->SetExtraRequestHeaderByName(HttpRequestHeaders::kRange, range, |
| true /*overwrite*/); |
| } |
| request->Start(); |
| |
| base::RunLoop().Run(); |
| } |
| |
| // Helper function to make a character array filled with |size| bytes of |
| // test content. |
| std::string MakeContentOfSize(int size) { |
| EXPECT_GE(size, 0); |
| std::string result; |
| result.reserve(size); |
| for (int i = 0; i < size; i++) { |
| result.append(1, static_cast<char>(i % 256)); |
| } |
| return result; |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, ZeroByteFile) { |
| RunSuccessfulRequestWithString(std::string(""), nullptr); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, TinyFile) { |
| RunSuccessfulRequestWithString(std::string("hello world"), NULL); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, SmallFile) { |
| RunSuccessfulRequestWithString(MakeContentOfSize(17 * 1024), NULL); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, BigFile) { |
| RunSuccessfulRequestWithString(MakeContentOfSize(3 * 1024 * 1024), NULL); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, Range) { |
| // Use a 15KB content file and read a range chosen somewhat arbitrarily but |
| // not aligned on any likely page boundaries. |
| int size = 15 * 1024; |
| Range range(1701, (6 * 1024) + 3); |
| RunSuccessfulRequestWithString(MakeContentOfSize(size), &range); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, DecodeSvgzFile) { |
| std::string expected_content("Hello, World!"); |
| unsigned char gzip_data[] = { |
| // From: |
| // echo -n 'Hello, World!' | gzip | xxd -i | sed -e 's/^/ /' |
| 0x1f, 0x8b, 0x08, 0x00, 0x2b, 0x02, 0x84, 0x55, 0x00, 0x03, 0xf3, |
| 0x48, 0xcd, 0xc9, 0xc9, 0xd7, 0x51, 0x08, 0xcf, 0x2f, 0xca, 0x49, |
| 0x51, 0x04, 0x00, 0xd0, 0xc3, 0x4a, 0xec, 0x0d, 0x00, 0x00, 0x00}; |
| RunSuccessfulRequestWithString( |
| std::string(reinterpret_cast<char*>(gzip_data), sizeof(gzip_data)), |
| expected_content, FILE_PATH_LITERAL("svgz"), nullptr); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, OpenNonExistentFile) { |
| base::FilePath path; |
| base::PathService::Get(base::DIR_TEST_DATA, &path); |
| path = path.Append( |
| FILE_PATH_LITERAL("net/data/url_request_unittest/non-existent.txt")); |
| |
| int open_result; |
| int64_t seek_position; |
| bool done_reading; |
| std::string observed_content; |
| RunRequestWithPath(path, std::string(), &open_result, &seek_position, |
| &done_reading, &observed_content); |
| |
| EXPECT_EQ(ERR_FILE_NOT_FOUND, open_result); |
| EXPECT_FALSE(done_reading); |
| EXPECT_TRUE(delegate_.request_failed()); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, MultiRangeRequestNotSupported) { |
| base::FilePath path; |
| base::PathService::Get(base::DIR_TEST_DATA, &path); |
| path = path.Append( |
| FILE_PATH_LITERAL("net/data/url_request_unittest/BullRunSpeech.txt")); |
| |
| int open_result; |
| int64_t seek_position; |
| bool done_reading; |
| std::string observed_content; |
| RunRequestWithPath(path, "bytes=1-5,20-30", &open_result, &seek_position, |
| &done_reading, &observed_content); |
| |
| EXPECT_EQ(OK, open_result); |
| EXPECT_EQ(ERR_REQUEST_RANGE_NOT_SATISFIABLE, seek_position); |
| EXPECT_FALSE(done_reading); |
| EXPECT_TRUE(delegate_.request_failed()); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, RangeExceedingFileSize) { |
| base::FilePath path; |
| base::PathService::Get(base::DIR_TEST_DATA, &path); |
| path = path.Append( |
| FILE_PATH_LITERAL("net/data/url_request_unittest/BullRunSpeech.txt")); |
| |
| int open_result; |
| int64_t seek_position; |
| bool done_reading; |
| std::string observed_content; |
| RunRequestWithPath(path, "bytes=50000-", &open_result, &seek_position, |
| &done_reading, &observed_content); |
| |
| EXPECT_EQ(OK, open_result); |
| EXPECT_EQ(ERR_REQUEST_RANGE_NOT_SATISFIABLE, seek_position); |
| EXPECT_FALSE(done_reading); |
| EXPECT_TRUE(delegate_.request_failed()); |
| } |
| |
| TEST_F(URLRequestFileJobEventsTest, IgnoreRangeParsingError) { |
| base::FilePath path; |
| base::PathService::Get(base::DIR_TEST_DATA, &path); |
| path = path.Append( |
| FILE_PATH_LITERAL("net/data/url_request_unittest/simple.html")); |
| |
| int open_result; |
| int64_t seek_position; |
| bool done_reading; |
| std::string observed_content; |
| RunRequestWithPath(path, "bytes=3-z", &open_result, &seek_position, |
| &done_reading, &observed_content); |
| |
| EXPECT_EQ(OK, open_result); |
| EXPECT_EQ(0, seek_position); |
| EXPECT_EQ("hello\n", observed_content); |
| EXPECT_TRUE(done_reading); |
| EXPECT_FALSE(delegate_.request_failed()); |
| } |
| |
| } // namespace |
| |
| } // namespace net |