blob: 838be2930e182212a6f19428e1397c912b188efd [file] [log] [blame]
// 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_file.h"
#include <utility>
#include "base/check.h"
#include "base/files/file.h"
#include "base/location.h"
#include "base/memory/scoped_refptr.h"
#include "base/numerics/checked_math.h"
#include "base/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "build/build_config.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/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/v8_throw_dom_exception.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_native_io_read_result.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_native_io_write_result.h"
#include "third_party/blink/renderer/core/dom/dom_exception.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_state_observer.h"
#include "third_party/blink/renderer/core/typed_arrays/array_buffer/array_buffer_contents.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.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_utils.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/script_wrappable.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
#include "third_party/blink/renderer/platform/scheduler/public/worker_pool.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "third_party/blink/renderer/platform/wtf/threading_primitives.h"
#include "third_party/blink/renderer/platform/wtf/wtf.h"
#if defined(OS_MAC)
#include "base/mac/mac_util.h"
#endif
namespace blink {
// State and logic for performing file I/O off the JavaScript thread.
//
// Instances are allocated on the PartitionAlloc heap. Instances cannot be
// garbage-collected, because garbage collected heaps get deallocated when the
// underlying threads are terminated, and we need a guarantee that each
// instance remains alive while it is used by a thread performing file I/O.
//
// Instances are initially constructed on a Blink thread that executes
// JavaScript, which can be Blink's main thread, or a worker thread. Afterwards,
// instances are (mostly*) only accessed on dedicated threads that do blocking
// file I/O.
//
// Mostly*: On macOS < 10.15, SetLength() synchronously accesses FileState on
// the JavaScript thread. This could be fixed with extra thread hopping. We're
// not currently planning to invest in the fix.
class NativeIOFile::FileState
: public base::RefCountedThreadSafe<NativeIOFile::FileState> {
public:
explicit FileState(base::File file) : file_(std::move(file)) {
DCHECK(file_.IsValid());
}
FileState(const FileState&) = delete;
FileState& operator=(const FileState&) = delete;
~FileState() = default;
// Returns true until Close() is called. Returns false afterwards.
//
// On macOS < 10.15, returns false between a TakeFile() call and the
// corresponding SetFile() call.
bool IsValid() {
DCHECK(!IsMainThread());
WTF::MutexLocker locker(mutex_);
return file_.IsValid();
}
void Close() {
DCHECK(!IsMainThread());
WTF::MutexLocker locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
file_.Close();
}
// Returns {length, base::File::FILE_OK} in case of success.
// Returns {invalid number, error} in case of failure.
std::pair<int64_t, base::File::Error> GetLength() {
DCHECK(!IsMainThread());
WTF::MutexLocker mutex_locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
int64_t length = file_.GetLength();
base::File::Error error =
(length < 0) ? file_.GetLastFileError() : base::File::FILE_OK;
return {length, error};
}
// Returns {expected_length, base::File::FILE_OK} in case of success.
// Returns {actual file length, error} in case of failure.
std::pair<int64_t, base::File::Error> SetLength(int64_t expected_length) {
DCHECK(!IsMainThread());
DCHECK_GE(expected_length, 0);
WTF::MutexLocker mutex_locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
bool success = file_.SetLength(expected_length);
base::File::Error error =
success ? base::File::FILE_OK : file_.GetLastFileError();
int64_t actual_length = success ? expected_length : file_.GetLength();
return {actual_length, error};
}
#if defined(OS_MAC)
// Used to implement browser-side SetLength() on macOS < 10.15.
base::File TakeFile() {
WTF::MutexLocker mutex_locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
return std::move(file_);
}
// Used to implement browser-side SetLength() on macOS < 10.15.
void SetFile(base::File file) {
WTF::MutexLocker locker(mutex_);
DCHECK(!file_.IsValid()) << __func__ << " called on valid file";
file_ = std::move(file);
}
#endif // defined(OS_MAC)
// Returns {read byte count, base::File::FILE_OK} in case of success.
// Returns {invalid number, error} in case of failure.
std::pair<int, base::File::Error> Read(NativeIODataBuffer* buffer,
int64_t file_offset,
int read_size) {
DCHECK(!IsMainThread());
DCHECK(buffer);
DCHECK_GE(file_offset, 0);
DCHECK_GE(read_size, 0);
WTF::MutexLocker mutex_locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
int read_bytes = file_.Read(file_offset, buffer->Data(), read_size);
base::File::Error error =
(read_bytes < 0) ? file_.GetLastFileError() : base::File::FILE_OK;
return {read_bytes, error};
}
// Returns {0, write_size, base::File::FILE_OK} in case of success.
// Returns {actual file length, written bytes, base::File::OK} in case of a
// short write.
// Returns {actual file length, invalid number, error} in case of failure.
std::tuple<int64_t, int, base::File::Error> Write(NativeIODataBuffer* buffer,
int64_t file_offset,
int write_size) {
DCHECK(!IsMainThread());
DCHECK(buffer);
DCHECK_GE(file_offset, 0);
DCHECK_GE(write_size, 0);
WTF::MutexLocker mutex_locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
int written_bytes = file_.Write(file_offset, buffer->Data(), write_size);
base::File::Error error =
(written_bytes < 0) ? file_.GetLastFileError() : base::File::FILE_OK;
int64_t actual_file_length_on_failure = 0;
if (written_bytes < write_size || error != base::File::FILE_OK) {
actual_file_length_on_failure = file_.GetLength();
if (actual_file_length_on_failure < 0 && error != base::File::FILE_OK)
error = file_.GetLastFileError();
}
return {actual_file_length_on_failure, written_bytes, error};
}
base::File::Error Flush() {
DCHECK(!IsMainThread());
WTF::MutexLocker mutex_locker(mutex_);
DCHECK(file_.IsValid()) << __func__ << " called on invalid file";
bool success = file_.Flush();
return success ? base::File::FILE_OK : file_.GetLastFileError();
}
private:
// Lock coordinating cross-thread access to the state.
WTF::Mutex mutex_;
// The file on disk backing this NativeIOFile.
base::File file_ GUARDED_BY(mutex_);
};
NativeIOFile::NativeIOFile(
base::File backing_file,
int64_t backing_file_length,
HeapMojoRemote<mojom::blink::NativeIOFileHost> backend_file,
NativeIOCapacityTracker* capacity_tracker,
ExecutionContext* execution_context)
: file_length_(backing_file_length),
file_state_(base::MakeRefCounted<FileState>(std::move(backing_file))),
// TODO(pwnall): Get a dedicated queue when the specification matures.
resolver_task_runner_(
execution_context->GetTaskRunner(TaskType::kMiscPlatformAPI)),
backend_file_(std::move(backend_file)),
capacity_tracker_(capacity_tracker) {
DCHECK_GE(backing_file_length, 0);
DCHECK(capacity_tracker);
backend_file_.set_disconnect_handler(
WTF::Bind(&NativeIOFile::OnBackendDisconnect, WrapWeakPersistent(this)));
}
NativeIOFile::~NativeIOFile() {
// Needed to avoid having the FileState destructor close the file descriptor
// synchronously on the main thread.
CloseBackingFile();
}
ScriptPromise NativeIOFile::close(ScriptState* script_state) {
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
if (closed_) {
// close() is idempotent.
resolver->Resolve();
return resolver->Promise();
}
closed_ = true;
DCHECK(!queued_close_resolver_) << "Close logic kicked off twice";
queued_close_resolver_ = resolver;
if (!io_pending_) {
DCHECK(file_state_)
<< "file_state_ nulled out without setting closed_ or io_pending_";
// Pretend that a close() promise was queued behind an I/O operation, and
// the operation just finished. This is less logic than handling the
// non-queued case separately.
DispatchQueuedClose();
}
return resolver->Promise();
}
ScriptPromise NativeIOFile::getLength(ScriptState* script_state,
ExceptionState& exception_state) {
if (io_pending_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"Another I/O operation is in progress on the same file");
return ScriptPromise();
}
if (closed_) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise();
}
DCHECK(file_state_)
<< "file_state_ nulled out without setting closed_ or io_pending_";
io_pending_ = true;
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
worker_pool::PostTask(
FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(&DoGetLength, WrapCrossThreadPersistent(this),
WrapCrossThreadPersistent(resolver), file_state_,
resolver_task_runner_));
return resolver->Promise();
}
ScriptPromise NativeIOFile::setLength(ScriptState* script_state,
uint64_t new_length,
ExceptionState& exception_state) {
if (!base::IsValueInRangeForNumericType<int64_t>(new_length)) {
// TODO(rstz): Consider throwing QuotaExceededError here.
exception_state.ThrowTypeError("Quota exceeded.");
return ScriptPromise();
}
if (io_pending_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"Another I/O operation is in progress on the same file");
return ScriptPromise();
}
if (closed_) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise();
}
DCHECK(file_state_)
<< "file_state_ nulled out without setting closed_ or io_pending_";
int64_t expected_length = base::as_signed(new_length);
DCHECK_GE(expected_length, 0);
DCHECK_GE(file_length_, 0);
static_assert(0 - std::numeric_limits<int32_t>::max() >=
std::numeric_limits<int32_t>::min(),
"The `length_delta` computation below may overflow");
// Since both values are positive, the arithmetic will not overflow.
int64_t length_delta = expected_length - file_length_;
// The available capacity must be reduced before performing an I/O operation
// that increases the file length. The available capacity must not be
// reduced before performing an I/O operation that decreases the file
// length. The capacity tracker then reports at most
// the capacity that is actually available to the execution context. This
// prevents double-spending by concurrent I/O operations on different files.
if (length_delta > 0) {
if (!capacity_tracker_->ChangeAvailableCapacity(-length_delta)) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kNoSpace,
"No capacity available for this operation"));
return ScriptPromise();
}
file_length_ = expected_length;
}
io_pending_ = true;
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
#if defined(OS_MAC)
// On macOS < 10.15, a sandboxing limitation causes failures in ftruncate()
// syscalls issued from renderers. For this reason, base::File::SetLength()
// fails in the renderer. We work around this problem by calling ftruncate()
// in the browser process. See crbug.com/1084565.
if (!base::mac::IsAtLeastOS10_15()) {
// Our system has at most one handle to a file, so we can avoid reasoning
// through the implications of multiple handles pointing to the same file.
//
// To preserve this invariant, we pass this file's handle to the browser
// process during the SetLength() mojo call, and the browser passes it back
// when the call completes.
base::File file = file_state_->TakeFile();
backend_file_->SetLength(
expected_length, std::move(file),
WTF::Bind(&NativeIOFile::DidSetLengthIpc, WrapPersistent(this),
WrapPersistent(resolver)));
return resolver->Promise();
}
#endif // defined(OS_MAC)
worker_pool::PostTask(
FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(&DoSetLength, WrapCrossThreadPersistent(this),
WrapCrossThreadPersistent(resolver), file_state_,
resolver_task_runner_, expected_length));
return resolver->Promise();
}
ScriptPromise NativeIOFile::read(ScriptState* script_state,
NotShared<DOMArrayBufferView> buffer,
uint64_t file_offset,
ExceptionState& exception_state) {
if (io_pending_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"Another I/O operation is in progress on the same file");
return ScriptPromise();
}
if (closed_) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise();
}
DCHECK(file_state_)
<< "file_state_ nulled out without setting closed_ or io_pending_";
// TODO(pwnall): This assignment should move right before the
// worker_pool::PostTask() call.
//
// `io_pending_` should only be set to true when we know for sure we'll post a
// task that eventually results in getting `io_pending_` set back to false.
// Having `io_pending_` set to true in an early return case (rejecting with an
// exception) leaves the NativeIOFile "stuck" in a state where all future I/O
// method calls will reject.
io_pending_ = true;
int read_size = NativeIOOperationSize(*buffer);
std::unique_ptr<NativeIODataBuffer> result_buffer_data =
NativeIODataBuffer::Create(script_state, buffer);
if (!result_buffer_data) {
exception_state.ThrowTypeError("Could not transfer buffer");
return ScriptPromise();
}
DCHECK(result_buffer_data->IsValid());
DCHECK(buffer->IsDetached());
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
worker_pool::PostTask(
FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(&DoRead, WrapCrossThreadPersistent(this),
WrapCrossThreadPersistent(resolver), file_state_,
resolver_task_runner_, std::move(result_buffer_data),
file_offset, read_size));
return resolver->Promise();
}
ScriptPromise NativeIOFile::write(ScriptState* script_state,
NotShared<DOMArrayBufferView> buffer,
uint64_t file_offset,
ExceptionState& exception_state) {
if (io_pending_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"Another I/O operation is in progress on the same file");
return ScriptPromise();
}
if (closed_) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise();
}
DCHECK(file_state_)
<< "file_state_ nulled out without setting closed_ or io_pending_";
int write_size = NativeIOOperationSize(*buffer);
int64_t write_end_offset;
if (!base::CheckAdd(file_offset, write_size)
.AssignIfValid(&write_end_offset)) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kNoSpace,
"No capacity available for this operation"));
return ScriptPromise();
}
DCHECK_GE(write_end_offset, 0);
DCHECK_GE(file_length_, 0);
static_assert(0 - std::numeric_limits<int32_t>::max() >=
std::numeric_limits<int32_t>::min(),
"The `length_delta` computation below may overflow");
// Since both values are positive, the arithmetic will not overflow.
int64_t length_delta = write_end_offset - file_length_;
// The available capacity must be reduced before performing an I/O operation
// that increases the file length. This prevents double-spending by concurrent
// I/O operations on different files.
if (length_delta > 0) {
if (!capacity_tracker_->ChangeAvailableCapacity(-length_delta)) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kNoSpace,
"No capacity available for this operation"));
return ScriptPromise();
}
file_length_ = write_end_offset;
}
// TODO(pwnall): This assignment should move right before the
// worker_pool::PostTask() call.
//
// `io_pending_` should only be set to true when we know for sure we'll post a
// task that eventually results in getting `io_pending_` set back to false.
// Having `io_pending_` set to true in an early return case (rejecting with an
// exception) leaves the NativeIOFile "stuck" in a state where all future I/O
// method calls will reject.
io_pending_ = true;
std::unique_ptr<NativeIODataBuffer> result_buffer_data =
NativeIODataBuffer::Create(script_state, buffer);
if (!result_buffer_data) {
exception_state.ThrowTypeError("Could not transfer buffer");
return ScriptPromise();
}
DCHECK(result_buffer_data->IsValid());
DCHECK(buffer->IsDetached());
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
worker_pool::PostTask(
FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(&DoWrite, WrapCrossThreadPersistent(this),
WrapCrossThreadPersistent(resolver), file_state_,
resolver_task_runner_, std::move(result_buffer_data),
file_offset, write_size));
return resolver->Promise();
}
ScriptPromise NativeIOFile::flush(ScriptState* script_state,
ExceptionState& exception_state) {
// This implementation of flush attempts to physically store the data it has
// written on disk. This behaviour might change in the future in order to
// support more performant but less reliable persistency guarantees.
if (io_pending_) {
exception_state.ThrowDOMException(
DOMExceptionCode::kInvalidStateError,
"Another I/O operation is in progress on the same file");
return ScriptPromise();
}
if (closed_) {
ThrowNativeIOWithError(exception_state,
mojom::blink::NativeIOError::New(
mojom::blink::NativeIOErrorType::kInvalidState,
"The file was already closed"));
return ScriptPromise();
}
DCHECK(file_state_)
<< "file_state_ nulled out without setting closed_ or io_pending_";
io_pending_ = true;
auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
worker_pool::PostTask(
FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(&DoFlush, WrapCrossThreadPersistent(this),
WrapCrossThreadPersistent(resolver), file_state_,
resolver_task_runner_));
return resolver->Promise();
}
void NativeIOFile::Trace(Visitor* visitor) const {
ScriptWrappable::Trace(visitor);
visitor->Trace(queued_close_resolver_);
visitor->Trace(backend_file_);
visitor->Trace(capacity_tracker_);
}
void NativeIOFile::OnBackendDisconnect() {
backend_file_.reset();
CloseBackingFile();
}
void NativeIOFile::DispatchQueuedClose() {
DCHECK(!io_pending_)
<< "Dispatching close() concurrently with other I/O operations is racy";
if (!queued_close_resolver_)
return;
DCHECK(closed_) << "close() resolver queued without setting closed_";
ScriptPromiseResolver* resolver = queued_close_resolver_;
queued_close_resolver_ = nullptr;
scoped_refptr<FileState> file_state = std::move(file_state_);
DCHECK(!file_state_);
worker_pool::PostTask(
FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(&DoClose, WrapCrossThreadPersistent(this),
WrapCrossThreadPersistent(resolver),
std::move(file_state), resolver_task_runner_));
}
// static
void NativeIOFile::DoClose(
CrossThreadPersistent<NativeIOFile> native_io_file,
CrossThreadPersistent<ScriptPromiseResolver> resolver,
scoped_refptr<NativeIOFile::FileState> file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
DCHECK(file_state);
DCHECK(file_state->IsValid())
<< "File I/O operation queued after file closed";
DCHECK(resolver_task_runner);
file_state->Close();
PostCrossThreadTask(
*resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidClose, std::move(native_io_file),
std::move(resolver)));
}
void NativeIOFile::DidClose(
CrossThreadPersistent<ScriptPromiseResolver> resolver) {
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid()) {
// If the context was torn down, the backend is disconnecting or
// disconnected. No need to report that the file is closing.
return;
}
if (!backend_file_.is_bound()) {
// If the backend went away, no need to tell it that the file was closed.
resolver->Resolve();
return;
}
backend_file_->Close(
WTF::Bind([](ScriptPromiseResolver* resolver) { resolver->Resolve(); },
WrapPersistent(resolver.Get())));
}
// static
void NativeIOFile::DoGetLength(
CrossThreadPersistent<NativeIOFile> native_io_file,
CrossThreadPersistent<ScriptPromiseResolver> resolver,
scoped_refptr<NativeIOFile::FileState> file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
DCHECK(file_state);
DCHECK(file_state->IsValid())
<< "File I/O operation queued after file closed";
DCHECK(resolver_task_runner);
int64_t length;
base::File::Error get_length_error;
std::tie(length, get_length_error) = file_state->GetLength();
PostCrossThreadTask(
*resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidGetLength,
std::move(native_io_file), std::move(resolver),
length, get_length_error));
}
void NativeIOFile::DidGetLength(
CrossThreadPersistent<ScriptPromiseResolver> resolver,
int64_t length,
base::File::Error get_length_error) {
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
DCHECK(io_pending_) << "I/O operation performed without io_pending_ set";
if (get_length_error == base::File::FILE_OK) {
DCHECK_EQ(file_length_, length)
<< "`file_length_` is not an upper bound anymore";
}
io_pending_ = false;
DispatchQueuedClose();
if (length < 0) {
DCHECK_NE(get_length_error, base::File::FILE_OK)
<< "Negative length reported with no error set";
blink::RejectNativeIOWithError(resolver, get_length_error);
return;
}
DCHECK_EQ(get_length_error, base::File::FILE_OK)
<< "File error reported when length is nonnegative";
// getLength returns an unsigned integer, which is different from e.g.,
// base::File and POSIX. The uses for negative integers are error handling,
// which is done through exceptions, and seeking from an offset without type
// conversions, which is not supported by NativeIO.
resolver->Resolve(length);
}
// static
void NativeIOFile::DoSetLength(
CrossThreadPersistent<NativeIOFile> native_io_file,
CrossThreadPersistent<ScriptPromiseResolver> resolver,
scoped_refptr<NativeIOFile::FileState> file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner,
int64_t expected_length) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
DCHECK(file_state);
DCHECK(file_state->IsValid())
<< "File I/O operation queued after file closed";
DCHECK(resolver_task_runner);
DCHECK_GE(expected_length, 0);
int64_t actual_length;
base::File::Error set_length_error;
std::tie(actual_length, set_length_error) =
file_state->SetLength(expected_length);
PostCrossThreadTask(
*resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidSetLengthIo,
std::move(native_io_file), std::move(resolver),
actual_length, set_length_error));
}
void NativeIOFile::DidSetLengthIo(
CrossThreadPersistent<ScriptPromiseResolver> resolver,
int64_t actual_length,
base::File::Error set_length_error) {
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
DCHECK(io_pending_) << "I/O operation performed without io_pending_ set";
io_pending_ = false;
if (actual_length >= 0) {
DCHECK_LE(actual_length, file_length_)
<< "file_length_ should be an upper bound during I/O";
if (actual_length < file_length_) {
// For successful length decreases, this logic returns freed up
// capacity. For unsuccessful length increases, this logic returns
// unused capacity.
bool change_success = capacity_tracker_->ChangeAvailableCapacity(
file_length_ - actual_length);
DCHECK(change_success) << "Capacity increases should always succeed";
file_length_ = actual_length;
}
} else {
DCHECK(set_length_error != base::File::FILE_OK);
// base::File::SetLength() failed. Then, attempting to File::GetLength()
// failed as well. We don't have a reliable measure of the file's length,
// and the file descriptor is probably unusable. Force-closing the file
// without reclaiming any capacity minimizes the risk of overusing our
// allocation.
if (!closed_) {
closed_ = true;
queued_close_resolver_ =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
}
}
DispatchQueuedClose();
if (set_length_error != base::File::FILE_OK) {
RejectNativeIOWithError(resolver, set_length_error);
return;
}
resolver->Resolve();
}
#if defined(OS_MAC)
void NativeIOFile::DidSetLengthIpc(
ScriptPromiseResolver* resolver,
base::File backing_file,
int64_t actual_length,
mojom::blink::NativeIOErrorPtr set_length_error) {
DCHECK(backing_file.IsValid()) << "browser returned closed file";
file_state_->SetFile(std::move(backing_file));
ScriptState* script_state = resolver->GetScriptState();
DCHECK(io_pending_) << "I/O operation performed without io_pending_ set";
io_pending_ = false;
if (actual_length >= 0) {
DCHECK_LE(actual_length, file_length_)
<< "file_length_ should be an upper bound during I/O";
if (actual_length < file_length_) {
// For successful length decreases, this logic returns freed up
// capacity. For unsuccessful length increases, this logic returns
// unused capacity.
bool change_success = capacity_tracker_->ChangeAvailableCapacity(
file_length_ - actual_length);
DCHECK(change_success) << "Capacity increases should always succeed";
file_length_ = actual_length;
}
} else {
DCHECK(set_length_error->type != mojom::blink::NativeIOErrorType::kSuccess);
// base::File::SetLength() failed. Then, attempting to File::GetLength()
// failed as well. We don't have a reliable measure of the file's length,
// and the file descriptor is probably unusable. Force-closing the file
// without reclaiming any capacity minimizes the risk of overusing our
// allocation.
if (!closed_) {
closed_ = true;
queued_close_resolver_ =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
}
}
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
DispatchQueuedClose();
if (set_length_error->type != mojom::blink::NativeIOErrorType::kSuccess) {
blink::RejectNativeIOWithError(resolver, std::move(set_length_error));
return;
}
resolver->Resolve();
}
#endif // defined(OS_MAC)
// static
void NativeIOFile::DoRead(
CrossThreadPersistent<NativeIOFile> native_io_file,
CrossThreadPersistent<ScriptPromiseResolver> resolver,
scoped_refptr<NativeIOFile::FileState> file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner,
std::unique_ptr<NativeIODataBuffer> result_buffer_data,
uint64_t file_offset,
int read_size) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
DCHECK(file_state);
DCHECK(file_state->IsValid())
<< "File I/O operation queued after file closed";
DCHECK(resolver_task_runner);
DCHECK(result_buffer_data);
DCHECK(result_buffer_data->IsValid());
DCHECK_GE(read_size, 0);
#if DCHECK_IS_ON()
DCHECK_LE(static_cast<size_t>(read_size), result_buffer_data->DataLength());
#endif // DCHECK_IS_ON()
int read_bytes;
base::File::Error read_error;
std::tie(read_bytes, read_error) =
file_state->Read(result_buffer_data.get(), file_offset, read_size);
PostCrossThreadTask(
*resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidRead, std::move(native_io_file),
std::move(resolver), std::move(result_buffer_data),
read_bytes, read_error));
}
void NativeIOFile::DidRead(
CrossThreadPersistent<ScriptPromiseResolver> resolver,
std::unique_ptr<NativeIODataBuffer> result_buffer_data,
int read_bytes,
base::File::Error read_error) {
DCHECK(result_buffer_data);
DCHECK(result_buffer_data->IsValid());
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
DCHECK(io_pending_) << "I/O operation performed without io_pending_ set";
io_pending_ = false;
DispatchQueuedClose();
if (read_bytes < 0) {
DCHECK_NE(read_error, base::File::FILE_OK)
<< "Negative bytes read reported with no error set";
blink::RejectNativeIOWithError(resolver, read_error);
return;
}
DCHECK_EQ(read_error, base::File::FILE_OK)
<< "Error set but positive number of bytes read.";
NativeIOReadResult* read_result = MakeGarbageCollected<NativeIOReadResult>();
read_result->setBuffer(result_buffer_data->Take());
read_result->setReadBytes(read_bytes);
resolver->Resolve(read_result);
}
// static
void NativeIOFile::DoWrite(
CrossThreadPersistent<NativeIOFile> native_io_file,
CrossThreadPersistent<ScriptPromiseResolver> resolver,
scoped_refptr<NativeIOFile::FileState> file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner,
std::unique_ptr<NativeIODataBuffer> result_buffer_data,
uint64_t file_offset,
int write_size) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
DCHECK(file_state);
DCHECK(file_state->IsValid())
<< "File I/O operation queued after file closed";
DCHECK(resolver_task_runner);
DCHECK(result_buffer_data);
DCHECK(result_buffer_data->IsValid());
DCHECK_GE(write_size, 0);
#if DCHECK_IS_ON()
DCHECK_LE(static_cast<size_t>(write_size), result_buffer_data->DataLength());
#endif // DCHECK_IS_ON()
int written_bytes;
int64_t actual_file_length_on_failure = 0;
base::File::Error write_error;
std::tie(actual_file_length_on_failure, written_bytes, write_error) =
file_state->Write(result_buffer_data.get(), file_offset, write_size);
PostCrossThreadTask(
*resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidWrite, std::move(native_io_file),
std::move(resolver), std::move(result_buffer_data),
written_bytes, write_error, write_size,
actual_file_length_on_failure));
}
void NativeIOFile::DidWrite(
CrossThreadPersistent<ScriptPromiseResolver> resolver,
std::unique_ptr<NativeIODataBuffer> result_buffer_data,
int written_bytes,
base::File::Error write_error,
int write_size,
int64_t actual_file_length_on_failure) {
DCHECK(result_buffer_data);
DCHECK(result_buffer_data->IsValid());
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
DCHECK(io_pending_) << "I/O operation performed without io_pending_ set";
io_pending_ = false;
if (write_error != base::File::FILE_OK || written_bytes < write_size) {
if (actual_file_length_on_failure >= 0) {
DCHECK_LE(actual_file_length_on_failure, file_length_)
<< "file_length_ should be an upper bound during I/O";
if (actual_file_length_on_failure < file_length_) {
bool change_success = capacity_tracker_->ChangeAvailableCapacity(
file_length_ - actual_file_length_on_failure);
DCHECK(change_success) << "Capacity increases should always succeed";
file_length_ = actual_file_length_on_failure;
}
} else {
DCHECK(write_error != base::File::FILE_OK);
// base::File::Write() failed. Then, attempting to File::GetLength()
// failed as well. We don't have a reliable measure of the file's length,
// and the file descriptor is probably unusable. Force-closing the file
// without reclaiming any capacity minimizes the risk of overusing our
// allocation.
if (!closed_) {
closed_ = true;
queued_close_resolver_ =
MakeGarbageCollected<ScriptPromiseResolver>(script_state);
}
}
}
DispatchQueuedClose();
if (write_error != base::File::FILE_OK) {
blink::RejectNativeIOWithError(resolver, write_error);
return;
}
DCHECK_EQ(write_error, base::File::FILE_OK);
NativeIOWriteResult* write_result =
MakeGarbageCollected<NativeIOWriteResult>();
write_result->setBuffer(result_buffer_data->Take());
write_result->setWrittenBytes(written_bytes);
resolver->Resolve(write_result);
}
// static
void NativeIOFile::DoFlush(
CrossThreadPersistent<NativeIOFile> native_io_file,
CrossThreadPersistent<ScriptPromiseResolver> resolver,
scoped_refptr<NativeIOFile::FileState> file_state,
scoped_refptr<base::SequencedTaskRunner> resolver_task_runner) {
DCHECK(!IsMainThread()) << "File I/O should not happen on the main thread";
DCHECK(file_state);
DCHECK(file_state->IsValid())
<< "File I/O operation queued after file closed";
base::File::Error flush_error = file_state->Flush();
PostCrossThreadTask(
*resolver_task_runner, FROM_HERE,
CrossThreadBindOnce(&NativeIOFile::DidFlush, std::move(native_io_file),
std::move(resolver), flush_error));
}
void NativeIOFile::DidFlush(
CrossThreadPersistent<ScriptPromiseResolver> resolver,
base::File::Error flush_error) {
ScriptState* script_state = resolver->GetScriptState();
if (!script_state->ContextIsValid())
return;
ScriptState::Scope scope(script_state);
DCHECK(io_pending_) << "I/O operation performed without io_pending_ set";
io_pending_ = false;
DispatchQueuedClose();
if (flush_error != base::File::FILE_OK) {
blink::RejectNativeIOWithError(resolver, flush_error);
return;
}
resolver->Resolve();
}
void NativeIOFile::CloseBackingFile() {
closed_ = true;
if (!file_state_) {
// Avoid posting a cross-thread task if the file is already closed. This is
// the expected path.
return;
}
scoped_refptr<FileState> file_state = std::move(file_state_);
DCHECK(!file_state_);
worker_pool::PostTask(FROM_HERE, {base::MayBlock(), base::ThreadPool()},
CrossThreadBindOnce(
[](scoped_refptr<FileState> file_state) {
DCHECK(file_state);
file_state->Close();
},
std::move(file_state)));
}
} // namespace blink