|  | // Copyright 2019 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 "content/browser/web_package/web_bundle_reader.h" | 
|  |  | 
|  | #include <limits> | 
|  |  | 
|  | #include "base/check_op.h" | 
|  | #include "base/numerics/safe_math.h" | 
|  | #include "base/task/task_traits.h" | 
|  | #include "base/task/thread_pool.h" | 
|  | #include "content/browser/web_package/web_bundle_blob_data_source.h" | 
|  | #include "content/browser/web_package/web_bundle_source.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  | #include "mojo/public/cpp/bindings/pending_remote.h" | 
|  | #include "mojo/public/cpp/system/data_pipe_producer.h" | 
|  | #include "mojo/public/cpp/system/file_data_source.h" | 
|  | #include "mojo/public/cpp/system/platform_handle.h" | 
|  | #include "net/base/url_util.h" | 
|  | #include "third_party/blink/public/common/web_package/web_package_request_matcher.h" | 
|  |  | 
|  | namespace content { | 
|  |  | 
|  | WebBundleReader::SharedFile::SharedFile( | 
|  | std::unique_ptr<WebBundleSource> source) { | 
|  | base::ThreadPool::PostTaskAndReplyWithResult( | 
|  | FROM_HERE, {base::MayBlock()}, | 
|  | base::BindOnce( | 
|  | [](std::unique_ptr<WebBundleSource> source) | 
|  | -> std::unique_ptr<base::File> { return source->OpenFile(); }, | 
|  | std::move(source)), | 
|  | base::BindOnce(&SharedFile::SetFile, base::RetainedRef(this))); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::SharedFile::DuplicateFile( | 
|  | base::OnceCallback<void(base::File)> callback) { | 
|  | // Basically this interface expects this method is called at most once. Have | 
|  | // a DCHECK for the case that does not work for a clear reason, just in case. | 
|  | // The call site also have another DCHECK for external callers not to cause | 
|  | // such problematic cases. | 
|  | DCHECK(duplicate_callback_.is_null()); | 
|  | duplicate_callback_ = std::move(callback); | 
|  |  | 
|  | if (file_) | 
|  | SetFile(std::move(file_)); | 
|  | } | 
|  |  | 
|  | base::File* WebBundleReader::SharedFile::operator->() { | 
|  | DCHECK(file_); | 
|  | return file_.get(); | 
|  | } | 
|  |  | 
|  | WebBundleReader::SharedFile::~SharedFile() { | 
|  | // Move the last reference to |file_| that leads an internal blocking call | 
|  | // that is not permitted here. | 
|  | base::ThreadPool::PostTask( | 
|  | FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()}, | 
|  | base::BindOnce([](std::unique_ptr<base::File> file) {}, | 
|  | std::move(file_))); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::SharedFile::SetFile(std::unique_ptr<base::File> file) { | 
|  | file_ = std::move(file); | 
|  |  | 
|  | if (duplicate_callback_.is_null()) | 
|  | return; | 
|  |  | 
|  | base::ThreadPool::PostTaskAndReplyWithResult( | 
|  | FROM_HERE, {base::MayBlock()}, | 
|  | base::BindOnce( | 
|  | [](base::File* file) -> base::File { return file->Duplicate(); }, | 
|  | file_.get()), | 
|  | std::move(duplicate_callback_)); | 
|  | } | 
|  |  | 
|  | class WebBundleReader::SharedFileDataSource final | 
|  | : public mojo::DataPipeProducer::DataSource { | 
|  | public: | 
|  | SharedFileDataSource(scoped_refptr<WebBundleReader::SharedFile> file, | 
|  | uint64_t offset, | 
|  | uint64_t length) | 
|  | : file_(std::move(file)), offset_(offset), length_(length) { | 
|  | error_ = mojo::FileDataSource::ConvertFileErrorToMojoResult( | 
|  | (*file_)->error_details()); | 
|  |  | 
|  | // base::File::Read takes int64_t as an offset. So, offset + length should | 
|  | // not overflow in int64_t. | 
|  | uint64_t max_offset; | 
|  | if (!base::CheckAdd(offset, length).AssignIfValid(&max_offset) || | 
|  | (std::numeric_limits<int64_t>::max() < max_offset)) { | 
|  | error_ = MOJO_RESULT_INVALID_ARGUMENT; | 
|  | } | 
|  | } | 
|  |  | 
|  | private: | 
|  | // Implements mojo::DataPipeProducer::DataSource. Following methods are called | 
|  | // on a blockable sequenced task runner. | 
|  | uint64_t GetLength() const override { return length_; } | 
|  | ReadResult Read(uint64_t offset, base::span<char> buffer) override { | 
|  | ReadResult result; | 
|  | result.result = error_; | 
|  |  | 
|  | if (length_ < offset) | 
|  | result.result = MOJO_RESULT_INVALID_ARGUMENT; | 
|  |  | 
|  | if (result.result != MOJO_RESULT_OK) | 
|  | return result; | 
|  |  | 
|  | uint64_t readable_size = length_ - offset; | 
|  | uint64_t writable_size = buffer.size(); | 
|  | uint64_t copyable_size = | 
|  | std::min(std::min(readable_size, writable_size), | 
|  | static_cast<uint64_t>(std::numeric_limits<int>::max())); | 
|  |  | 
|  | int bytes_read = | 
|  | (*file_)->Read(offset_ + offset, buffer.data(), copyable_size); | 
|  | if (bytes_read < 0) { | 
|  | result.result = mojo::FileDataSource::ConvertFileErrorToMojoResult( | 
|  | (*file_)->GetLastFileError()); | 
|  | } else { | 
|  | result.bytes_read = bytes_read; | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | scoped_refptr<WebBundleReader::SharedFile> file_; | 
|  | MojoResult error_; | 
|  | const uint64_t offset_; | 
|  | const uint64_t length_; | 
|  |  | 
|  | DISALLOW_COPY_AND_ASSIGN(SharedFileDataSource); | 
|  | }; | 
|  |  | 
|  | WebBundleReader::WebBundleReader(std::unique_ptr<WebBundleSource> source) | 
|  | : source_(std::move(source)), | 
|  | parser_(std::make_unique<data_decoder::SafeWebBundleParser>()), | 
|  | file_(base::MakeRefCounted<SharedFile>(source_->Clone())) { | 
|  | DCHECK(source_->is_trusted_file() || source_->is_file()); | 
|  | } | 
|  |  | 
|  | WebBundleReader::WebBundleReader( | 
|  | std::unique_ptr<WebBundleSource> source, | 
|  | int64_t content_length, | 
|  | mojo::ScopedDataPipeConsumerHandle outer_response_body, | 
|  | network::mojom::URLLoaderClientEndpointsPtr endpoints, | 
|  | BrowserContext::BlobContextGetter blob_context_getter) | 
|  | : source_(std::move(source)), | 
|  | parser_(std::make_unique<data_decoder::SafeWebBundleParser>()) { | 
|  | DCHECK(source_->is_network()); | 
|  | mojo::PendingRemote<web_package::mojom::BundleDataSource> pending_remote; | 
|  | blob_data_source_ = std::make_unique<WebBundleBlobDataSource>( | 
|  | content_length, std::move(outer_response_body), std::move(endpoints), | 
|  | std::move(blob_context_getter)); | 
|  | blob_data_source_->AddReceiver( | 
|  | pending_remote.InitWithNewPipeAndPassReceiver()); | 
|  | parser_->OpenDataSource(std::move(pending_remote)); | 
|  | } | 
|  |  | 
|  | WebBundleReader::~WebBundleReader() { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::ReadMetadata(MetadataCallback callback) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_EQ(state_, State::kInitial); | 
|  |  | 
|  | if (!blob_data_source_) { | 
|  | DCHECK(source_->is_trusted_file() || source_->is_file()); | 
|  | file_->DuplicateFile(base::BindOnce(&WebBundleReader::ReadMetadataInternal, | 
|  | this, std::move(callback))); | 
|  | return; | 
|  | } | 
|  | DCHECK(source_->is_network()); | 
|  | parser_->ParseMetadata(base::BindOnce(&WebBundleReader::OnMetadataParsed, | 
|  | base::Unretained(this), | 
|  | std::move(callback))); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::ReadResponse( | 
|  | const network::ResourceRequest& resource_request, | 
|  | const std::string& accept_langs, | 
|  | ResponseCallback callback) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_NE(state_, State::kInitial); | 
|  |  | 
|  | auto it = entries_.find(net::SimplifyUrlForRequest(resource_request.url)); | 
|  | if (it == entries_.end() || it->second->response_locations.empty()) { | 
|  | base::ThreadPool::PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce( | 
|  | std::move(callback), nullptr, | 
|  | web_package::mojom::BundleResponseParseError::New( | 
|  | web_package::mojom::BundleParseErrorType::kParserInternalError, | 
|  | "Not found in Web Bundle file."))); | 
|  | return; | 
|  | } | 
|  | const web_package::mojom::BundleIndexValuePtr& entry = it->second; | 
|  |  | 
|  | size_t response_index = 0; | 
|  | if (!entry->variants_value.empty()) { | 
|  | // Select the best variant for the request. | 
|  | blink::WebPackageRequestMatcher matcher(resource_request.headers, | 
|  | accept_langs); | 
|  | auto found = matcher.FindBestMatchingIndex(entry->variants_value); | 
|  | if (!found || *found >= entry->response_locations.size()) { | 
|  | base::ThreadPool::PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce( | 
|  | std::move(callback), nullptr, | 
|  | web_package::mojom::BundleResponseParseError::New( | 
|  | web_package::mojom::BundleParseErrorType:: | 
|  | kParserInternalError, | 
|  | "Cannot find a response that matches request headers."))); | 
|  | return; | 
|  | } | 
|  | response_index = *found; | 
|  | } | 
|  | auto response_location = entry->response_locations[response_index].Clone(); | 
|  |  | 
|  | if (state_ == State::kDisconnected) { | 
|  | // Try reconnecting, if not attempted yet. | 
|  | if (pending_read_responses_.empty()) | 
|  | Reconnect(); | 
|  | pending_read_responses_.emplace_back(std::move(response_location), | 
|  | std::move(callback)); | 
|  | return; | 
|  | } | 
|  |  | 
|  | ReadResponseInternal(std::move(response_location), std::move(callback)); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::ReadResponseInternal( | 
|  | web_package::mojom::BundleResponseLocationPtr location, | 
|  | ResponseCallback callback) { | 
|  | parser_->ParseResponse( | 
|  | location->offset, location->length, | 
|  | base::BindOnce(&WebBundleReader::OnResponseParsed, base::Unretained(this), | 
|  | std::move(callback))); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::Reconnect() { | 
|  | DCHECK(!parser_); | 
|  | parser_ = std::make_unique<data_decoder::SafeWebBundleParser>(); | 
|  |  | 
|  | if (!blob_data_source_) { | 
|  | DCHECK(source_->is_trusted_file() || source_->is_file()); | 
|  | file_->DuplicateFile( | 
|  | base::BindOnce(&WebBundleReader::ReconnectForFile, this)); | 
|  | return; | 
|  | } | 
|  | DCHECK(source_->is_network()); | 
|  | mojo::PendingRemote<web_package::mojom::BundleDataSource> pending_remote; | 
|  | blob_data_source_->AddReceiver( | 
|  | pending_remote.InitWithNewPipeAndPassReceiver()); | 
|  | parser_->OpenDataSource(std::move(pending_remote)); | 
|  |  | 
|  | GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, base::BindOnce(&WebBundleReader::DidReconnect, this, | 
|  | base::nullopt /* error */)); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::ReconnectForFile(base::File file) { | 
|  | base::File::Error file_error = parser_->OpenFile(std::move(file)); | 
|  | base::Optional<std::string> error; | 
|  | if (file_error != base::File::FILE_OK) | 
|  | error = base::File::ErrorToString(file_error); | 
|  | GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&WebBundleReader::DidReconnect, this, std::move(error))); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::DidReconnect(base::Optional<std::string> error) { | 
|  | DCHECK_EQ(state_, State::kDisconnected); | 
|  | DCHECK(parser_); | 
|  | auto read_tasks = std::move(pending_read_responses_); | 
|  |  | 
|  | if (error) { | 
|  | for (auto& pair : read_tasks) { | 
|  | base::ThreadPool::PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(std::move(pair.second), nullptr, | 
|  | web_package::mojom::BundleResponseParseError::New( | 
|  | web_package::mojom::BundleParseErrorType:: | 
|  | kParserInternalError, | 
|  | *error))); | 
|  | } | 
|  | return; | 
|  | } | 
|  |  | 
|  | state_ = State::kMetadataReady; | 
|  | parser_->SetDisconnectCallback(base::BindOnce( | 
|  | &WebBundleReader::OnParserDisconnected, base::Unretained(this))); | 
|  | for (auto& pair : read_tasks) | 
|  | ReadResponseInternal(std::move(pair.first), std::move(pair.second)); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::ReadResponseBody( | 
|  | web_package::mojom::BundleResponsePtr response, | 
|  | mojo::ScopedDataPipeProducerHandle producer_handle, | 
|  | BodyCompletionCallback callback) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_NE(state_, State::kInitial); | 
|  |  | 
|  | if (!blob_data_source_) { | 
|  | DCHECK(source_->is_trusted_file() || source_->is_file()); | 
|  | auto data_producer = | 
|  | std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle)); | 
|  | mojo::DataPipeProducer* raw_producer = data_producer.get(); | 
|  | raw_producer->Write( | 
|  | std::make_unique<SharedFileDataSource>(file_, response->payload_offset, | 
|  | response->payload_length), | 
|  | base::BindOnce( | 
|  | [](std::unique_ptr<mojo::DataPipeProducer> producer, | 
|  | BodyCompletionCallback callback, MojoResult result) { | 
|  | std::move(callback).Run( | 
|  | result == MOJO_RESULT_OK ? net::OK : net::ERR_UNEXPECTED); | 
|  | }, | 
|  | std::move(data_producer), std::move(callback))); | 
|  | return; | 
|  | } | 
|  |  | 
|  | DCHECK(source_->is_network()); | 
|  | blob_data_source_->ReadToDataPipe( | 
|  | response->payload_offset, response->payload_length, | 
|  | std::move(producer_handle), std::move(callback)); | 
|  | } | 
|  |  | 
|  | bool WebBundleReader::HasEntry(const GURL& url) const { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_NE(state_, State::kInitial); | 
|  |  | 
|  | return entries_.contains(net::SimplifyUrlForRequest(url)); | 
|  | } | 
|  |  | 
|  | const GURL& WebBundleReader::GetPrimaryURL() const { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_NE(state_, State::kInitial); | 
|  |  | 
|  | return primary_url_; | 
|  | } | 
|  |  | 
|  | const WebBundleSource& WebBundleReader::source() const { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | return *source_; | 
|  | } | 
|  |  | 
|  | void WebBundleReader::ReadMetadataInternal(MetadataCallback callback, | 
|  | base::File file) { | 
|  | DCHECK(source_->is_trusted_file() || source_->is_file()); | 
|  | base::File::Error error = parser_->OpenFile(std::move(file)); | 
|  | if (base::File::FILE_OK != error) { | 
|  | GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce( | 
|  | std::move(callback), | 
|  | web_package::mojom::BundleMetadataParseError::New( | 
|  | web_package::mojom::BundleParseErrorType::kParserInternalError, | 
|  | GURL() /* fallback_url */, base::File::ErrorToString(error)))); | 
|  | } else { | 
|  | parser_->ParseMetadata(base::BindOnce(&WebBundleReader::OnMetadataParsed, | 
|  | base::Unretained(this), | 
|  | std::move(callback))); | 
|  | } | 
|  | } | 
|  |  | 
|  | void WebBundleReader::OnMetadataParsed( | 
|  | MetadataCallback callback, | 
|  | web_package::mojom::BundleMetadataPtr metadata, | 
|  | web_package::mojom::BundleMetadataParseErrorPtr error) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_EQ(state_, State::kInitial); | 
|  |  | 
|  | state_ = State::kMetadataReady; | 
|  | parser_->SetDisconnectCallback(base::BindOnce( | 
|  | &WebBundleReader::OnParserDisconnected, base::Unretained(this))); | 
|  |  | 
|  | if (metadata) { | 
|  | primary_url_ = metadata->primary_url; | 
|  | entries_ = std::move(metadata->requests); | 
|  | } | 
|  | std::move(callback).Run(std::move(error)); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::OnResponseParsed( | 
|  | ResponseCallback callback, | 
|  | web_package::mojom::BundleResponsePtr response, | 
|  | web_package::mojom::BundleResponseParseErrorPtr error) { | 
|  | DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); | 
|  | DCHECK_NE(state_, State::kInitial); | 
|  |  | 
|  | std::move(callback).Run(std::move(response), std::move(error)); | 
|  | } | 
|  |  | 
|  | void WebBundleReader::OnParserDisconnected() { | 
|  | DCHECK_EQ(state_, State::kMetadataReady); | 
|  |  | 
|  | state_ = State::kDisconnected; | 
|  | parser_ = nullptr; | 
|  | // Reconnection will be attempted on next ReadResponse() call. | 
|  | } | 
|  |  | 
|  | }  // namespace content |