Commit df72c8d5 authored by Richard Stotz's avatar Richard Stotz Committed by Commit Bot

NativeIO: Add Error Handling for file errors

This CL adds a mechanism to report file errors, as reported by
base::File::Error, though DOMExceptions. Due to security and privacy
considerations, not all errors are exposed to the user. For those
operations performed by the browser process (open, rename), the exact
error type is not exposed to the renderer.

Errors that are not reported by base::File::Error are left unchanged and
will be addressed in a followup CL.

The design document for this change is
https://docs.google.com/document/d/1rvs615AU2s8kVsmUlukbmtQNvUWFny0yzAS_gsnYZEs/

Bug: 1095537
Change-Id: If047ddccb6464dd6efb2b06a5da262d049f1e9a8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2436344
Commit-Queue: Richard Stotz <rstz@chromium.org>
Reviewed-by: default avatarVictor Costan <pwnall@chromium.org>
Reviewed-by: default avatarMike West <mkwst@chromium.org>
Cr-Commit-Position: refs/heads/master@{#829643}
parent d6d4f991
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
#include "storage/browser/quota/quota_manager_proxy.h" #include "storage/browser/quota/quota_manager_proxy.h"
#include "storage/browser/quota/special_storage_policy.h" #include "storage/browser/quota/special_storage_policy.h"
#include "storage/common/database/database_identifier.h" #include "storage/common/database/database_identifier.h"
#include "third_party/blink/public/common/native_io/native_io_utils.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom.h" #include "third_party/blink/public/mojom/native_io/native_io.mojom.h"
#include "url/origin.h" #include "url/origin.h"
...@@ -25,14 +26,6 @@ namespace { ...@@ -25,14 +26,6 @@ namespace {
constexpr base::FilePath::CharType kNativeIODirectoryName[] = constexpr base::FilePath::CharType kNativeIODirectoryName[] =
FILE_PATH_LITERAL("NativeIO"); FILE_PATH_LITERAL("NativeIO");
base::FilePath GetNativeIORootPath(const base::FilePath& profile_root) {
if (profile_root.empty())
return base::FilePath();
return profile_root.Append(kNativeIODirectoryName);
}
} // namespace } // namespace
NativeIOContext::NativeIOContext( NativeIOContext::NativeIOContext(
...@@ -110,4 +103,26 @@ base::FilePath NativeIOContext::RootPathForOrigin(const url::Origin& origin) { ...@@ -110,4 +103,26 @@ base::FilePath NativeIOContext::RootPathForOrigin(const url::Origin& origin) {
return origin_path; return origin_path;
} }
// static
base::FilePath NativeIOContext::GetNativeIORootPath(
const base::FilePath& profile_root) {
if (profile_root.empty())
return base::FilePath();
return profile_root.Append(kNativeIODirectoryName);
}
// static
blink::mojom::NativeIOErrorPtr NativeIOContext::FileErrorToNativeIOError(
base::File::Error file_error,
std::string message) {
blink::mojom::NativeIOErrorType native_io_error_type =
blink::native_io::FileErrorToNativeIOErrorType(file_error);
std::string final_message =
message.empty()
? blink::native_io::GetDefaultMessage(native_io_error_type)
: message;
return blink::mojom::NativeIOError::New(native_io_error_type, final_message);
}
} // namespace content } // namespace content
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include "base/files/file.h"
#include "base/files/file_path.h" #include "base/files/file_path.h"
#include "base/sequence_checker.h" #include "base/sequence_checker.h"
#include "content/common/content_export.h" #include "content/common/content_export.h"
...@@ -55,12 +56,22 @@ class CONTENT_EXPORT NativeIOContext { ...@@ -55,12 +56,22 @@ class CONTENT_EXPORT NativeIOContext {
// |host| must be owned by this context. This method should only be called by // |host| must be owned by this context. This method should only be called by
// NativeIOHost. // NativeIOHost.
void OnHostReceiverDisconnect(NativeIOHost* host); void OnHostReceiverDisconnect(NativeIOHost* host);
private:
// Computes the path to the directory storing an origin's NativeIO files. // Computes the path to the directory storing an origin's NativeIO files.
// //
// Returns an empty path if the origin isn't supported for NativeIO. // Returns an empty path if the origin isn't supported for NativeIO.
base::FilePath RootPathForOrigin(const url::Origin& origin); base::FilePath RootPathForOrigin(const url::Origin& origin);
// Computes the path to the directory storing a profile's NativeIO files.
static base::FilePath GetNativeIORootPath(const base::FilePath& profile_root);
// Transform a base::File::Error into a NativeIOError with default error
// message if none is provided.
static blink::mojom::NativeIOErrorPtr FileErrorToNativeIOError(
base::File::Error file_error,
std::string message = "");
private:
std::map<url::Origin, std::unique_ptr<NativeIOHost>> hosts_; std::map<url::Origin, std::unique_ptr<NativeIOHost>> hosts_;
// Points to the root directory for NativeIO files. // Points to the root directory for NativeIO files.
......
// 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 "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/native_io/native_io_context.h"
#include "content/public/browser/browser_context.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "storage/common/database/database_identifier.h"
#include "url/gurl.h"
namespace content {
class NativeIOContextBrowserTest : public ContentBrowserTest {
public:
NativeIOContextBrowserTest() {
// SharedArrayBuffers are not enabled by default on Android, see also
// https://crbug.com/1144104 .
feature_list_.InitAndEnableFeature(features::kSharedArrayBuffer);
}
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
ContentBrowserTest::SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
ASSERT_TRUE(embedded_test_server()->Start());
}
base::FilePath GetNativeIODir(base::FilePath user_data_dir,
const GURL& test_url) {
std::string origin_identifier =
storage::GetIdentifierFromOrigin(test_url.GetOrigin());
base::FilePath root_dir =
NativeIOContext::GetNativeIORootPath(user_data_dir);
return root_dir.AppendASCII(origin_identifier);
}
DISALLOW_COPY_AND_ASSIGN(NativeIOContextBrowserTest);
private:
base::test::ScopedFeatureList feature_list_;
};
IN_PROC_BROWSER_TEST_F(NativeIOContextBrowserTest, ReadFromDeletedFile) {
const GURL& test_url = embedded_test_server()->GetURL(
"/native_io/read_from_deleted_file_test.html");
Shell* browser = CreateBrowser();
base::FilePath user_data_dir = GetNativeIODir(
browser->web_contents()->GetBrowserContext()->GetPath(), test_url);
NavigateToURLBlockUntilNavigationsComplete(browser, test_url,
/*number_of_navigations=*/1);
EXPECT_TRUE(EvalJs(browser, "writeToFile()").ExtractBool());
{
base::ScopedAllowBlockingForTesting allow_blocking;
ASSERT_TRUE(base::DeleteFile(user_data_dir.AppendASCII("test_file")));
}
EXPECT_TRUE(EvalJs(browser, "readFromFile()").ExtractBool());
}
// This test depends on POSIX file permissions, which do not work on Windows,
// Android, or Fuchsia.
#if !defined(OS_WIN) && !defined(OS_ANDROID) && !defined(OS_FUCHSIA)
IN_PROC_BROWSER_TEST_F(NativeIOContextBrowserTest, TryOpenProtectedFileTest) {
const GURL& test_url = embedded_test_server()->GetURL(
"/native_io/try_open_protected_file_test.html");
Shell* browser = CreateBrowser();
base::FilePath user_data_dir_ = GetNativeIODir(
browser->web_contents()->GetBrowserContext()->GetPath(), test_url);
NavigateToURLBlockUntilNavigationsComplete(browser, test_url,
/*number_of_navigations=*/1);
EXPECT_TRUE(EvalJs(browser, "createAndCloseFile()").ExtractBool());
{
base::ScopedAllowBlockingForTesting allow_blocking;
base::SetPosixFilePermissions(user_data_dir_.AppendASCII("test_file"),
0300); // not readable
}
std::string expected_caught_error = "InvalidStateError";
EXPECT_EQ(EvalJs(browser, "tryOpeningFile()").ExtractString(),
expected_caught_error);
}
#endif // !defined(OS_WIN) && !defined(OS_ANDROID) && !defined(OS_FUCHSIA)
} // namespace content
...@@ -8,28 +8,39 @@ ...@@ -8,28 +8,39 @@
#include <utility> #include <utility>
#include "base/bind.h" #include "base/bind.h"
#include "base/files/file.h"
#include "base/sequence_checker.h" #include "base/sequence_checker.h"
#include "base/task/task_traits.h" #include "base/task/task_traits.h"
#include "base/task/thread_pool.h" #include "base/task/thread_pool.h"
#include "content/browser/native_io/native_io_context.h"
#include "content/browser/native_io/native_io_host.h" #include "content/browser/native_io/native_io_host.h"
#include "third_party/blink/public/common/native_io/native_io_utils.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom.h" #include "third_party/blink/public/mojom/native_io/native_io.mojom.h"
using blink::mojom::NativeIOError;
using blink::mojom::NativeIOErrorPtr;
using blink::mojom::NativeIOErrorType;
namespace content { namespace content {
namespace { namespace {
// Performs the file I/O work in SetLength(). // Performs the file I/O work in SetLength().
std::pair<bool, base::File> DoSetLength(const int64_t length, base::File file) { std::pair<base::File, base::File::Error> DoSetLength(const int64_t length,
bool set_length_success = false; base::File file) {
DCHECK_GE(length, 0) << "The file length should not be negative"; DCHECK_GE(length, 0) << "The file length should not be negative";
set_length_success = file.SetLength(length); base::File::Error set_length_error;
bool success = file.SetLength(length);
set_length_error = success ? base::File::FILE_OK : file.GetLastFileError();
return {set_length_success, std::move(file)}; return {std::move(file), set_length_error};
} }
void DidSetLength(blink::mojom::NativeIOFileHost::SetLengthCallback callback, void DidSetLength(blink::mojom::NativeIOFileHost::SetLengthCallback callback,
std::pair<bool, base::File> result) { std::pair<base::File, base::File::Error> result) {
std::move(callback).Run(result.first, std::move(result.second)); NativeIOErrorPtr error =
NativeIOContext::FileErrorToNativeIOError(result.second);
std::move(callback).Run(std::move(result.first), std::move(error));
} }
} // namespace } // namespace
...@@ -64,14 +75,20 @@ void NativeIOFileHost::SetLength(const int64_t length, ...@@ -64,14 +75,20 @@ void NativeIOFileHost::SetLength(const int64_t length,
if (length < 0) { if (length < 0) {
mojo::ReportBadMessage("The file length cannot be negative."); mojo::ReportBadMessage("The file length cannot be negative.");
std::move(callback).Run(false, std::move(file)); // No error message is specified as the ReportBadMessage() call should close
// the pipe and kill the renderer.
std::move(callback).Run(
std::move(file), NativeIOError::New(NativeIOErrorType::kUnknown, ""));
return; return;
} }
// file.IsValid() does not interact with the file system, so we may call it on // file.IsValid() does not interact with the file system, so we may call it on
// this thread. // this thread.
if (!file.IsValid()) { if (!file.IsValid()) {
mojo::ReportBadMessage("The file is invalid."); mojo::ReportBadMessage("The file is invalid.");
std::move(callback).Run(false, std::move(file)); // No error message is specified as the ReportBadMessage() call should close
// the pipe and kill the renderer.
std::move(callback).Run(
std::move(file), NativeIOError::New(NativeIOErrorType::kUnknown, ""));
return; return;
} }
......
...@@ -23,8 +23,13 @@ ...@@ -23,8 +23,13 @@
#include "content/browser/native_io/native_io_file_host.h" #include "content/browser/native_io/native_io_file_host.h"
#include "mojo/public/cpp/bindings/message.h" #include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_receiver.h" #include "mojo/public/cpp/bindings/pending_receiver.h"
#include "third_party/blink/public/common/native_io/native_io_utils.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom.h" #include "third_party/blink/public/mojom/native_io/native_io.mojom.h"
using blink::mojom::NativeIOError;
using blink::mojom::NativeIOErrorPtr;
using blink::mojom::NativeIOErrorType;
namespace content { namespace content {
namespace { namespace {
...@@ -87,14 +92,20 @@ base::File DoOpenFile(const base::FilePath& root_path, ...@@ -87,14 +92,20 @@ base::File DoOpenFile(const base::FilePath& root_path,
} }
// Performs the file I/O work in DeleteFile(). // Performs the file I/O work in DeleteFile().
bool DoDeleteFile(const base::FilePath& root_path, const std::string& name) { NativeIOErrorPtr DoDeleteFile(const base::FilePath& root_path,
const std::string& name) {
DCHECK(IsValidNativeIOName(name)); DCHECK(IsValidNativeIOName(name));
// If the origin's directory wasn't created yet, there's nothing to delete. // If the origin's directory wasn't created yet, there's nothing to delete.
if (!base::PathExists(root_path)) if (!base::PathExists(root_path))
return true; return NativeIOError::New(NativeIOErrorType::kSuccess, "");
return base::DeleteFile(GetNativeIOFilePath(root_path, name)); bool success = base::DeleteFile(GetNativeIOFilePath(root_path, name));
if (!success) {
return NativeIOContext::FileErrorToNativeIOError(
base::File::GetLastFileError());
}
return NativeIOError::New(NativeIOErrorType::kSuccess, "");
} }
using GetAllFileNamesResult = using GetAllFileNamesResult =
...@@ -155,24 +166,31 @@ void DidGetAllFileNames( ...@@ -155,24 +166,31 @@ void DidGetAllFileNames(
} }
// Performs the file I/O work in RenameFile(). // Performs the file I/O work in RenameFile().
bool DoRenameFile(const base::FilePath& root_path, NativeIOErrorPtr DoRenameFile(const base::FilePath& root_path,
const std::string& old_name, const std::string& old_name,
const std::string& new_name) { const std::string& new_name) {
DCHECK(IsValidNativeIOName(old_name)); DCHECK(IsValidNativeIOName(old_name));
DCHECK(IsValidNativeIOName(new_name)); DCHECK(IsValidNativeIOName(new_name));
base::File::Error error = base::File::FILE_OK;
// If the origin's directory wasn't created yet, there's nothing to rename. // If the origin's directory wasn't created yet, there's nothing to rename.
if (!base::PathExists(root_path)) // This error cannot be used to determine the existence of files outside of
return false; // the origin's directory, as |old_name| is a valid NativeIO name.
if (!base::PathExists(root_path) ||
// Do not overwrite an existing file. !base::PathExists(GetNativeIOFilePath(root_path, old_name)))
return NativeIOError::New(NativeIOErrorType::kNotFound,
"Source file does not exist");
// Do not overwrite an existing file. This error cannot be used to determine
// the existence of files outside of the origin's directory, as |new_name| is
// a valid NativeIO name.
if (base::PathExists(GetNativeIOFilePath(root_path, new_name))) if (base::PathExists(GetNativeIOFilePath(root_path, new_name)))
return false; return NativeIOError::New(NativeIOErrorType::kNoModificationAllowed,
"Target file exists");
// TODO(rstz): Report error. base::ReplaceFile(GetNativeIOFilePath(root_path, old_name),
base::File::Error error; GetNativeIOFilePath(root_path, new_name), &error);
return base::ReplaceFile(GetNativeIOFilePath(root_path, old_name), return NativeIOContext::FileErrorToNativeIOError(error);
GetNativeIOFilePath(root_path, new_name), &error);
} }
} // namespace } // namespace
...@@ -213,21 +231,27 @@ void NativeIOHost::OpenFile( ...@@ -213,21 +231,27 @@ void NativeIOHost::OpenFile(
if (!IsValidNativeIOName(name)) { if (!IsValidNativeIOName(name)) {
mojo::ReportBadMessage("Invalid file name"); mojo::ReportBadMessage("Invalid file name");
std::move(callback).Run(base::File()); std::move(callback).Run(
base::File(),
NativeIOError::New(NativeIOErrorType::kUnknown, "Invalid file name"));
return; return;
} }
if (open_file_hosts_.find(name) != open_file_hosts_.end()) { if (open_file_hosts_.find(name) != open_file_hosts_.end()) {
// TODO(pwnall): Report that the file is locked. std::move(callback).Run(
std::move(callback).Run(base::File()); base::File(),
NativeIOError::New(NativeIOErrorType::kNoModificationAllowed,
"File is open"));
return; return;
} }
auto insert_result = io_pending_files_.insert(name); auto insert_result = io_pending_files_.insert(name);
bool insert_success = insert_result.second; bool insert_success = insert_result.second;
if (!insert_success) { if (!insert_success) {
// TODO(pwnall): Report that the file is locked. std::move(callback).Run(
std::move(callback).Run(base::File()); base::File(),
NativeIOError::New(NativeIOErrorType::kNoModificationAllowed,
"Operation pending on file"));
return; return;
} }
...@@ -243,21 +267,23 @@ void NativeIOHost::DeleteFile(const std::string& name, ...@@ -243,21 +267,23 @@ void NativeIOHost::DeleteFile(const std::string& name,
if (!IsValidNativeIOName(name)) { if (!IsValidNativeIOName(name)) {
mojo::ReportBadMessage("Invalid file name"); mojo::ReportBadMessage("Invalid file name");
std::move(callback).Run(false); std::move(callback).Run(
NativeIOError::New(NativeIOErrorType::kUnknown, "Invalid file name"));
return; return;
} }
if (open_file_hosts_.find(name) != open_file_hosts_.end()) { if (open_file_hosts_.find(name) != open_file_hosts_.end()) {
// TODO(pwnall): Report that the file is locked. std::move(callback).Run(NativeIOError::New(
std::move(callback).Run(false); NativeIOErrorType::kNoModificationAllowed, "File is open"));
return; return;
} }
auto insert_result = io_pending_files_.insert(name); auto insert_result = io_pending_files_.insert(name);
bool insert_success = insert_result.second; bool insert_success = insert_result.second;
if (!insert_success) { if (!insert_success) {
// TODO(pwnall): Report that the file is locked. std::move(callback).Run(
std::move(callback).Run(false); NativeIOError::New(NativeIOErrorType::kNoModificationAllowed,
"Operation pending on file"));
return; return;
} }
...@@ -282,26 +308,37 @@ void NativeIOHost::RenameFile(const std::string& old_name, ...@@ -282,26 +308,37 @@ void NativeIOHost::RenameFile(const std::string& old_name,
if (!IsValidNativeIOName(old_name) || !IsValidNativeIOName(new_name)) { if (!IsValidNativeIOName(old_name) || !IsValidNativeIOName(new_name)) {
mojo::ReportBadMessage("Invalid file name"); mojo::ReportBadMessage("Invalid file name");
std::move(callback).Run(false); std::move(callback).Run(
NativeIOError::New(NativeIOErrorType::kUnknown, "Invalid file name"));
return; return;
} }
if (open_file_hosts_.find(old_name) != open_file_hosts_.end() || if (open_file_hosts_.find(old_name) != open_file_hosts_.end() ||
open_file_hosts_.find(new_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(NativeIOError::New(
std::move(callback).Run(false); NativeIOErrorType::kNoModificationAllowed, "Source file is open"));
return;
}
if (open_file_hosts_.find(old_name) != open_file_hosts_.end()) {
std::move(callback).Run(NativeIOError::New(
NativeIOErrorType::kNoModificationAllowed, "Target file is open"));
return; return;
} }
auto old_iterator_and_success = io_pending_files_.insert(old_name); auto old_iterator_and_success = io_pending_files_.insert(old_name);
if (!old_iterator_and_success.second) { if (!old_iterator_and_success.second) {
std::move(callback).Run(false); std::move(callback).Run(
NativeIOError::New(NativeIOErrorType::kNoModificationAllowed,
"Operation pending on source file"));
return; return;
} }
auto new_iterator_and_success = io_pending_files_.insert(new_name); auto new_iterator_and_success = io_pending_files_.insert(new_name);
if (!new_iterator_and_success.second) { if (!new_iterator_and_success.second) {
io_pending_files_.erase(old_iterator_and_success.first); io_pending_files_.erase(old_iterator_and_success.first);
std::move(callback).Run(false); std::move(callback).Run(
NativeIOError::New(NativeIOErrorType::kNoModificationAllowed,
"Operation pending on target file"));
return; return;
} }
...@@ -333,8 +370,15 @@ void NativeIOHost::DidOpenFile( ...@@ -333,8 +370,15 @@ void NativeIOHost::DidOpenFile(
DCHECK(!open_file_hosts_.count(name)); DCHECK(!open_file_hosts_.count(name));
io_pending_files_.erase(name); io_pending_files_.erase(name);
base::File::Error open_error = file.error_details();
if (!file.IsValid()) { if (!file.IsValid()) {
std::move(callback).Run(std::move(file)); // Make sure an error is reported whenever the file is not valid.
open_error = open_error != base::File::FILE_OK
? open_error
: base::File::FILE_ERROR_FAILED;
std::move(callback).Run(
std::move(file), NativeIOContext::FileErrorToNativeIOError(open_error));
return; return;
} }
...@@ -342,25 +386,26 @@ void NativeIOHost::DidOpenFile( ...@@ -342,25 +386,26 @@ void NativeIOHost::DidOpenFile(
{name, std::make_unique<NativeIOFileHost>(std::move(file_host_receiver), {name, std::make_unique<NativeIOFileHost>(std::move(file_host_receiver),
this, name)}); this, name)});
std::move(callback).Run(std::move(file)); std::move(callback).Run(
std::move(file), NativeIOContext::FileErrorToNativeIOError(open_error));
return; return;
} }
void NativeIOHost::DidDeleteFile(const std::string& name, void NativeIOHost::DidDeleteFile(const std::string& name,
DeleteFileCallback callback, DeleteFileCallback callback,
bool success) { NativeIOErrorPtr delete_error) {
DCHECK(io_pending_files_.count(name)); DCHECK(io_pending_files_.count(name));
DCHECK(!open_file_hosts_.count(name)); DCHECK(!open_file_hosts_.count(name));
io_pending_files_.erase(name); io_pending_files_.erase(name);
std::move(callback).Run(success); std::move(callback).Run(std::move(delete_error));
return; return;
} }
void NativeIOHost::DidRenameFile(const std::string& old_name, void NativeIOHost::DidRenameFile(const std::string& old_name,
const std::string& new_name, const std::string& new_name,
RenameFileCallback callback, RenameFileCallback callback,
bool success) { NativeIOErrorPtr rename_error) {
DCHECK(io_pending_files_.count(old_name)); DCHECK(io_pending_files_.count(old_name));
DCHECK(!open_file_hosts_.count(old_name)); DCHECK(!open_file_hosts_.count(old_name));
DCHECK(io_pending_files_.count(new_name)); DCHECK(io_pending_files_.count(new_name));
...@@ -368,7 +413,7 @@ void NativeIOHost::DidRenameFile(const std::string& old_name, ...@@ -368,7 +413,7 @@ void NativeIOHost::DidRenameFile(const std::string& old_name,
io_pending_files_.erase(old_name); io_pending_files_.erase(old_name);
io_pending_files_.erase(new_name); io_pending_files_.erase(new_name);
std::move(callback).Run(success); std::move(callback).Run(std::move(rename_error));
return; return;
} }
......
...@@ -91,13 +91,13 @@ class NativeIOHost : public blink::mojom::NativeIOHost { ...@@ -91,13 +91,13 @@ class NativeIOHost : public blink::mojom::NativeIOHost {
// Called after the file I/O part of DeleteFile() completed. // Called after the file I/O part of DeleteFile() completed.
void DidDeleteFile(const std::string& name, void DidDeleteFile(const std::string& name,
DeleteFileCallback callback, DeleteFileCallback callback,
bool success); blink::mojom::NativeIOErrorPtr delete_error);
// Called after the file I/O part of RenameFile() completed. // Called after the file I/O part of RenameFile() completed.
void DidRenameFile(const std::string& old_name, void DidRenameFile(const std::string& old_name,
const std::string& new_name, const std::string& new_name,
RenameFileCallback callback, RenameFileCallback callback,
bool success); blink::mojom::NativeIOErrorPtr rename_error);
// The directory holding all the files for this origin. // The directory holding all the files for this origin.
const base::FilePath root_path_; const base::FilePath root_path_;
......
...@@ -1062,6 +1062,7 @@ test("content_browsertests") { ...@@ -1062,6 +1062,7 @@ test("content_browsertests") {
"../browser/media/webaudio/audio_context_manager_browsertest.cc", "../browser/media/webaudio/audio_context_manager_browsertest.cc",
"../browser/message_port_provider_browsertest.cc", "../browser/message_port_provider_browsertest.cc",
"../browser/mojo_sandbox_browsertest.cc", "../browser/mojo_sandbox_browsertest.cc",
"../browser/native_io/native_io_context_browsertest.cc",
"../browser/navigation_browsertest.cc", "../browser/navigation_browsertest.cc",
"../browser/navigation_mhtml_browsertest.cc", "../browser/navigation_mhtml_browsertest.cc",
"../browser/net/accept_header_browsertest.cc", "../browser/net/accept_header_browsertest.cc",
......
<!doctype html>
<title>Read from deleted file test</title>
<script>
let test_file;
async function writeToFile() {
if (nativeIO) {
test_file = await nativeIO.open('test_file');
const writtenBytes = new Uint8Array(new SharedArrayBuffer(4));
writtenBytes.set([64, 65, 66, 67]);
const writeCount = await test_file.write(writtenBytes, 0);
document.location.hash = '#ready';
return writeCount == 4;
} else {
return false;
}
}
async function readFromFile() {
const readSharedArrayBuffer = new SharedArrayBuffer(4);
const readBytes = new Uint8Array(readSharedArrayBuffer);
const readCount = await test_file.read(readBytes, 0);
return readCount == 4;
}
</script>
<!doctype html>
<title>Try opening a protected file test</title>
<script>
async function createAndCloseFile() {
if (nativeIO) {
const test_file = await nativeIO.open('test_file');
await test_file.close();
return true;
} else {
return false;
}
}
async function tryOpeningFile() {
try {
await nativeIO.open('test_file');
} catch (e) {
return e.name;
}
return '';
}
</script>
...@@ -140,6 +140,7 @@ source_set("common") { ...@@ -140,6 +140,7 @@ source_set("common") {
"messaging/web_message_port.cc", "messaging/web_message_port.cc",
"mime_util/mime_util.cc", "mime_util/mime_util.cc",
"mobile_metrics/mobile_friendliness.cc", "mobile_metrics/mobile_friendliness.cc",
"native_io/native_io_utils.cc",
"notifications/notification_mojom_traits.cc", "notifications/notification_mojom_traits.cc",
"notifications/notification_resources.cc", "notifications/notification_resources.cc",
"notifications/platform_notification_data.cc", "notifications/platform_notification_data.cc",
......
// 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 "third_party/blink/public/common/native_io/native_io_utils.h"
#include "base/files/file.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-shared.h"
namespace blink {
namespace native_io {
// See https://crbug.com/1095537 for a design doc of this mapping.
blink::mojom::NativeIOErrorType FileErrorToNativeIOErrorType(
const base::File::Error error) {
switch (error) {
case base::File::FILE_OK:
return mojom::NativeIOErrorType::kSuccess;
// Errors in this category are unexpected and provide no way of recovery.
case base::File::FILE_ERROR_ABORT:
case base::File::FILE_ERROR_INVALID_OPERATION:
case base::File::FILE_ERROR_INVALID_URL:
case base::File::FILE_ERROR_IO:
case base::File::FILE_ERROR_NOT_A_DIRECTORY:
case base::File::FILE_ERROR_NOT_A_FILE:
case base::File::FILE_ERROR_NOT_EMPTY:
return mojom::NativeIOErrorType::kUnknown;
// Errors in this category have no recovery path within NativeIO. NOT_FOUND
// is included here, as Windows returns NOT_FOUND when attempting to use an
// overly long file name.
case base::File::FILE_ERROR_ACCESS_DENIED:
case base::File::FILE_ERROR_SECURITY:
case base::File::FILE_ERROR_FAILED:
return mojom::NativeIOErrorType::kInvalidState;
case base::File::FILE_ERROR_NOT_FOUND:
return mojom::NativeIOErrorType::kNotFound;
// Errors in this category have a recovery path.
case base::File::FILE_ERROR_EXISTS:
case base::File::FILE_ERROR_IN_USE:
case base::File::FILE_ERROR_NO_MEMORY:
case base::File::FILE_ERROR_TOO_MANY_OPENED:
return mojom::NativeIOErrorType::kNoModificationAllowed;
case base::File::FILE_ERROR_NO_SPACE:
return mojom::NativeIOErrorType::kNoSpace;
case base::File::FILE_ERROR_MAX:
NOTREACHED();
return mojom::NativeIOErrorType::kUnknown;
}
NOTREACHED();
return mojom::NativeIOErrorType::kUnknown;
}
std::string GetDefaultMessage(const mojom::NativeIOErrorType nativeio_error) {
switch (nativeio_error) {
case mojom::NativeIOErrorType::kSuccess:
return "";
case mojom::NativeIOErrorType::kUnknown:
return "Unspecified internal error.";
case mojom::NativeIOErrorType::kInvalidState:
return "Operation failed.";
case mojom::NativeIOErrorType::kNotFound:
return "File not found.";
case mojom::NativeIOErrorType::kNoModificationAllowed:
return "No modification allowed.";
case mojom::NativeIOErrorType::kNoSpace:
return "No space available.";
}
NOTREACHED();
return std::string();
}
} // namespace native_io
} // namespace blink
...@@ -156,6 +156,7 @@ source_set("headers") { ...@@ -156,6 +156,7 @@ source_set("headers") {
"metrics/form_element_pii_type.h", "metrics/form_element_pii_type.h",
"mime_util/mime_util.h", "mime_util/mime_util.h",
"mobile_metrics/mobile_friendliness.h", "mobile_metrics/mobile_friendliness.h",
"native_io/native_io_utils.h",
"navigation/triggering_event_info.h", "navigation/triggering_event_info.h",
"notifications/notification_constants.h", "notifications/notification_constants.h",
"notifications/notification_mojom_traits.h", "notifications/notification_mojom_traits.h",
......
// 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.
#ifndef THIRD_PARTY_BLINK_PUBLIC_COMMON_NATIVE_IO_NATIVE_IO_UTILS_H_
#define THIRD_PARTY_BLINK_PUBLIC_COMMON_NATIVE_IO_NATIVE_IO_UTILS_H_
#include <cstdint>
#include "base/files/file.h"
#include "third_party/blink/public/common/common_export.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-shared.h"
namespace blink {
namespace native_io {
BLINK_COMMON_EXPORT blink::mojom::NativeIOErrorType
FileErrorToNativeIOErrorType(const base::File::Error error);
BLINK_COMMON_EXPORT std::string GetDefaultMessage(
const blink::mojom::NativeIOErrorType nativeio_error);
} // namespace native_io
} // namespace blink
#endif // THIRD_PARTY_BLINK_PUBLIC_COMMON_NATIVE_IO_NATIVE_IO_UTILS_H_
...@@ -5,6 +5,34 @@ ...@@ -5,6 +5,34 @@
module blink.mojom; module blink.mojom;
import "mojo/public/mojom/base/file.mojom"; import "mojo/public/mojom/base/file.mojom";
import "mojo/public/mojom/base/file_error.mojom";
// NativeIOErrorTypes are designed to give sufficient information to web
// application developers while masking platform-specific behaviour that may
// create privacy or security issues.
// See https://crbug.com/1095537 for a design doc.
// TODO(rstz): Add error descriptions to the specification.
enum NativeIOErrorType {
kSuccess,
// The operation failed due to an unspecified file error. There is no clear
// path to recovery from this error.
kUnknown,
// The object being operated on was in a permanent invalid state for the
// operation. Retrying is unlikely to succeed.
kInvalidState,
// An object being operated on was not found.
kNotFound,
// An object being operated cannot be modified at this time. Retrying might
// succeed.
kNoModificationAllowed,
// No space on disk is available for this operation.
kNoSpace,
};
struct NativeIOError {
NativeIOErrorType type;
string message;
};
// The NativeIO API currently offers synchronous access to storage. This is // The NativeIO API currently offers synchronous access to storage. This is
// purely for purpose of allowing developers to experiment, so we can learn the // purely for purpose of allowing developers to experiment, so we can learn the
...@@ -45,10 +73,12 @@ interface NativeIOFileHost { ...@@ -45,10 +73,12 @@ interface NativeIOFileHost {
// multiple file handles to the same file. A compromised renderer may hand // multiple file handles to the same file. A compromised renderer may hand
// over an arbitrary file. As base::File::SetLength() is allowed by the // over an arbitrary file. As base::File::SetLength() is allowed by the
// Windows, Linux, and macOS 10.15+ sandbox, it is safe to expose this // Windows, Linux, and macOS 10.15+ sandbox, it is safe to expose this
// functionality to the renderer on macOS < 10.15 as well. See // functionality to the renderer on macOS < 10.15 as well. We also return
// any unsanitized base::File::Error that might have occurred. See
// crbug.com/1084565. // crbug.com/1084565.
[Sync] SetLength(int64 length, mojo_base.mojom.File backing_file) => (bool [Sync] SetLength(int64 length, mojo_base.mojom.File backing_file) =>
success, mojo_base.mojom.File backing_file); (mojo_base.mojom.File backing_file,
NativeIOError set_length_error);
}; };
// Origin-scoped implementation of the Web Platform's NativeIO API. // Origin-scoped implementation of the Web Platform's NativeIO API.
...@@ -67,11 +97,11 @@ interface NativeIOHost { ...@@ -67,11 +97,11 @@ interface NativeIOHost {
// |lock_handle|, or until it disconnects from it. // |lock_handle|, or until it disconnects from it.
[Sync] [Sync]
OpenFile(string name, pending_receiver<NativeIOFileHost> file_host_receiver) OpenFile(string name, pending_receiver<NativeIOFileHost> file_host_receiver)
=> (mojo_base.mojom.File? backing_file); => (mojo_base.mojom.File? backing_file, NativeIOError open_error);
// Deletes a previously created file. // Deletes a previously created file.
[Sync] [Sync]
DeleteFile(string name) => (bool success); DeleteFile(string name) => (NativeIOError delete_error);
// Lists all the files created by the origin. // Lists all the files created by the origin.
[Sync] [Sync]
...@@ -82,7 +112,8 @@ interface NativeIOHost { ...@@ -82,7 +112,8 @@ interface NativeIOHost {
// Rename does not allow renaming any files that are currently open and does // Rename does not allow renaming any files that are currently open and does
// not override existing files. // not override existing files.
[Sync] [Sync]
RenameFile(string old_name, string new_name) => (bool success); RenameFile(string old_name, string new_name) =>
(NativeIOError rename_error);
// TODO(pwnall): Build quota integration before this API exits Dev Trials. // TODO(pwnall): Build quota integration before this API exits Dev Trials.
}; };
...@@ -8,6 +8,8 @@ blink_modules_sources("native_io") { ...@@ -8,6 +8,8 @@ blink_modules_sources("native_io") {
sources = [ sources = [
"global_native_io.cc", "global_native_io.cc",
"global_native_io.h", "global_native_io.h",
"native_io_error.cc",
"native_io_error.h",
"native_io_file.cc", "native_io_file.cc",
"native_io_file.h", "native_io_file.h",
"native_io_file_sync.cc", "native_io_file_sync.cc",
......
// 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 "third_party/blink/renderer/modules/native_io/native_io_error.h"
#include "base/files/file.h"
#include "third_party/blink/public/common/native_io/native_io_utils.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-blink.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
using blink::mojom::blink::NativeIOErrorPtr;
using blink::mojom::blink::NativeIOErrorType;
namespace blink {
namespace {
DOMExceptionCode NativeIOErrorToDOMExceptionCode(NativeIOErrorType error) {
switch (error) {
case NativeIOErrorType::kSuccess:
// This function should only be called with an error.
NOTREACHED();
return DOMExceptionCode::kNoError;
case NativeIOErrorType::kUnknown:
return DOMExceptionCode::kUnknownError;
case NativeIOErrorType::kInvalidState:
return DOMExceptionCode::kInvalidStateError;
case NativeIOErrorType::kNotFound:
return DOMExceptionCode::kNotFoundError;
case NativeIOErrorType::kNoModificationAllowed:
return DOMExceptionCode::kNoModificationAllowedError;
case NativeIOErrorType::kNoSpace:
return DOMExceptionCode::kQuotaExceededError;
}
NOTREACHED();
return DOMExceptionCode::kUnknownError;
}
} // namespace
void RejectNativeIOWithError(ScriptPromiseResolver* resolver,
NativeIOErrorPtr error) {
DCHECK(resolver->GetScriptState()->ContextIsValid())
<< "The resolver's script state must be valid.";
ScriptState* script_state = resolver->GetScriptState();
DOMExceptionCode exception_code =
NativeIOErrorToDOMExceptionCode(error->type);
resolver->Reject(V8ThrowDOMException::CreateOrEmpty(
script_state->GetIsolate(), exception_code, error->message));
return;
}
void RejectNativeIOWithError(ScriptPromiseResolver* resolver,
base::File::Error file_error,
const String& message) {
DCHECK(resolver->GetScriptState()->ContextIsValid())
<< "The resolver's script state must be valid.";
RejectNativeIOWithError(resolver,
FileErrorToNativeIOError(file_error, message));
return;
}
void ThrowNativeIOWithError(ExceptionState& exception_state,
NativeIOErrorPtr error) {
DOMExceptionCode exception_code =
NativeIOErrorToDOMExceptionCode(error->type);
exception_state.ThrowDOMException(exception_code, error->message);
return;
}
void ThrowNativeIOWithError(ExceptionState& exception_state,
base::File::Error file_error,
const String& message) {
ThrowNativeIOWithError(exception_state,
FileErrorToNativeIOError(file_error, message));
return;
}
NativeIOErrorPtr FileErrorToNativeIOError(base::File::Error file_error,
const String& message) {
NativeIOErrorType native_io_error_type =
blink::native_io::FileErrorToNativeIOErrorType(file_error);
String final_message =
message.IsEmpty() ? String::FromUTF8(blink::native_io::GetDefaultMessage(
native_io_error_type)
.c_str())
: message;
return mojom::blink::NativeIOError::New(native_io_error_type, final_message);
}
} // namespace blink
// 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_MODULES_NATIVE_IO_NATIVE_IO_ERROR_H_
#define THIRD_PARTY_BLINK_RENDERER_MODULES_NATIVE_IO_NATIVE_IO_ERROR_H_
#include "base/files/file.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-blink.h"
using blink::mojom::blink::NativeIOErrorPtr;
using blink::mojom::blink::NativeIOErrorType;
namespace blink {
class ScriptPromiseResolver;
class ExceptionState;
// Reject the `resolver` with the appropriate DOMException given
// `error`. The resolver's execution context should be valid.
void RejectNativeIOWithError(ScriptPromiseResolver* resolver,
NativeIOErrorPtr error);
// Reject the `resolver` with the appropriate DOMException given `file_error`.
// When no `message` is provided, the default one is chosen. The resolver's
// execution context should be valid.
void RejectNativeIOWithError(ScriptPromiseResolver* resolver,
base::File::Error file_error,
const String& message = String());
// Throw with the appropriate DOMException given `error`.
void ThrowNativeIOWithError(ExceptionState& exception_state,
NativeIOErrorPtr error);
// Throw with the appropriate DOMException given `file_error`. When no `message`
// is provided, the standard one is chosen.
void ThrowNativeIOWithError(ExceptionState& exception_state,
base::File::Error file_error,
const String& message = String());
NativeIOErrorPtr FileErrorToNativeIOError(base::File::Error file_error,
const String& message);
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_MODULES_NATIVE_IO_NATIVE_IO_ERROR_H_
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
#include "base/numerics/safe_conversions.h" #include "base/numerics/safe_conversions.h"
#include "base/sequenced_task_runner.h" #include "base/sequenced_task_runner.h"
#include "base/task/thread_pool.h" #include "base/task/thread_pool.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h" #include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h" #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
...@@ -21,6 +22,7 @@ ...@@ -21,6 +22,7 @@
#include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h" #include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h"
#include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_state_observer.h" #include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_state_observer.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h" #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/native_io/native_io_error.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h" #include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_wrappable.h" #include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
...@@ -124,8 +126,10 @@ ScriptPromise NativeIOFile::getLength(ScriptState* script_state, ...@@ -124,8 +126,10 @@ ScriptPromise NativeIOFile::getLength(ScriptState* script_state,
return ScriptPromise(); return ScriptPromise();
} }
if (closed_) { if (closed_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise(); return ScriptPromise();
} }
io_pending_ = true; io_pending_ = true;
...@@ -158,8 +162,10 @@ ScriptPromise NativeIOFile::setLength(ScriptState* script_state, ...@@ -158,8 +162,10 @@ ScriptPromise NativeIOFile::setLength(ScriptState* script_state,
return ScriptPromise(); return ScriptPromise();
} }
if (closed_) { if (closed_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise(); return ScriptPromise();
} }
io_pending_ = true; io_pending_ = true;
...@@ -199,8 +205,10 @@ ScriptPromise NativeIOFile::read(ScriptState* script_state, ...@@ -199,8 +205,10 @@ ScriptPromise NativeIOFile::read(ScriptState* script_state,
return ScriptPromise(); return ScriptPromise();
} }
if (closed_) { if (closed_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise(); return ScriptPromise();
} }
io_pending_ = true; io_pending_ = true;
...@@ -249,8 +257,10 @@ ScriptPromise NativeIOFile::write(ScriptState* script_state, ...@@ -249,8 +257,10 @@ ScriptPromise NativeIOFile::write(ScriptState* script_state,
return ScriptPromise(); return ScriptPromise();
} }
if (closed_) { if (closed_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise(); return ScriptPromise();
} }
io_pending_ = true; io_pending_ = true;
...@@ -294,8 +304,10 @@ ScriptPromise NativeIOFile::flush(ScriptState* script_state, ...@@ -294,8 +304,10 @@ ScriptPromise NativeIOFile::flush(ScriptState* script_state,
return ScriptPromise(); return ScriptPromise();
} }
if (closed_) { if (closed_) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise(); return ScriptPromise();
} }
io_pending_ = true; io_pending_ = true;
...@@ -391,16 +403,15 @@ void NativeIOFile::DoGetLength( ...@@ -391,16 +403,15 @@ void NativeIOFile::DoGetLength(
NativeIOFile::FileState* file_state, NativeIOFile::FileState* file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) { scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread"; DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
base::File::Error get_length_error = base::File::FILE_OK; base::File::Error get_length_error;
int64_t length = -1; int64_t length = -1;
{ {
WTF::MutexLocker mutex_locker(file_state->mutex); WTF::MutexLocker mutex_locker(file_state->mutex);
DCHECK(file_state->file.IsValid()) DCHECK(file_state->file.IsValid())
<< "file I/O operation queued after file closed"; << "file I/O operation queued after file closed";
length = file_state->file.GetLength(); length = file_state->file.GetLength();
if (length < 0) { get_length_error = (length < 0) ? file_state->file.GetLastFileError()
get_length_error = file_state->file.GetLastFileError(); : base::File::FILE_OK;
}
} }
PostCrossThreadTask( PostCrossThreadTask(
...@@ -425,14 +436,12 @@ void NativeIOFile::DidGetLength( ...@@ -425,14 +436,12 @@ void NativeIOFile::DidGetLength(
DispatchQueuedClose(); DispatchQueuedClose();
if (length < 0) { if (length < 0) {
DCHECK(get_length_error != base::File::FILE_OK) DCHECK_NE(get_length_error, base::File::FILE_OK)
<< "Negative length reported with no error set"; << "Negative length reported with no error set";
resolver->Reject(V8ThrowDOMException::CreateOrEmpty( blink::RejectNativeIOWithError(resolver, get_length_error);
script_state->GetIsolate(), DOMExceptionCode::kOperationError,
"getLength() failed"));
return; return;
} }
DCHECK(get_length_error == base::File::FILE_OK) DCHECK_EQ(get_length_error, base::File::FILE_OK)
<< "File error reported when length is nonnegative"; << "File error reported when length is nonnegative";
// getLength returns an unsigned integer, which is different from e.g., // getLength returns an unsigned integer, which is different from e.g.,
// base::File and POSIX. The uses for negative integers are error handling, // base::File and POSIX. The uses for negative integers are error handling,
...@@ -441,9 +450,10 @@ void NativeIOFile::DidGetLength( ...@@ -441,9 +450,10 @@ void NativeIOFile::DidGetLength(
resolver->Resolve(length); resolver->Resolve(length);
} }
void NativeIOFile::DidSetLength(ScriptPromiseResolver* resolver, void NativeIOFile::DidSetLength(
bool backend_success, ScriptPromiseResolver* resolver,
base::File backing_file) { base::File backing_file,
mojom::blink::NativeIOErrorPtr set_length_result) {
DCHECK(backing_file.IsValid()) << "browser returned closed file"; DCHECK(backing_file.IsValid()) << "browser returned closed file";
{ {
WTF::MutexLocker locker(file_state_->mutex); WTF::MutexLocker locker(file_state_->mutex);
...@@ -458,10 +468,8 @@ void NativeIOFile::DidSetLength(ScriptPromiseResolver* resolver, ...@@ -458,10 +468,8 @@ void NativeIOFile::DidSetLength(ScriptPromiseResolver* resolver,
return; return;
ScriptState::Scope scope(script_state); ScriptState::Scope scope(script_state);
if (!backend_success) { if (set_length_result->type != mojom::blink::NativeIOErrorType::kSuccess) {
resolver->Reject(V8ThrowDOMException::CreateOrEmpty( blink::RejectNativeIOWithError(resolver, std::move(set_length_result));
script_state->GetIsolate(), DOMExceptionCode::kUnknownError,
"setLength() failed"));
return; return;
} }
...@@ -512,11 +520,13 @@ void NativeIOFile::DidRead( ...@@ -512,11 +520,13 @@ void NativeIOFile::DidRead(
DispatchQueuedClose(); DispatchQueuedClose();
if (read_bytes < 0) { if (read_bytes < 0) {
resolver->Reject(V8ThrowDOMException::CreateOrEmpty( DCHECK_NE(read_error, base::File::FILE_OK)
script_state->GetIsolate(), DOMExceptionCode::kOperationError, << "Negative bytes read reported with no error set";
"read() failed")); blink::RejectNativeIOWithError(resolver, read_error);
return; return;
} }
DCHECK_EQ(read_error, base::File::FILE_OK)
<< "Error set but positive number of bytes read.";
resolver->Resolve(read_bytes); resolver->Resolve(read_bytes);
} }
...@@ -564,11 +574,13 @@ void NativeIOFile::DidWrite( ...@@ -564,11 +574,13 @@ void NativeIOFile::DidWrite(
DispatchQueuedClose(); DispatchQueuedClose();
if (written_bytes < 0) { if (written_bytes < 0) {
resolver->Reject(V8ThrowDOMException::CreateOrEmpty( DCHECK_NE(write_error, base::File::FILE_OK)
script_state->GetIsolate(), DOMExceptionCode::kOperationError, << "Negative bytes written reported with no error set";
"write() failed")); blink::RejectNativeIOWithError(resolver, write_error);
return; return;
} }
DCHECK_EQ(write_error, base::File::FILE_OK);
resolver->Resolve(written_bytes); resolver->Resolve(written_bytes);
} }
...@@ -579,23 +591,25 @@ void NativeIOFile::DoFlush( ...@@ -579,23 +591,25 @@ void NativeIOFile::DoFlush(
NativeIOFile::FileState* file_state, NativeIOFile::FileState* file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) { scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread"; DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
bool success = false; base::File::Error flush_error;
{ {
WTF::MutexLocker mutex_locker(file_state->mutex); WTF::MutexLocker mutex_locker(file_state->mutex);
DCHECK(file_state->file.IsValid()) DCHECK(file_state->file.IsValid())
<< "file I/O operation queued after file closed"; << "file I/O operation queued after file closed";
success = file_state->file.Flush(); bool success = file_state->file.Flush();
flush_error =
success ? base::File::FILE_OK : file_state->file.GetLastFileError();
} }
PostCrossThreadTask( PostCrossThreadTask(
*resolver_task_runner, FROM_HERE, *resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidFlush, std::move(native_io_file), CrossThreadBindOnce(&NativeIOFile::DidFlush, std::move(native_io_file),
std::move(resolver), success)); std::move(resolver), flush_error));
} }
void NativeIOFile::DidFlush( void NativeIOFile::DidFlush(
CrossThreadPersistent<ScriptPromiseResolver> resolver, CrossThreadPersistent<ScriptPromiseResolver> resolver,
bool success) { base::File::Error flush_error) {
ScriptState* script_state = resolver->GetScriptState(); ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid()) if (!script_state->ContextIsValid())
return; return;
...@@ -606,10 +620,8 @@ void NativeIOFile::DidFlush( ...@@ -606,10 +620,8 @@ void NativeIOFile::DidFlush(
DispatchQueuedClose(); DispatchQueuedClose();
if (!success) { if (flush_error != base::File::FILE_OK) {
resolver->Reject(V8ThrowDOMException::CreateOrEmpty( blink::RejectNativeIOWithError(resolver, flush_error);
script_state->GetIsolate(), DOMExceptionCode::kOperationError,
"flush() failed"));
return; return;
} }
resolver->Resolve(); resolver->Resolve();
......
...@@ -98,8 +98,8 @@ class NativeIOFile final : public ScriptWrappable { ...@@ -98,8 +98,8 @@ class NativeIOFile final : public ScriptWrappable {
// Performs the post file I/O part of setLength(), on the main thread. // Performs the post file I/O part of setLength(), on the main thread.
void DidSetLength(ScriptPromiseResolver* resolver, void DidSetLength(ScriptPromiseResolver* resolver,
bool backend_success, base::File backing_file,
base::File backing_file); mojom::blink::NativeIOErrorPtr set_length_result);
// Performs the file I/O part of read(), off the main thread. // Performs the file I/O part of read(), off the main thread.
static void DoRead( static void DoRead(
...@@ -139,7 +139,7 @@ class NativeIOFile final : public ScriptWrappable { ...@@ -139,7 +139,7 @@ class NativeIOFile final : public ScriptWrappable {
scoped_refptr<base::SequencedTaskRunner> file_task_runner); scoped_refptr<base::SequencedTaskRunner> file_task_runner);
// Performs the post file-I/O part of flush(), on the main thread. // Performs the post file-I/O part of flush(), on the main thread.
void DidFlush(CrossThreadPersistent<ScriptPromiseResolver> resolver, void DidFlush(CrossThreadPersistent<ScriptPromiseResolver> resolver,
bool success); base::File::Error flush_error);
// Kicks off closing the file from the main thread. // Kicks off closing the file from the main thread.
void CloseBackingFile(); void CloseBackingFile();
......
...@@ -7,10 +7,12 @@ ...@@ -7,10 +7,12 @@
#include <limits> #include <limits>
#include "base/numerics/safe_conversions.h" #include "base/numerics/safe_conversions.h"
#include "third_party/blink/public/mojom/native_io/native_io.mojom-blink.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h" #include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h" #include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h"
#include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h" #include "third_party/blink/renderer/core/typed_arrays/array_buffer_view_helpers.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h" #include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer_view.h"
#include "third_party/blink/renderer/modules/native_io/native_io_error.h"
#include "third_party/blink/renderer/modules/native_io/native_io_file.h" #include "third_party/blink/renderer/modules/native_io/native_io_file.h"
#include "third_party/blink/renderer/platform/bindings/exception_code.h" #include "third_party/blink/renderer/platform/bindings/exception_code.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/bindings/exception_state.h"
...@@ -51,14 +53,15 @@ void NativeIOFileSync::close() { ...@@ -51,14 +53,15 @@ void NativeIOFileSync::close() {
uint64_t NativeIOFileSync::getLength(ExceptionState& exception_state) { uint64_t NativeIOFileSync::getLength(ExceptionState& exception_state) {
if (!backing_file_.IsValid()) { if (!backing_file_.IsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"NativeIOHost backend went away"));
return 0; return 0;
} }
int64_t length = backing_file_.GetLength(); int64_t length = backing_file_.GetLength();
if (length < 0) { if (length < 0) {
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError, ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
"getLength() failed");
return 0; return 0;
} }
// getLength returns an unsigned integer, which is different from e.g., // getLength returns an unsigned integer, which is different from e.g.,
...@@ -75,11 +78,13 @@ void NativeIOFileSync::setLength(uint64_t length, ...@@ -75,11 +78,13 @@ void NativeIOFileSync::setLength(uint64_t length,
return; return;
} }
if (!backing_file_.IsValid()) { if (!backing_file_.IsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"NativeIOHost backend went away"));
return; return;
} }
bool backend_success = false; mojom::blink::NativeIOErrorPtr set_length_result;
// Calls to setLength are routed through the browser process, see // Calls to setLength are routed through the browser process, see
// crbug.com/1084565. // crbug.com/1084565.
...@@ -87,11 +92,10 @@ void NativeIOFileSync::setLength(uint64_t length, ...@@ -87,11 +92,10 @@ void NativeIOFileSync::setLength(uint64_t length,
// We keep a single handle per file, so this handle is passed to the backend // We keep a single handle per file, so this handle is passed to the backend
// and is then given back to the renderer afterwards. // and is then given back to the renderer afterwards.
backend_file_->SetLength(base::as_signed(length), std::move(backing_file_), backend_file_->SetLength(base::as_signed(length), std::move(backing_file_),
&backend_success, &backing_file_); &backing_file_, &set_length_result);
DCHECK(backing_file_.IsValid()) << "browser returned closed file"; DCHECK(backing_file_.IsValid()) << "browser returned closed file";
if (!backend_success) { if (set_length_result->type != mojom::blink::NativeIOErrorType::kSuccess) {
exception_state.ThrowDOMException(DOMExceptionCode::kDataError, ThrowNativeIOWithError(exception_state, std::move(set_length_result));
"setLength() failed");
} }
return; return;
} }
...@@ -102,14 +106,16 @@ uint64_t NativeIOFileSync::read(MaybeShared<DOMArrayBufferView> buffer, ...@@ -102,14 +106,16 @@ uint64_t NativeIOFileSync::read(MaybeShared<DOMArrayBufferView> buffer,
int read_size = OperationSize(*buffer.View()); int read_size = OperationSize(*buffer.View());
char* read_data = static_cast<char*>(buffer.View()->BaseAddressMaybeShared()); char* read_data = static_cast<char*>(buffer.View()->BaseAddressMaybeShared());
if (!backing_file_.IsValid()) { if (!backing_file_.IsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return 0; return 0;
} }
int read_bytes = backing_file_.Read(file_offset, read_data, read_size); int read_bytes = backing_file_.Read(file_offset, read_data, read_size);
if (read_bytes < 0) { if (read_bytes < 0) {
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError, ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
"read() failed"); return 0;
} }
return base::as_unsigned(read_bytes); return base::as_unsigned(read_bytes);
} }
...@@ -121,14 +127,16 @@ uint64_t NativeIOFileSync::write(MaybeShared<DOMArrayBufferView> buffer, ...@@ -121,14 +127,16 @@ uint64_t NativeIOFileSync::write(MaybeShared<DOMArrayBufferView> buffer,
char* write_data = char* write_data =
static_cast<char*>(buffer.View()->BaseAddressMaybeShared()); static_cast<char*>(buffer.View()->BaseAddressMaybeShared());
if (!backing_file_.IsValid()) { if (!backing_file_.IsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return 0; return 0;
} }
int written_bytes = backing_file_.Write(file_offset, write_data, write_size); int written_bytes = backing_file_.Write(file_offset, write_data, write_size);
if (written_bytes < 0) { if (written_bytes < 0) {
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError, ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
"write() failed"); return 0;
} }
return base::as_unsigned(written_bytes); return base::as_unsigned(written_bytes);
} }
...@@ -137,14 +145,16 @@ void NativeIOFileSync::flush(ExceptionState& exception_state) { ...@@ -137,14 +145,16 @@ void NativeIOFileSync::flush(ExceptionState& exception_state) {
// This implementation of flush attempts to physically store the data it has // This implementation of flush attempts to physically store the data it has
// written on disk. This behaviour might change in the future. // written on disk. This behaviour might change in the future.
if (!backing_file_.IsValid()) { if (!backing_file_.IsValid()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, ThrowNativeIOWithError(exception_state,
"The file was already closed"); mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return; return;
} }
bool success = backing_file_.Flush(); bool success = backing_file_.Flush();
if (!success) { if (!success) {
exception_state.ThrowDOMException(DOMExceptionCode::kOperationError, ThrowNativeIOWithError(exception_state, backing_file_.GetLastFileError());
"flush() failed"); return;
} }
return; return;
} }
......
...@@ -18,3 +18,8 @@ promise_test(async testCase => { ...@@ -18,3 +18,8 @@ promise_test(async testCase => {
assert_equals(fileNames.indexOf('test_file'), -1); assert_equals(fileNames.indexOf('test_file'), -1);
}, 'nativeIO.getAll does not return file deleted by nativeIO.delete'); }, 'nativeIO.getAll does not return file deleted by nativeIO.delete');
promise_test(async testCase => {
await nativeIO.delete('test_file');
// Delete a second time if the file existed before the first delete.
await nativeIO.delete('test_file');
}, 'nativeIO.delete does not fail when deleting a non-existing file');
...@@ -17,3 +17,9 @@ test(testCase => { ...@@ -17,3 +17,9 @@ test(testCase => {
const fileNames = nativeIO.getAllSync(); const fileNames = nativeIO.getAllSync();
assert_equals(fileNames.indexOf('test_file'), -1); assert_equals(fileNames.indexOf('test_file'), -1);
}, 'nativeIO.getAllSync does not return file deleted by nativeIO.deleteSync'); }, 'nativeIO.getAllSync does not return file deleted by nativeIO.deleteSync');
test(testCase => {
nativeIO.deleteSync('test_file');
// Delete a second time if the file existed before the first delete.
nativeIO.deleteSync('test_file');
}, 'nativeIO.deleteSync does not fail when deleting a non-existing file');
...@@ -5,8 +5,7 @@ ...@@ -5,8 +5,7 @@
'use strict'; 'use strict';
setup(async () => { setup(async () => {
assert_implements(nativeIO.rename, assert_implements(nativeIO.rename, 'nativeIO.rename is not implemented.');
"nativeIO.rename is not implemented.");
}); });
promise_test(async testCase => { promise_test(async testCase => {
...@@ -29,8 +28,9 @@ promise_test(async testCase => { ...@@ -29,8 +28,9 @@ promise_test(async testCase => {
await file1.close(); await file1.close();
await file2.close(); await file2.close();
await promise_rejects_dom(testCase, "UnknownError", await promise_rejects_dom(
nativeIO.rename('test_file_1', 'test_file_2')); testCase, 'NoModificationAllowedError',
nativeIO.rename('test_file_1', 'test_file_2'));
const fileNamesAfterRename = await nativeIO.getAll(); const fileNamesAfterRename = await nativeIO.getAll();
assert_in_array('test_file_1', fileNamesAfterRename); assert_in_array('test_file_1', fileNamesAfterRename);
...@@ -53,10 +53,12 @@ promise_test(async testCase => { ...@@ -53,10 +53,12 @@ promise_test(async testCase => {
const readSharedArrayBuffer2 = new SharedArrayBuffer(writtenBytes2.length); const readSharedArrayBuffer2 = new SharedArrayBuffer(writtenBytes2.length);
const readBytes2 = new Uint8Array(readSharedArrayBuffer2); const readBytes2 = new Uint8Array(readSharedArrayBuffer2);
await file2_after.read(readBytes2, 0); await file2_after.read(readBytes2, 0);
assert_array_equals(readBytes1, writtenBytes1, assert_array_equals(
'the bytes read should match the bytes written'); readBytes1, writtenBytes1,
assert_array_equals(readBytes2, writtenBytes2, 'the bytes read should match the bytes written');
'the bytes read should match the bytes written'); assert_array_equals(
readBytes2, writtenBytes2,
'the bytes read should match the bytes written');
}, 'nativeIO.rename does not overwrite an existing file.'); }, 'nativeIO.rename does not overwrite an existing file.');
promise_test(async testCase => { promise_test(async testCase => {
...@@ -65,8 +67,9 @@ promise_test(async testCase => { ...@@ -65,8 +67,9 @@ promise_test(async testCase => {
await file.close(); await file.close();
await nativeIO.delete('test_file'); await nativeIO.delete('test_file');
}); });
await promise_rejects_dom(testCase, "UnknownError", await promise_rejects_dom(
nativeIO.rename('test_file', 'renamed_test_file')); testCase, 'NoModificationAllowedError',
nativeIO.rename('test_file', 'renamed_test_file'));
await file.close(); await file.close();
const fileNamesAfterRename = await nativeIO.getAll(); const fileNamesAfterRename = await nativeIO.getAll();
...@@ -86,10 +89,10 @@ promise_test(async testCase => { ...@@ -86,10 +89,10 @@ promise_test(async testCase => {
const file = await nativeIO.open('test_file'); const file = await nativeIO.open('test_file');
await file.close(); await file.close();
for (let name of kBadNativeIoNames) { for (let name of kBadNativeIoNames) {
await promise_rejects_js(testCase, TypeError, await promise_rejects_js(
nativeIO.rename('test_file', name)); testCase, TypeError, nativeIO.rename('test_file', name));
await promise_rejects_js(testCase, TypeError, await promise_rejects_js(
nativeIO.rename(name, 'test_file_2')); testCase, TypeError, nativeIO.rename(name, 'test_file_2'));
} }
}, 'nativeIO.rename does not allow renaming from or to invalid names.'); }, 'nativeIO.rename does not allow renaming from or to invalid names.');
...@@ -105,11 +108,13 @@ promise_test(async testCase => { ...@@ -105,11 +108,13 @@ promise_test(async testCase => {
}); });
// First rename fails, as source is still open. // First rename fails, as source is still open.
await promise_rejects_dom(testCase, "UnknownError", await promise_rejects_dom(
nativeIO.rename('opened_file', 'closed_file')); testCase, 'NoModificationAllowedError',
nativeIO.rename('opened_file', 'closed_file'));
// First rename fails again, as source has not been unlocked. // First rename fails again, as source has not been unlocked.
await promise_rejects_dom(testCase, "UnknownError", await promise_rejects_dom(
nativeIO.rename('opened_file', 'closed_file')); testCase, 'NoModificationAllowedError',
nativeIO.rename('opened_file', 'closed_file'));
}, 'Failed nativeIO.rename does not unlock the source.'); }, 'Failed nativeIO.rename does not unlock the source.');
promise_test(async testCase => { promise_test(async testCase => {
...@@ -124,9 +129,22 @@ promise_test(async testCase => { ...@@ -124,9 +129,22 @@ promise_test(async testCase => {
}); });
// First rename fails, as destination is still open. // First rename fails, as destination is still open.
await promise_rejects_dom(testCase, "UnknownError", await promise_rejects_dom(
nativeIO.rename('closed_file', 'opened_file')); testCase, 'NoModificationAllowedError',
nativeIO.rename('closed_file', 'opened_file'));
// First rename fails again, as destination has not been unlocked. // First rename fails again, as destination has not been unlocked.
await promise_rejects_dom(testCase, "UnknownError", await promise_rejects_dom(
nativeIO.rename('closed_file', 'opened_file')); testCase, 'NoModificationAllowedError',
nativeIO.rename('closed_file', 'opened_file'));
}, 'Failed nativeIO.rename does not unlock the destination.'); }, 'Failed nativeIO.rename does not unlock the destination.');
promise_test(async testCase => {
// Make sure that the file does not exist.
await nativeIO.delete('does_not_exist');
testCase.add_cleanup(async () => {
await nativeIO.delete('new_does_not_exist');
});
promise_rejects_dom(
testCase, 'NotFoundError',
nativeIO.rename('does_not_exist', 'new_does_not_exist'));
}, 'Renaming a non-existing file fails with a NotFoundError.');
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
setup(() => { setup(() => {
// Without this assertion, one test passes even if renameSync is not defined // Without this assertion, one test passes even if renameSync is not defined
assert_implements(nativeIO.renameSync, assert_implements(
"nativeIO.renameSync is not implemented."); nativeIO.renameSync, 'nativeIO.renameSync is not implemented.');
}); });
test(testCase => { test(testCase => {
...@@ -26,8 +26,9 @@ test(testCase => { ...@@ -26,8 +26,9 @@ test(testCase => {
file1.close(); file1.close();
file2.close(); file2.close();
assert_throws_dom("UnknownError", assert_throws_dom(
() => nativeIO.renameSync('test_file_1', 'test_file_2')); 'NoModificationAllowedError',
() => nativeIO.renameSync('test_file_1', 'test_file_2'));
const fileNamesAfterRename = nativeIO.getAllSync(); const fileNamesAfterRename = nativeIO.getAllSync();
assert_in_array('test_file_1', fileNamesAfterRename); assert_in_array('test_file_1', fileNamesAfterRename);
...@@ -45,12 +46,14 @@ test(testCase => { ...@@ -45,12 +46,14 @@ test(testCase => {
}); });
const readBytes1 = new Uint8Array(writtenBytes1.length); const readBytes1 = new Uint8Array(writtenBytes1.length);
file1_after.read(readBytes1, 0); file1_after.read(readBytes1, 0);
assert_array_equals(readBytes1, writtenBytes1, assert_array_equals(
'the bytes read should match the bytes written'); readBytes1, writtenBytes1,
'the bytes read should match the bytes written');
const readBytes2 = new Uint8Array(writtenBytes2.length); const readBytes2 = new Uint8Array(writtenBytes2.length);
file2_after.read(readBytes2, 0); file2_after.read(readBytes2, 0);
assert_array_equals(readBytes2, writtenBytes2, assert_array_equals(
'the bytes read should match the bytes written'); readBytes2, writtenBytes2,
'the bytes read should match the bytes written');
}, 'nativeIO.renameSync does not overwrite an existing file.'); }, 'nativeIO.renameSync does not overwrite an existing file.');
test(testCase => { test(testCase => {
...@@ -59,8 +62,9 @@ test(testCase => { ...@@ -59,8 +62,9 @@ test(testCase => {
file.close(); file.close();
nativeIO.deleteSync('test_file'); nativeIO.deleteSync('test_file');
}); });
assert_throws_dom("UnknownError", () => assert_throws_dom(
nativeIO.renameSync('test_file', 'renamed_test_file')); 'NoModificationAllowedError',
() => nativeIO.renameSync('test_file', 'renamed_test_file'));
file.close(); file.close();
const fileNamesAfterRename = nativeIO.getAllSync(); const fileNamesAfterRename = nativeIO.getAllSync();
...@@ -97,11 +101,13 @@ test(testCase => { ...@@ -97,11 +101,13 @@ test(testCase => {
}); });
// First rename fails, as source is still open. // First rename fails, as source is still open.
assert_throws_dom("UnknownError", assert_throws_dom(
() => nativeIO.renameSync('opened_file', 'closed_file')); 'NoModificationAllowedError',
() => nativeIO.renameSync('opened_file', 'closed_file'));
// First rename fails again, as source has not been unlocked. // First rename fails again, as source has not been unlocked.
assert_throws_dom("UnknownError", assert_throws_dom(
() => nativeIO.renameSync('opened_file', 'closed_file')); 'NoModificationAllowedError',
() => nativeIO.renameSync('opened_file', 'closed_file'));
}, 'Failed nativeIO.renameSync does not unlock the source.'); }, 'Failed nativeIO.renameSync does not unlock the source.');
test(testCase => { test(testCase => {
...@@ -116,9 +122,22 @@ test(testCase => { ...@@ -116,9 +122,22 @@ test(testCase => {
}); });
// First rename fails, as destination is still open. // First rename fails, as destination is still open.
assert_throws_dom("UnknownError", assert_throws_dom(
() => nativeIO.renameSync('closed_file', 'opened_file')); 'NoModificationAllowedError',
() => nativeIO.renameSync('closed_file', 'opened_file'));
// First rename fails again, as destination has not been unlocked. // First rename fails again, as destination has not been unlocked.
assert_throws_dom("UnknownError", assert_throws_dom(
() => nativeIO.renameSync('closed_file', 'opened_file')); 'NoModificationAllowedError',
() => nativeIO.renameSync('closed_file', 'opened_file'));
}, 'Failed nativeIO.renameSync does not unlock the destination.'); }, 'Failed nativeIO.renameSync does not unlock the destination.');
test(testCase => {
// Make sure that the file does not exist.
nativeIO.deleteSync('does_not_exist');
testCase.add_cleanup(() => {
nativeIO.deleteSync('new_name');
});
assert_throws_dom(
'NotFoundError',
() => nativeIO.renameSync('does_not_exist', 'new_name'));
}, 'Renaming a non-existing file fails with a NotFoundError.');
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment