| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/modules/file_system_access/file_system_underlying_sink.h" |
| |
| #include "mojo/public/cpp/system/string_data_source.h" |
| #include "third_party/blink/public/common/blob/blob_utils.h" |
| #include "third_party/blink/renderer/bindings/core/v8/array_buffer_or_array_buffer_view_or_blob_or_usv_string.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_value.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h" |
| #include "third_party/blink/renderer/bindings/modules/v8/v8_write_params.h" |
| #include "third_party/blink/renderer/core/dom/dom_exception.h" |
| #include "third_party/blink/renderer/core/fileapi/blob.h" |
| #include "third_party/blink/renderer/modules/file_system_access/file_system_access_error.h" |
| #include "third_party/blink/renderer/modules/file_system_access/file_system_writable_file_stream.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/blob/blob_data.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h" |
| |
| namespace blink { |
| |
| FileSystemUnderlyingSink::FileSystemUnderlyingSink( |
| ExecutionContext* context, |
| mojo::PendingRemote<mojom::blink::FileSystemAccessFileWriter> writer_remote) |
| : writer_remote_(context) { |
| writer_remote_.Bind(std::move(writer_remote), |
| context->GetTaskRunner(TaskType::kMiscPlatformAPI)); |
| DCHECK(writer_remote_.is_bound()); |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::start( |
| ScriptState* script_state, |
| WritableStreamDefaultController* controller, |
| ExceptionState& exception_state) { |
| return ScriptPromise::CastUndefined(script_state); |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::write( |
| ScriptState* script_state, |
| ScriptValue chunk, |
| WritableStreamDefaultController* controller, |
| ExceptionState& exception_state) { |
| v8::Local<v8::Value> value = chunk.V8Value(); |
| |
| ArrayBufferOrArrayBufferViewOrBlobOrUSVStringOrWriteParams input; |
| V8ArrayBufferOrArrayBufferViewOrBlobOrUSVStringOrWriteParams::ToImpl( |
| script_state->GetIsolate(), value, input, |
| UnionTypeConversionMode::kNotNullable, exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| |
| if (input.IsNull()) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Cannot provide null object"); |
| return ScriptPromise(); |
| } |
| |
| if (input.IsWriteParams()) { |
| return HandleParams(script_state, std::move(*input.GetAsWriteParams()), |
| exception_state); |
| } |
| |
| ArrayBufferOrArrayBufferViewOrBlobOrUSVString write_data; |
| V8ArrayBufferOrArrayBufferViewOrBlobOrUSVString::ToImpl( |
| script_state->GetIsolate(), value, write_data, |
| UnionTypeConversionMode::kNotNullable, exception_state); |
| if (exception_state.HadException()) |
| return ScriptPromise(); |
| return WriteData(script_state, offset_, std::move(write_data), |
| exception_state); |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::close(ScriptState* script_state, |
| ExceptionState& exception_state) { |
| if (!writer_remote_.is_bound() || pending_operation_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Object reached an invalid state"); |
| return ScriptPromise(); |
| } |
| pending_operation_ = |
| MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise result = pending_operation_->Promise(); |
| writer_remote_->Close(WTF::Bind(&FileSystemUnderlyingSink::CloseComplete, |
| WrapPersistent(this))); |
| |
| return result; |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::abort(ScriptState* script_state, |
| ScriptValue reason, |
| ExceptionState& exception_state) { |
| // The specification guarantees that this will only be called after all |
| // pending writes have been aborted. Terminating the remote connection |
| // will ensure that the writes are not closed successfully. |
| if (writer_remote_.is_bound()) |
| writer_remote_.reset(); |
| return ScriptPromise::CastUndefined(script_state); |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::HandleParams( |
| ScriptState* script_state, |
| const WriteParams& params, |
| ExceptionState& exception_state) { |
| if (params.type() == "truncate") { |
| if (!params.hasSizeNonNull()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kSyntaxError, |
| "Invalid params passed. truncate requires a size argument"); |
| return ScriptPromise(); |
| } |
| return Truncate(script_state, params.sizeNonNull(), exception_state); |
| } |
| |
| if (params.type() == "seek") { |
| if (!params.hasPositionNonNull()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kSyntaxError, |
| "Invalid params passed. seek requires a position argument"); |
| return ScriptPromise(); |
| } |
| return Seek(script_state, params.positionNonNull(), exception_state); |
| } |
| |
| if (params.type() == "write") { |
| uint64_t position = |
| params.hasPositionNonNull() ? params.positionNonNull() : offset_; |
| if (!params.hasData()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kSyntaxError, |
| "Invalid params passed. write requires a data argument"); |
| return ScriptPromise(); |
| } |
| return WriteData(script_state, position, params.data(), exception_state); |
| } |
| |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Object reached an invalid state"); |
| return ScriptPromise(); |
| } |
| |
| namespace { |
| // Write operations generally consist of two separate operations, both of which |
| // can result in an error: |
| // 1) The data producer (be it a Blob or mojo::DataPipeProducer) writes data to |
| // a data pipe. |
| // 2) The browser side file writer implementation reads data from the data pipe, |
| // and writes this to the file. |
| // |
| // Both operations can report errors in either order, and we have to wait for |
| // both to report success before we can consider the combined write call to have |
| // succeeded. This helper class listens for both complete events and signals |
| // success when both succeeded, or an error when either operation failed. |
| // |
| // This class deletes itself after calling its callback. |
| class WriterHelper : public base::SupportsWeakPtr<WriterHelper> { |
| public: |
| explicit WriterHelper( |
| base::OnceCallback<void(mojom::blink::FileSystemAccessErrorPtr result, |
| uint64_t bytes_written)> callback) |
| : callback_(std::move(callback)) {} |
| virtual ~WriterHelper() = default; |
| |
| // This method is called in response to the mojom Write call. It reports the |
| // result of the write operation from the point of view of the file system API |
| // implementation. |
| void WriteComplete(mojom::blink::FileSystemAccessErrorPtr result, |
| uint64_t bytes_written) { |
| DCHECK(!write_result_); |
| write_result_ = std::move(result); |
| bytes_written_ = bytes_written; |
| MaybeCallCallbackAndDeleteThis(); |
| } |
| |
| // This method is called by renderer side code (in subclasses of this class) |
| // when we've finished producing data to be written. |
| void ProducerComplete(mojom::blink::FileSystemAccessErrorPtr result) { |
| DCHECK(!producer_result_); |
| producer_result_ = std::move(result); |
| MaybeCallCallbackAndDeleteThis(); |
| } |
| |
| private: |
| void MaybeCallCallbackAndDeleteThis() { |
| DCHECK(callback_); |
| |
| if (!producer_result_.is_null() && |
| producer_result_->status != mojom::blink::FileSystemAccessStatus::kOk) { |
| // Producing data failed, report that error. |
| std::move(callback_).Run(std::move(producer_result_), bytes_written_); |
| delete this; |
| return; |
| } |
| |
| if (!write_result_.is_null() && |
| write_result_->status != mojom::blink::FileSystemAccessStatus::kOk) { |
| // Writing failed, report that error. |
| std::move(callback_).Run(std::move(write_result_), bytes_written_); |
| delete this; |
| return; |
| } |
| |
| if (!producer_result_.is_null() && !write_result_.is_null()) { |
| // Both operations succeeded, report success. |
| std::move(callback_).Run(std::move(write_result_), bytes_written_); |
| delete this; |
| return; |
| } |
| |
| // Still waiting for the other operation to complete, so don't call the |
| // callback yet. |
| } |
| |
| base::OnceCallback<void(mojom::blink::FileSystemAccessErrorPtr result, |
| uint64_t bytes_written)> |
| callback_; |
| |
| mojom::blink::FileSystemAccessErrorPtr producer_result_; |
| mojom::blink::FileSystemAccessErrorPtr write_result_; |
| uint64_t bytes_written_ = 0; |
| }; |
| |
| // WriterHelper implementation that is used when data is being produced by a |
| // mojo::DataPipeProducer, generally because the data was passed in as an |
| // ArrayBuffer or String. |
| class StreamWriterHelper : public WriterHelper { |
| public: |
| StreamWriterHelper( |
| std::unique_ptr<mojo::DataPipeProducer> producer, |
| base::OnceCallback<void(mojom::blink::FileSystemAccessErrorPtr result, |
| uint64_t bytes_written)> callback) |
| : WriterHelper(std::move(callback)), producer_(std::move(producer)) {} |
| |
| void DataProducerComplete(MojoResult result) { |
| // Reset `producer_` to close the DataPipe. Without this the Write operation |
| // will never complete as it will keep waiting for more data. |
| producer_ = nullptr; |
| |
| if (result == MOJO_RESULT_OK) { |
| ProducerComplete(mojom::blink::FileSystemAccessError::New( |
| mojom::blink::FileSystemAccessStatus::kOk, base::File::FILE_OK, "")); |
| } else { |
| ProducerComplete(mojom::blink::FileSystemAccessError::New( |
| mojom::blink::FileSystemAccessStatus::kOperationAborted, |
| base::File::FILE_OK, "Failed to write data to data pipe")); |
| } |
| } |
| |
| private: |
| std::unique_ptr<mojo::DataPipeProducer> producer_; |
| }; |
| |
| // WriterHelper implementation that is used when data is being produced by a |
| // Blob. |
| class BlobWriterHelper : public mojom::blink::BlobReaderClient, |
| public WriterHelper { |
| public: |
| BlobWriterHelper( |
| mojo::PendingReceiver<mojom::blink::BlobReaderClient> receiver, |
| base::OnceCallback<void(mojom::blink::FileSystemAccessErrorPtr result, |
| uint64_t bytes_written)> callback) |
| : WriterHelper(std::move(callback)), |
| receiver_(this, std::move(receiver)) { |
| receiver_.set_disconnect_handler( |
| WTF::Bind(&BlobWriterHelper::OnDisconnect, WTF::Unretained(this))); |
| } |
| |
| // BlobReaderClient: |
| void OnCalculatedSize(uint64_t total_size, |
| uint64_t expected_content_size) override {} |
| void OnComplete(int32_t status, uint64_t data_length) override { |
| complete_called_ = true; |
| // This error conversion matches what FileReaderLoader does. Failing to read |
| // a blob using FileReader should result in the same exception type as |
| // failing to read a blob here. |
| if (status == net::OK) { |
| ProducerComplete(mojom::blink::FileSystemAccessError::New( |
| mojom::blink::FileSystemAccessStatus::kOk, base::File::FILE_OK, "")); |
| } else if (status == net::ERR_FILE_NOT_FOUND) { |
| ProducerComplete(mojom::blink::FileSystemAccessError::New( |
| mojom::blink::FileSystemAccessStatus::kFileError, |
| base::File::FILE_ERROR_NOT_FOUND, "")); |
| } else { |
| ProducerComplete(mojom::blink::FileSystemAccessError::New( |
| mojom::blink::FileSystemAccessStatus::kFileError, |
| base::File::FILE_ERROR_IO, "")); |
| } |
| } |
| |
| private: |
| void OnDisconnect() { |
| if (!complete_called_) { |
| // Disconnected without getting a read result, treat this as read failure. |
| ProducerComplete(mojom::blink::FileSystemAccessError::New( |
| mojom::blink::FileSystemAccessStatus::kOperationAborted, |
| base::File::FILE_OK, "Blob disconnected while reading")); |
| } |
| } |
| |
| mojo::Receiver<mojom::blink::BlobReaderClient> receiver_; |
| bool complete_called_ = false; |
| }; |
| |
| // Creates a mojo data pipe, where the capacity of the data pipe is derived from |
| // the provided `data_size`. Returns false and throws an exception if creating |
| // the data pipe failed. |
| bool CreateDataPipe(uint64_t data_size, |
| ExceptionState& exception_state, |
| mojo::ScopedDataPipeProducerHandle& producer, |
| mojo::ScopedDataPipeConsumerHandle& consumer) { |
| MojoCreateDataPipeOptions options; |
| options.struct_size = sizeof(MojoCreateDataPipeOptions); |
| options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE; |
| options.element_num_bytes = 1; |
| options.capacity_num_bytes = BlobUtils::GetDataPipeCapacity(data_size); |
| |
| MojoResult rv = CreateDataPipe(&options, producer, consumer); |
| if (rv != MOJO_RESULT_OK) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Failed to create datapipe"); |
| return false; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| ScriptPromise FileSystemUnderlyingSink::WriteData( |
| ScriptState* script_state, |
| uint64_t position, |
| const ArrayBufferOrArrayBufferViewOrBlobOrUSVString& data, |
| ExceptionState& exception_state) { |
| DCHECK(!data.IsNull()); |
| |
| if (!writer_remote_.is_bound() || pending_operation_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Object reached an invalid state"); |
| return ScriptPromise(); |
| } |
| |
| std::unique_ptr<mojo::DataPipeProducer::DataSource> data_source; |
| if (data.IsArrayBuffer()) { |
| DOMArrayBuffer* array_buffer = data.GetAsArrayBuffer(); |
| data_source = std::make_unique<mojo::StringDataSource>( |
| base::span<const char>(static_cast<const char*>(array_buffer->Data()), |
| array_buffer->ByteLength()), |
| mojo::StringDataSource::AsyncWritingMode:: |
| STRING_MAY_BE_INVALIDATED_BEFORE_COMPLETION); |
| } else if (data.IsArrayBufferView()) { |
| DOMArrayBufferView* array_buffer_view = data.GetAsArrayBufferView().Get(); |
| data_source = std::make_unique<mojo::StringDataSource>( |
| base::span<const char>( |
| static_cast<const char*>(array_buffer_view->BaseAddress()), |
| array_buffer_view->byteLength()), |
| mojo::StringDataSource::AsyncWritingMode:: |
| STRING_MAY_BE_INVALIDATED_BEFORE_COMPLETION); |
| } else if (data.IsUSVString()) { |
| data_source = std::make_unique<mojo::StringDataSource>( |
| StringUTF8Adaptor(data.GetAsUSVString()).AsStringPiece(), |
| mojo::StringDataSource::AsyncWritingMode:: |
| STRING_MAY_BE_INVALIDATED_BEFORE_COMPLETION); |
| } |
| |
| DCHECK(data_source || data.IsBlob()); |
| uint64_t data_size = |
| data_source ? data_source->GetLength() : data.GetAsBlob()->size(); |
| |
| mojo::ScopedDataPipeProducerHandle producer_handle; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| if (!CreateDataPipe(data_size, exception_state, producer_handle, |
| consumer_handle)) { |
| return ScriptPromise(); |
| } |
| |
| WriterHelper* helper; |
| if (data.IsBlob()) { |
| mojo::PendingRemote<mojom::blink::BlobReaderClient> reader_client; |
| helper = |
| new BlobWriterHelper(reader_client.InitWithNewPipeAndPassReceiver(), |
| WTF::Bind(&FileSystemUnderlyingSink::WriteComplete, |
| WrapPersistent(this))); |
| data.GetAsBlob()->GetBlobDataHandle()->ReadAll(std::move(producer_handle), |
| std::move(reader_client)); |
| } else { |
| auto producer = |
| std::make_unique<mojo::DataPipeProducer>(std::move(producer_handle)); |
| auto* producer_ptr = producer.get(); |
| helper = new StreamWriterHelper( |
| std::move(producer), WTF::Bind(&FileSystemUnderlyingSink::WriteComplete, |
| WrapPersistent(this))); |
| // Unretained is safe because the producer is owned by `helper`. |
| producer_ptr->Write( |
| std::move(data_source), |
| WTF::Bind(&StreamWriterHelper::DataProducerComplete, |
| WTF::Unretained(static_cast<StreamWriterHelper*>(helper)))); |
| } |
| |
| writer_remote_->Write( |
| position, std::move(consumer_handle), |
| WTF::Bind(&WriterHelper::WriteComplete, helper->AsWeakPtr())); |
| |
| pending_operation_ = |
| MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| return pending_operation_->Promise(); |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::Truncate( |
| ScriptState* script_state, |
| uint64_t size, |
| ExceptionState& exception_state) { |
| if (!writer_remote_.is_bound() || pending_operation_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Object reached an invalid state"); |
| return ScriptPromise(); |
| } |
| pending_operation_ = |
| MakeGarbageCollected<ScriptPromiseResolver>(script_state); |
| ScriptPromise result = pending_operation_->Promise(); |
| writer_remote_->Truncate( |
| size, WTF::Bind(&FileSystemUnderlyingSink::TruncateComplete, |
| WrapPersistent(this), size)); |
| return result; |
| } |
| |
| ScriptPromise FileSystemUnderlyingSink::Seek(ScriptState* script_state, |
| uint64_t offset, |
| ExceptionState& exception_state) { |
| if (!writer_remote_.is_bound() || pending_operation_) { |
| exception_state.ThrowDOMException(DOMExceptionCode::kInvalidStateError, |
| "Object reached an invalid state"); |
| return ScriptPromise(); |
| } |
| offset_ = offset; |
| return ScriptPromise::CastUndefined(script_state); |
| } |
| |
| void FileSystemUnderlyingSink::WriteComplete( |
| mojom::blink::FileSystemAccessErrorPtr result, |
| uint64_t bytes_written) { |
| DCHECK(pending_operation_); |
| file_system_access_error::ResolveOrReject(pending_operation_, *result); |
| pending_operation_ = nullptr; |
| |
| if (result->status == mojom::blink::FileSystemAccessStatus::kOk) { |
| // Advance offset. |
| offset_ += bytes_written; |
| } |
| } |
| |
| void FileSystemUnderlyingSink::TruncateComplete( |
| uint64_t to_size, |
| mojom::blink::FileSystemAccessErrorPtr result) { |
| DCHECK(pending_operation_); |
| file_system_access_error::ResolveOrReject(pending_operation_, *result); |
| pending_operation_ = nullptr; |
| |
| if (result->status == mojom::blink::FileSystemAccessStatus::kOk) { |
| // Set offset to smallest last set size so that a subsequent write is not |
| // out of bounds. |
| offset_ = to_size < offset_ ? to_size : offset_; |
| } |
| } |
| |
| void FileSystemUnderlyingSink::CloseComplete( |
| mojom::blink::FileSystemAccessErrorPtr result) { |
| DCHECK(pending_operation_); |
| file_system_access_error::ResolveOrReject(pending_operation_, *result); |
| pending_operation_ = nullptr; |
| // We close the mojo pipe because we intend this writable file stream to be |
| // discarded after close. Subsequent operations will fail. |
| writer_remote_.reset(); |
| } |
| |
| void FileSystemUnderlyingSink::Trace(Visitor* visitor) const { |
| ScriptWrappable::Trace(visitor); |
| UnderlyingSinkBase::Trace(visitor); |
| visitor->Trace(writer_remote_); |
| visitor->Trace(pending_operation_); |
| } |
| |
| } // namespace blink |