| // Copyright 2020 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/native_io/native_io_host.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/sequenced_task_runner.h" |
| #include "base/task/post_task.h" |
| #include "base/task/task_traits.h" |
| #include "base/task/thread_pool.h" |
| #include "base/task_runner.h" |
| #include "content/browser/native_io/native_io_context.h" |
| #include "content/browser/native_io/native_io_file_host.h" |
| #include "mojo/public/cpp/bindings/message.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "third_party/blink/public/mojom/native_io/native_io.mojom.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| bool IsValidNativeIONameCharacter(char name_char) { |
| return base::IsAsciiLower(name_char) || base::IsAsciiDigit(name_char) || |
| name_char == '_'; |
| } |
| |
| bool IsValidNativeIOName(const std::string& name) { |
| if (name.empty()) |
| return false; |
| |
| return std::all_of(name.begin(), name.end(), &IsValidNativeIONameCharacter); |
| } |
| |
| base::FilePath GetNativeIOFilePath(const base::FilePath& root_path, |
| const std::string& name) { |
| DCHECK(IsValidNativeIOName(name)); |
| |
| // This simple implementation assumes that the name doesn't have any special |
| // meaning to the host operating system. |
| base::FilePath file_path = root_path.AppendASCII(name); |
| DCHECK(root_path.IsParent(file_path)); |
| return file_path; |
| } |
| |
| // Creates a task runner suitable for running file I/O tasks. |
| scoped_refptr<base::TaskRunner> CreateFileTaskRunner() { |
| // We use a SequencedTaskRunner so that there is a global ordering to an |
| // origin's directory operations. |
| return base::ThreadPool::CreateSequencedTaskRunner({ |
| // Needed for file I/O. |
| base::MayBlock(), |
| |
| // Reasonable compromise, given that a few database operations are |
| // blocking, while most operations are not. We should be able to do better |
| // when we get scheduling APIs on the Web Platform. |
| base::TaskPriority::USER_VISIBLE, |
| |
| // BLOCK_SHUTDOWN is definitely not appropriate. We might be able to move |
| // to CONTINUE_ON_SHUTDOWN after very careful analysis. |
| base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN, |
| }); |
| } |
| |
| // Performs the file I/O work in OpenFile(). |
| base::File DoOpenFile(const base::FilePath& root_path, |
| const std::string& name) { |
| DCHECK(IsValidNativeIOName(name)); |
| |
| // Lazily create the origin's directory. |
| base::File::Error error; |
| if (!base::CreateDirectoryAndGetError(root_path, &error)) |
| return base::File(); |
| |
| return base::File(GetNativeIOFilePath(root_path, name), |
| base::File::FLAG_OPEN_ALWAYS | base::File::FLAG_READ | |
| base::File::FLAG_WRITE | base::File::FLAG_SHARE_DELETE); |
| } |
| |
| // Performs the file I/O work in DeleteFile(). |
| bool DoDeleteFile(const base::FilePath& root_path, const std::string& name) { |
| DCHECK(IsValidNativeIOName(name)); |
| |
| // If the origin's directory wasn't created yet, there's nothing to delete. |
| if (!base::PathExists(root_path)) |
| return true; |
| |
| return base::DeleteFile(GetNativeIOFilePath(root_path, name), |
| /*recursive=*/false); |
| } |
| |
| using GetAllFileNamesResult = |
| std::pair<base::File::Error, std::vector<std::string>>; |
| |
| // Performs the file I/O work in GetAllFileNames(). |
| GetAllFileNamesResult DoGetAllFileNames(const base::FilePath& root_path) { |
| std::vector<std::string> result; |
| |
| // If the origin's directory wasn't created yet, there's no file to report. |
| if (!base::PathExists(root_path)) |
| return {base::File::FILE_OK, std::move(result)}; |
| |
| base::FileEnumerator file_enumerator( |
| root_path, /*recursive=*/false, base::FileEnumerator::FILES, |
| /*pattern=*/base::FilePath::StringType(), |
| base::FileEnumerator::FolderSearchPolicy::ALL, |
| base::FileEnumerator::ErrorPolicy::STOP_ENUMERATION); |
| |
| // TODO(pwnall): The result vector can grow to an unbounded size. Add a limit |
| // parameter with a reasonable upper bound. |
| |
| for (base::FilePath file_path = file_enumerator.Next(); !file_path.empty(); |
| file_path = file_enumerator.Next()) { |
| // If the file name has a non-ASCII character, |file_name| will be the empty |
| // string. This will correctly be flagged as corruption by the check below. |
| std::string file_name = file_path.BaseName().MaybeAsASCII(); |
| |
| // Chrome's NativeIO implementation only creates files that have valid |
| // NativeIO names. Any other file names imply directory corruption. |
| if (!IsValidNativeIOName(file_name)) { |
| // TODO(pwnall): Figure out the corruption handling strategy. We could |
| // silently ignore the corrupted file, delete it, or stop |
| // and report an error. |
| continue; |
| } |
| result.push_back(std::move(file_name)); |
| } |
| |
| // Don't return a partial list of files if an error occurred. The partial list |
| // isn't meaningful, and may be useful information for a compromised renderer. |
| // |
| // TODO(pwnall): Reconsider this if we end up making NativeIO unusually |
| // friendly to corruption recovery. |
| base::File::Error enumeration_error = file_enumerator.GetError(); |
| if (enumeration_error != base::File::FILE_OK) |
| result.clear(); |
| |
| return {enumeration_error, std::move(result)}; |
| } |
| |
| // Reports the result of the file I/O work in GetAllFileNames(). |
| void DidGetAllFileNames( |
| blink::mojom::NativeIOHost::GetAllFileNamesCallback callback, |
| GetAllFileNamesResult result) { |
| std::move(callback).Run(result.first == base::File::FILE_OK, |
| std::move(result.second)); |
| } |
| |
| // Performs the file I/O work in RenameFile(). |
| bool DoRenameFile(const base::FilePath& root_path, |
| const std::string& old_name, |
| const std::string& new_name) { |
| DCHECK(IsValidNativeIOName(old_name)); |
| DCHECK(IsValidNativeIOName(new_name)); |
| |
| // If the origin's directory wasn't created yet, there's nothing to rename. |
| if (!base::PathExists(root_path)) |
| return false; |
| |
| // Do not overwrite an existing file. |
| if (base::PathExists(GetNativeIOFilePath(root_path, new_name))) |
| return false; |
| |
| // TODO(rstz): Report error. |
| base::File::Error error; |
| return base::ReplaceFile(GetNativeIOFilePath(root_path, old_name), |
| GetNativeIOFilePath(root_path, new_name), &error); |
| } |
| |
| } // namespace |
| |
| NativeIOHost::NativeIOHost(NativeIOContext* context, |
| const url::Origin& origin, |
| base::FilePath root_path) |
| : root_path_(std::move(root_path)), |
| context_(context), |
| origin_(origin), |
| file_task_runner_(CreateFileTaskRunner()) { |
| DCHECK(!root_path_.empty()); |
| DCHECK(context != nullptr); |
| |
| // base::Unretained is safe here because this NativeIOHost owns |receivers_|. |
| // So, the unretained NativeIOHost is guaranteed to outlive |receivers_| and |
| // the closure that it uses. |
| receivers_.set_disconnect_handler(base::BindRepeating( |
| &NativeIOHost::OnReceiverDisconnect, base::Unretained(this))); |
| } |
| |
| NativeIOHost::~NativeIOHost() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| } |
| |
| void NativeIOHost::BindReceiver( |
| mojo::PendingReceiver<blink::mojom::NativeIOHost> receiver) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| receivers_.Add(this, std::move(receiver)); |
| } |
| |
| void NativeIOHost::OpenFile( |
| const std::string& name, |
| mojo::PendingReceiver<blink::mojom::NativeIOFileHost> file_host_receiver, |
| OpenFileCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!IsValidNativeIOName(name)) { |
| mojo::ReportBadMessage("Invalid file name"); |
| std::move(callback).Run(base::File()); |
| return; |
| } |
| |
| if (open_file_hosts_.find(name) != open_file_hosts_.end()) { |
| // TODO(pwnall): Report that the file is locked. |
| std::move(callback).Run(base::File()); |
| return; |
| } |
| |
| auto insert_result = io_pending_files_.insert(name); |
| bool insert_success = insert_result.second; |
| if (!insert_success) { |
| // TODO(pwnall): Report that the file is locked. |
| std::move(callback).Run(base::File()); |
| return; |
| } |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoOpenFile, root_path_, name), |
| base::BindOnce(&NativeIOHost::DidOpenFile, weak_factory_.GetWeakPtr(), |
| name, std::move(file_host_receiver), std::move(callback))); |
| } |
| |
| void NativeIOHost::DeleteFile(const std::string& name, |
| DeleteFileCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!IsValidNativeIOName(name)) { |
| mojo::ReportBadMessage("Invalid file name"); |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| if (open_file_hosts_.find(name) != open_file_hosts_.end()) { |
| // TODO(pwnall): Report that the file is locked. |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| auto insert_result = io_pending_files_.insert(name); |
| bool insert_success = insert_result.second; |
| if (!insert_success) { |
| // TODO(pwnall): Report that the file is locked. |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoDeleteFile, root_path_, name), |
| base::BindOnce(&NativeIOHost::DidDeleteFile, weak_factory_.GetWeakPtr(), |
| name, std::move(callback))); |
| } |
| |
| void NativeIOHost::GetAllFileNames(GetAllFileNamesCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoGetAllFileNames, root_path_), |
| base::BindOnce(&DidGetAllFileNames, std::move(callback))); |
| } |
| |
| void NativeIOHost::RenameFile(const std::string& old_name, |
| const std::string& new_name, |
| RenameFileCallback callback) { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| if (!IsValidNativeIOName(old_name) || !IsValidNativeIOName(new_name)) { |
| mojo::ReportBadMessage("Invalid file name"); |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| if (open_file_hosts_.find(old_name) != open_file_hosts_.end() || |
| open_file_hosts_.find(new_name) != open_file_hosts_.end()) { |
| // TODO(rstz): Report that the file is locked. |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| auto old_iterator_and_success = io_pending_files_.insert(old_name); |
| if (!old_iterator_and_success.second) { |
| std::move(callback).Run(false); |
| return; |
| } |
| auto new_iterator_and_success = io_pending_files_.insert(new_name); |
| if (!new_iterator_and_success.second) { |
| io_pending_files_.erase(old_iterator_and_success.first); |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| file_task_runner_->PostTaskAndReplyWithResult( |
| FROM_HERE, base::BindOnce(&DoRenameFile, root_path_, old_name, new_name), |
| base::BindOnce(&NativeIOHost::DidRenameFile, weak_factory_.GetWeakPtr(), |
| old_name, new_name, std::move(callback))); |
| } |
| |
| void NativeIOHost::OnFileClose(NativeIOFileHost* file_host) { |
| DCHECK(open_file_hosts_.count(file_host->file_name()) > 0); |
| DCHECK_EQ(open_file_hosts_[file_host->file_name()].get(), file_host); |
| |
| open_file_hosts_.erase(file_host->file_name()); |
| } |
| |
| void NativeIOHost::OnReceiverDisconnect() { |
| DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); |
| |
| context_->OnHostReceiverDisconnect(this); |
| } |
| |
| void NativeIOHost::DidOpenFile( |
| const std::string& name, |
| mojo::PendingReceiver<blink::mojom::NativeIOFileHost> file_host_receiver, |
| OpenFileCallback callback, |
| base::File file) { |
| DCHECK(io_pending_files_.count(name)); |
| DCHECK(!open_file_hosts_.count(name)); |
| io_pending_files_.erase(name); |
| |
| if (!file.IsValid()) { |
| std::move(callback).Run(std::move(file)); |
| return; |
| } |
| |
| open_file_hosts_.insert( |
| {name, std::make_unique<NativeIOFileHost>(std::move(file_host_receiver), |
| this, name)}); |
| |
| std::move(callback).Run(std::move(file)); |
| return; |
| } |
| |
| void NativeIOHost::DidDeleteFile(const std::string& name, |
| DeleteFileCallback callback, |
| bool success) { |
| DCHECK(io_pending_files_.count(name)); |
| DCHECK(!open_file_hosts_.count(name)); |
| io_pending_files_.erase(name); |
| |
| std::move(callback).Run(success); |
| return; |
| } |
| |
| void NativeIOHost::DidRenameFile(const std::string& old_name, |
| const std::string& new_name, |
| RenameFileCallback callback, |
| bool success) { |
| DCHECK(io_pending_files_.count(old_name)); |
| DCHECK(!open_file_hosts_.count(old_name)); |
| DCHECK(io_pending_files_.count(new_name)); |
| DCHECK(!open_file_hosts_.count(new_name)); |
| io_pending_files_.erase(old_name); |
| io_pending_files_.erase(new_name); |
| |
| std::move(callback).Run(success); |
| return; |
| } |
| |
| } // namespace content |