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

[nativeio] NativeIO: Add Rename.

Bug: 914488
Change-Id: I81a96bc4f395d82d7a0c1d804eef25a861eb1fe5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2228152
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@{#780220}
parent f4b08231
......@@ -77,6 +77,18 @@ class NativeIOHostSync {
return names;
}
bool RenameFile(const std::string& old_name, const std::string& new_name) {
bool success = false;
base::RunLoop run_loop;
io_host_->RenameFile(old_name, new_name,
base::BindLambdaForTesting([&](bool backend_success) {
success = backend_success;
run_loop.Quit();
}));
run_loop.Run();
return success;
}
private:
blink::mojom::NativeIOHost* const io_host_;
};
......@@ -142,17 +154,18 @@ class NativeIOContextTest : public testing::Test {
mojo::Remote<blink::mojom::NativeIOHost> google_host_remote_;
std::unique_ptr<NativeIOHostSync> example_host_;
std::unique_ptr<NativeIOHostSync> google_host_;
};
TEST_F(NativeIOContextTest, OpenFile_BadNames) {
std::vector<std::string> bad_names = {
// Names disallowed by NativeIO
const std::vector<std::string> bad_names_ = {
"Uppercase",
"has-dash",
"has.dot",
"has/slash",
};
};
for (const std::string& bad_name : bad_names) {
TEST_F(NativeIOContextTest, OpenFile_BadNames) {
for (const std::string& bad_name : bad_names_) {
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<blink::mojom::NativeIOFileHost> file_host;
......@@ -199,14 +212,7 @@ TEST_F(NativeIOContextTest, OpenFile_SameName) {
}
TEST_F(NativeIOContextTest, DeleteFile_BadNames) {
std::vector<std::string> bad_names = {
"Uppercase",
"has-dash",
"has.dot",
"has/slash",
};
for (const std::string& bad_name : bad_names) {
for (const std::string& bad_name : bad_names_) {
mojo::test::BadMessageObserver bad_message_observer;
EXPECT_FALSE(example_host_->DeleteFile(bad_name));
......@@ -223,6 +229,30 @@ TEST_F(NativeIOContextTest, OpenFile_Locks_DeleteFile) {
EXPECT_FALSE(example_host_->DeleteFile("test_file"));
}
TEST_F(NativeIOContextTest, OpenFile_Locks_RenameFile) {
mojo::Remote<blink::mojom::NativeIOFileHost> file_host;
base::File file = example_host_->OpenFile(
"test_file_in_use", file_host.BindNewPipeAndPassReceiver());
EXPECT_TRUE(file.IsValid());
mojo::Remote<blink::mojom::NativeIOFileHost> file_host2;
base::File file2 = example_host_->OpenFile(
"test_file_closed", file_host2.BindNewPipeAndPassReceiver());
EXPECT_TRUE(file2.IsValid());
file2.Close();
NativeIOFileHostSync file_host2_sync(file_host2.get());
file_host2_sync.Close();
EXPECT_FALSE(
example_host_->RenameFile("test_file_in_use", "renamed_test_file"))
<< "An open file cannot be renamed";
EXPECT_FALSE(
example_host_->RenameFile("test_file_closed", "test_file_in_use"))
<< "An open file cannot be overwritten";
;
}
TEST_F(NativeIOContextTest, DeleteFile_WipesData) {
const std::string kTestData("Test Data");
......@@ -264,6 +294,39 @@ TEST_F(NativeIOContextTest, GetAllFiles_AfterOpen) {
EXPECT_EQ("test_file", file_names[0]);
}
TEST_F(NativeIOContextTest, RenameFile_AfterOpenAndRename) {
mojo::Remote<blink::mojom::NativeIOFileHost> file_host_remote;
base::File file = example_host_->OpenFile(
"test_file", file_host_remote.BindNewPipeAndPassReceiver());
file.Close();
NativeIOFileHostSync file_host(file_host_remote.get());
file_host.Close();
example_host_->RenameFile("test_file", "renamed_test_file");
std::vector<std::string> file_names = example_host_->GetAllFileNames();
EXPECT_EQ(1u, file_names.size());
EXPECT_EQ("renamed_test_file", file_names[0]);
}
TEST_F(NativeIOContextTest, RenameFile_BadNames) {
mojo::Remote<blink::mojom::NativeIOFileHost> file_host_remote;
base::File file = example_host_->OpenFile(
"test_file", file_host_remote.BindNewPipeAndPassReceiver());
file.Close();
NativeIOFileHostSync file_host(file_host_remote.get());
file_host.Close();
for (const std::string& bad_name : bad_names_) {
mojo::test::BadMessageObserver bad_message_observer;
EXPECT_FALSE(example_host_->RenameFile("test_file", bad_name));
EXPECT_EQ("Invalid file name", bad_message_observer.WaitForBadMessage());
EXPECT_FALSE(example_host_->RenameFile(bad_name, "inexistant_test_file"));
EXPECT_EQ("Invalid file name", bad_message_observer.WaitForBadMessage());
}
}
TEST_F(NativeIOContextTest, OriginIsolation) {
const std::string kTestData("Test Data");
......
......@@ -155,6 +155,27 @@ void DidGetAllFileNames(
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,
......@@ -255,6 +276,42 @@ void NativeIOHost::GetAllFileNames(GetAllFileNamesCallback callback) {
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);
......@@ -301,4 +358,19 @@ void NativeIOHost::DidDeleteFile(const std::string& name,
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
......@@ -67,6 +67,9 @@ class NativeIOHost : public blink::mojom::NativeIOHost {
void DeleteFile(const std::string& name,
DeleteFileCallback callback) override;
void GetAllFileNames(GetAllFileNamesCallback callback) override;
void RenameFile(const std::string& old_name,
const std::string& new_name,
RenameFileCallback callback) override;
// Called when one of the open files for this origin closes.
//
......@@ -90,6 +93,12 @@ class NativeIOHost : public blink::mojom::NativeIOHost {
DeleteFileCallback callback,
bool success);
// Called after the file I/O part of RenameFile() completed.
void DidRenameFile(const std::string& old_name,
const std::string& new_name,
RenameFileCallback callback,
bool success);
// The directory holding all the files for this origin.
const base::FilePath root_path_;
......
......@@ -66,5 +66,12 @@ interface NativeIOHost {
[Sync]
GetAllFileNames() => (bool success, array<string> names);
// Renames a previously created file.
//
// Rename does not allow renaming any files that are currently open and does
// not override existing files.
[Sync]
RenameFile(string old_name, string new_name) => (bool success);
// TODO(pwnall): Build quota integration before this API exits Dev Trials.
};
......@@ -104,6 +104,21 @@ void OnGetAllResult(ScriptPromiseResolver* resolver,
resolver->Resolve(file_names);
}
void OnRenameResult(ScriptPromiseResolver* resolver, bool backend_success) {
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
if (!backend_success) {
resolver->Reject(V8ThrowDOMException::CreateOrEmpty(
script_state->GetIsolate(), DOMExceptionCode::kUnknownError,
"rename() failed"));
return;
}
resolver->Resolve();
}
} // namespace
NativeIOManager::NativeIOManager(
......@@ -184,6 +199,27 @@ ScriptPromise NativeIOManager::getAll(ScriptState* script_state,
return resolver->Promise();
}
ScriptPromise NativeIOManager::rename(ScriptState* script_state,
String old_name,
String new_name,
ExceptionState& exception_state) {
if (!IsValidNativeIOName(old_name) || !IsValidNativeIOName(new_name)) {
exception_state.ThrowTypeError("Invalid file name");
return ScriptPromise();
}
if (!backend_.is_bound()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"NativeIOHost backend went away");
return ScriptPromise();
}
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
backend_->RenameFile(old_name, new_name,
WTF::Bind(&OnRenameResult, WrapPersistent(resolver)));
return resolver->Promise();
}
NativeIOFileSync* NativeIOManager::openSync(String name,
ExceptionState& exception_state) {
if (!IsValidNativeIOName(name)) {
......@@ -258,6 +294,30 @@ Vector<String> NativeIOManager::getAllSync(ExceptionState& exception_state) {
return result;
}
void NativeIOManager::renameSync(String old_name,
String new_name,
ExceptionState& exception_state) {
if (!IsValidNativeIOName(old_name) || !IsValidNativeIOName(new_name)) {
exception_state.ThrowTypeError("Invalid file name");
return;
}
if (!backend_.is_bound()) {
exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError,
"NativeIOHost backend went away");
return;
}
bool backend_success = false;
bool call_succeeded =
backend_->RenameFile(old_name, new_name, &backend_success);
if (!call_succeeded || !backend_success) {
exception_state.ThrowDOMException(DOMExceptionCode::kUnknownError,
"renameSync() failed");
}
}
void NativeIOManager::Trace(Visitor* visitor) const {
visitor->Trace(backend_);
ScriptWrappable::Trace(visitor);
......
......@@ -40,10 +40,15 @@ class NativeIOManager final : public ScriptWrappable,
ScriptPromise open(ScriptState*, String name, ExceptionState&);
ScriptPromise Delete(ScriptState*, String name, ExceptionState&);
ScriptPromise getAll(ScriptState*, ExceptionState&);
ScriptPromise rename(ScriptState*,
String old_name,
String new_name,
ExceptionState&);
NativeIOFileSync* openSync(String name, ExceptionState&);
void deleteSync(String name, ExceptionState&);
Vector<String> getAllSync(ExceptionState&);
void renameSync(String old_name, String new_name, ExceptionState&);
// GarbageCollected
void Trace(Visitor* visitor) const override;
......
......@@ -25,4 +25,9 @@
CallWith=ScriptState, RaisesException
] Promise<sequence<DOMString>> getAll();
[Exposed=DedicatedWorker, RaisesException] sequence<DOMString> getAllSync();
[
CallWith=ScriptState, RaisesException
] Promise<void> rename(DOMString old_name, DOMString new_name);
[Exposed=DedicatedWorker, RaisesException] void renameSync(DOMString old_name, DOMString new_name);
};
// META: title=NativeIO API: File renaming is reflected in listing.
// META: global=window,worker
'use strict';
promise_test(async testCase => {
const file = await nativeIO.open('test_file');
await file.close();
const fileNamesBeforeRename = await nativeIO.getAll();
assert_in_array('test_file', fileNamesBeforeRename);
await nativeIO.rename('test_file', 'renamed_test_file');
testCase.add_cleanup(async () => {
await nativeIO.delete('test_file');
await nativeIO.delete('renamed_test_file');
});
const fileNamesAfterRename = await nativeIO.getAll();
assert_false(fileNamesAfterRename.includes('test_file'));
assert_in_array('renamed_test_file', fileNamesAfterRename);
}, 'nativeIO.getAll returns a file renamed by nativeIO.rename' +
' with its new name.');
// META: title=NativeIO API: Failures of rename are properly handled.
// META: global=window,worker
// META: script=resources/support.js
'use strict';
promise_test(async testCase => {
const file1 = await nativeIO.open('test_file_1');
const file2 = await nativeIO.open('test_file_2');
testCase.add_cleanup(async () => {
await file1.close();
await file2.close();
});
const writeSharedArrayBuffer1 = new SharedArrayBuffer(4);
const writtenBytes1 = new Uint8Array(writeSharedArrayBuffer1);
writtenBytes1.set([64, 65, 66, 67]);
const writeSharedArrayBuffer2 = new SharedArrayBuffer(4);
const writtenBytes2 = new Uint8Array(writeSharedArrayBuffer2);
writtenBytes2.set([96, 97, 98, 99]);
await file1.write(writtenBytes1, 0);
await file2.write(writtenBytes2, 0);
await file1.close();
await file2.close();
await promise_rejects_dom(testCase, "UnknownError",
nativeIO.rename('test_file_1', 'test_file_2'));
const fileNamesAfterRename = await nativeIO.getAll();
assert_in_array('test_file_1', fileNamesAfterRename);
assert_in_array('test_file_2', fileNamesAfterRename);
// Make sure that a failed rename does not modify file contents.
const file1_after = await nativeIO.open('test_file_1');
const file2_after = await nativeIO.open('test_file_2');
testCase.add_cleanup(async () => {
await file1_after.close();
await file2_after.close();
await nativeIO.delete('test_file_1');
await nativeIO.delete('test_file_2');
});
const readSharedArrayBuffer1 = new SharedArrayBuffer(writtenBytes1.length);
const readBytes1 = new Uint8Array(readSharedArrayBuffer1);
await file1_after.read(readBytes1, 0);
const readSharedArrayBuffer2 = new SharedArrayBuffer(writtenBytes2.length);
const readBytes2 = new Uint8Array(readSharedArrayBuffer2);
await file2_after.read(readBytes2, 0);
assert_array_equals(readBytes1, writtenBytes1,
'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.');
promise_test(async testCase => {
const file = await nativeIO.open('test_file');
testCase.add_cleanup(async () => {
await file.close();
await nativeIO.delete('test_file');
});
await promise_rejects_dom(testCase, "UnknownError",
nativeIO.rename('test_file', 'renamed_test_file'));
await file.close();
const fileNamesAfterRename = await nativeIO.getAll();
assert_false(fileNamesAfterRename.includes('renamed_test_file'));
assert_in_array('test_file', fileNamesAfterRename);
}, 'nativeIO.rename does not allow renaming an open file.');
promise_test(async testCase => {
testCase.add_cleanup(async () => {
await file.close();
await nativeIO.delete('test_file');
for (let name of await nativeIO.getAll()) {
await nativeIO.delete(name);
}
});
const file = await nativeIO.open('test_file');
await file.close();
for (let name of kBadNativeIoNames) {
await promise_rejects_js(testCase, TypeError,
nativeIO.rename('test_file', name));
await promise_rejects_js(testCase, TypeError,
nativeIO.rename(name, 'test_file_2'));
}
}, 'nativeIO.rename does not allow renaming from or to invalid names.');
promise_test(async testCase => {
const closed_file = await nativeIO.open('closed_file');
closed_file.close();
const opened_file = await nativeIO.open('opened_file');
testCase.add_cleanup(async () => {
closed_file.close();
opened_file.close();
await nativeIO.delete('closed_file');
await nativeIO.delete('opened_file');
});
// First rename fails, as source is still open.
await promise_rejects_dom(testCase, "UnknownError",
nativeIO.rename('opened_file', 'closed_file'));
// First rename fails again, as source has not been unlocked.
await promise_rejects_dom(testCase, "UnknownError",
nativeIO.rename('opened_file', 'closed_file'));
}, 'Failed nativeIO.rename does not unlock the source.');
promise_test(async testCase => {
const closed_file = await nativeIO.open('closed_file');
await closed_file.close();
const opened_file = await nativeIO.open('opened_file');
testCase.add_cleanup(async () => {
await closed_file.close();
await opened_file.close();
await nativeIO.delete('closed_file');
await nativeIO.delete('opened_file');
});
// First rename fails, as destination is still open.
await promise_rejects_dom(testCase, "UnknownError",
nativeIO.rename('closed_file', 'opened_file'));
// First rename fails again, as destination has not been unlocked.
await promise_rejects_dom(testCase, "UnknownError",
nativeIO.rename('closed_file', 'opened_file'));
}, 'Failed nativeIO.rename does not unlock the destination.');
// META: title=Synchronous NativeIO API: File renaming is reflected in listing.
// META: global=dedicatedworker
'use strict';
test(testCase => {
const file = nativeIO.openSync('test_file');
file.close();
const fileNamesBeforeRename = nativeIO.getAllSync();
assert_in_array('test_file', fileNamesBeforeRename);
nativeIO.renameSync('test_file', 'renamed_test_file');
testCase.add_cleanup(() => {
file.close();
nativeIO.deleteSync('test_file');
nativeIO.deleteSync('renamed_test_file');
});
const fileNamesAfterRename = nativeIO.getAllSync();
assert_equals(fileNamesAfterRename.indexOf('test_file'), -1);
assert_in_array('renamed_test_file', fileNamesAfterRename);
}, 'nativeIO.getAllSync returns a file renamed' +
' by nativeIOFile.renameSync with its new name.');
// META: title=Synchronous NativeIO API: Failures of rename are properly handled.
// META: global=dedicatedworker
// META: script=resources/support.js
'use strict';
test(testCase => {
const file1 = nativeIO.openSync('test_file_1');
const file2 = nativeIO.openSync('test_file_2');
testCase.add_cleanup(() => {
file1.close();
file2.close();
});
const writtenBytes1 = Uint8Array.from([64, 65, 66, 67]);
file1.write(writtenBytes1, 0);
const writtenBytes2 = Uint8Array.from([96, 97, 98, 99]);
file2.write(writtenBytes2, 0);
file1.close();
file2.close();
assert_throws_dom("UnknownError",
() => nativeIO.renameSync('test_file_1', 'test_file_2'));
const fileNamesAfterRename = nativeIO.getAllSync();
assert_in_array('test_file_1', fileNamesAfterRename);
assert_in_array('test_file_2', fileNamesAfterRename);
// Make sure that a failed rename does not modify file contents.
const file1_after = nativeIO.openSync('test_file_1');
const file2_after = nativeIO.openSync('test_file_2');
testCase.add_cleanup(() => {
file1_after.close();
file2_after.close();
nativeIO.deleteSync('test_file_1');
nativeIO.deleteSync('test_file_2');
});
const readBytes1 = new Uint8Array(writtenBytes1.length);
file1_after.read(readBytes1, 0);
assert_array_equals(readBytes1, writtenBytes1,
'the bytes read should match the bytes written');
const readBytes2 = new Uint8Array(writtenBytes2.length);
file2_after.read(readBytes2, 0);
assert_array_equals(readBytes2, writtenBytes2,
'the bytes read should match the bytes written');
}, 'nativeIO.renameSync does not overwrite an existing file.');
test(testCase => {
const file = nativeIO.openSync('test_file');
testCase.add_cleanup(() => {
file.close();
nativeIO.deleteSync('test_file');
});
assert_throws_dom("UnknownError", () =>
nativeIO.renameSync('test_file', 'renamed_test_file'));
file.close();
const fileNamesAfterRename = nativeIO.getAllSync();
assert_equals(fileNamesAfterRename.indexOf('renamed_test_file'), -1);
assert_in_array('test_file', fileNamesAfterRename);
}, 'nativeIO.renameSync allows renaming an open file.');
test(testCase => {
testCase.add_cleanup(() => {
file.close();
nativeIO.deleteSync('test_file');
for (let name of nativeIO.getAllSync()) {
nativeIO.deleteSync(name);
}
});
const file = nativeIO.openSync('test_file');
file.close();
for (let name of kBadNativeIoNames) {
assert_throws_js(TypeError, () => nativeIO.renameSync('test_file', name));
assert_throws_js(TypeError, () => nativeIO.renameSync(name, 'test_file_2'));
}
}, 'nativeIO.renameSync does not allow renaming from or to invalid names.');
test(testCase => {
const closed_file = nativeIO.openSync('closed_file');
closed_file.close();
const opened_file = nativeIO.openSync('opened_file');
testCase.add_cleanup(() => {
closed_file.close();
opened_file.close();
nativeIO.deleteSync('closed_file');
nativeIO.deleteSync('opened_file');
});
// First rename fails, as source is still open.
assert_throws_dom("UnknownError",
() => nativeIO.renameSync('opened_file', 'closed_file'));
// First rename fails again, as source has not been unlocked.
assert_throws_dom("UnknownError",
() => nativeIO.renameSync('opened_file', 'closed_file'));
}, 'Failed nativeIO.renameSync does not unlock the source.');
test(testCase => {
const closed_file = nativeIO.openSync('closed_file');
closed_file.close();
const opened_file = nativeIO.openSync('opened_file');
testCase.add_cleanup(() => {
closed_file.close();
opened_file.close();
nativeIO.deleteSync('closed_file');
nativeIO.deleteSync('opened_file');
});
// First rename fails, as destination is still open.
assert_throws_dom("UnknownError",
() => nativeIO.renameSync('closed_file', 'opened_file'));
// First rename fails again, as destination has not been unlocked.
assert_throws_dom("UnknownError",
() => nativeIO.renameSync('closed_file', 'opened_file'));
}, 'Failed nativeIO.renameSync does not unlock the destination.');
// Names disallowed by NativeIO
const kBadNativeIoNames = [
"Uppercase",
"has-dash",
"has.dot",
"has/slash",
];
......@@ -903,6 +903,7 @@ interface NativeIOManager
method delete
method getAll
method open
method rename
interface NavigationPreloadManager
attribute @@toStringTag
method constructor
......
......@@ -872,6 +872,8 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method getAllSync
[Worker] method open
[Worker] method openSync
[Worker] method rename
[Worker] method renameSync
[Worker] interface NavigationPreloadManager
[Worker] attribute @@toStringTag
[Worker] method constructor
......
......@@ -5406,6 +5406,7 @@ interface NativeIOManager
method delete
method getAll
method open
method rename
interface NavigationPreloadManager
attribute @@toStringTag
method constructor
......
......@@ -830,6 +830,7 @@ Starting worker: resources/global-interface-listing-worker.js
[Worker] method delete
[Worker] method getAll
[Worker] method open
[Worker] method rename
[Worker] interface NavigationPreloadManager
[Worker] attribute @@toStringTag
[Worker] method constructor
......
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