|  | // 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 |