/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "FileSystemWritableFileStream.h" #include "mozilla/Buffer.h" #include "mozilla/ErrorResult.h" #include "mozilla/dom/Blob.h" #include "mozilla/dom/FileSystemHandle.h" #include "mozilla/dom/FileSystemLog.h" #include "mozilla/dom/FileSystemManager.h" #include "mozilla/dom/FileSystemWritableFileStreamBinding.h" #include "mozilla/dom/FileSystemWritableFileStreamChild.h" #include "mozilla/dom/Promise.h" #include "mozilla/dom/WritableStreamDefaultController.h" #include "mozilla/dom/quota/QuotaCommon.h" #include "mozilla/dom/quota/ResultExtensions.h" #include "nsIInputStream.h" #include "nsNetUtil.h" #include "private/pprio.h" namespace mozilla::dom { namespace { class WritableFileStreamUnderlyingSinkAlgorithms final : public UnderlyingSinkAlgorithmsBase { NS_DECL_ISUPPORTS_INHERITED NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) explicit WritableFileStreamUnderlyingSinkAlgorithms( FileSystemWritableFileStream& aStream) : mStream(&aStream) {} // Streams algorithms void StartCallback(JSContext* aCx, WritableStreamDefaultController& aController, JS::MutableHandle aRetVal, ErrorResult& aRv) override; MOZ_CAN_RUN_SCRIPT already_AddRefed WriteCallback( JSContext* aCx, JS::Handle aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) override; MOZ_CAN_RUN_SCRIPT already_AddRefed CloseCallback( JSContext* aCx, ErrorResult& aRv) override; MOZ_CAN_RUN_SCRIPT already_AddRefed AbortCallback( JSContext* aCx, const Optional>& aReason, ErrorResult& aRv) override; void ReleaseObjects() override; private: ~WritableFileStreamUnderlyingSinkAlgorithms() = default; RefPtr mStream; }; /** * TODO: Duplicated from netwerk/cache2/CacheFileIOManager.cpp * Please remove after bug 1286601 is fixed, * https://bugzilla.mozilla.org/show_bug.cgi?id=1286601 */ nsresult TruncFile(PRFileDesc* aFD, int64_t aEOF) { #if defined(XP_UNIX) if (ftruncate(PR_FileDesc2NativeHandle(aFD), aEOF) != 0) { NS_ERROR("ftruncate failed"); return NS_ERROR_FAILURE; } #elif defined(XP_WIN) const int64_t currentOffset = PR_Seek64(aFD, 0, PR_SEEK_CUR); if (currentOffset == -1) { return NS_ERROR_FAILURE; } int64_t cnt = PR_Seek64(aFD, aEOF, PR_SEEK_SET); if (cnt == -1) { return NS_ERROR_FAILURE; } if (!SetEndOfFile((HANDLE)PR_FileDesc2NativeHandle(aFD))) { NS_ERROR("SetEndOfFile failed"); return NS_ERROR_FAILURE; } if (PR_Seek64(aFD, currentOffset, PR_SEEK_SET) == -1) { NS_ERROR("Restoring seek offset failed"); return NS_ERROR_FAILURE; } #else MOZ_ASSERT(false, "Not implemented!"); return NS_ERROR_NOT_IMPLEMENTED; #endif return NS_OK; } } // namespace FileSystemWritableFileStream::FileSystemWritableFileStream( nsIGlobalObject* aGlobal, RefPtr& aManager, RefPtr aActor, const ::mozilla::ipc::FileDescriptor& aFileDescriptor, const fs::FileSystemEntryMetadata& aMetadata) : WritableStream(aGlobal, HoldDropJSObjectsCaller::Explicit), mManager(aManager), mActor(std::move(aActor)), mFileDesc(nullptr), mMetadata(aMetadata), mClosed(false) { auto rawFD = aFileDescriptor.ClonePlatformHandle(); mFileDesc = PR_ImportFile(PROsfd(rawFD.release())); LOG(("Created WritableFileStream %p for fd %p", this, mFileDesc)); // Connect with the actor directly in the constructor. This way the actor // can call `FileSystemWritableFileStream::ClearActor` when we call // `PFileSystemWritableFileStreamChild::Send__delete__` even when // FileSystemWritableFileStream::Create fails, in which case the not yet // fully constructed FileSystemWritableFileStream is being destroyed. mActor->SetStream(this); mozilla::HoldJSObjects(this); } FileSystemWritableFileStream::~FileSystemWritableFileStream() { MOZ_ASSERT(!mActor); MOZ_ASSERT(mClosed); mozilla::DropJSObjects(this); } // https://streams.spec.whatwg.org/#writablestream-set-up // * This is fallible because of OOM handling of JSAPI. See bug 1762233. // * Consider extracting this as UnderlyingSinkAlgorithmsWrapper if more classes // start subclassing WritableStream. // For now this skips step 2 - 4 as they are not required here. // XXX(krosylight): _BOUNDARY because SetUpWritableStreamDefaultController here // can't run script because StartCallback here is no-op. Can we let the static // check automatically detect this situation? /* static */ MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed FileSystemWritableFileStream::Create( nsIGlobalObject* aGlobal, RefPtr& aManager, RefPtr aActor, const ::mozilla::ipc::FileDescriptor& aFileDescriptor, const fs::FileSystemEntryMetadata& aMetadata) { AutoJSAPI jsapi; if (!jsapi.Init(aGlobal)) { return nullptr; } JSContext* cx = jsapi.cx(); // Step 5. Perform ! InitializeWritableStream(stream). // (Done by the constructor) RefPtr stream = new FileSystemWritableFileStream(aGlobal, aManager, std::move(aActor), aFileDescriptor, aMetadata); // Step 1 - 3 auto algorithms = MakeRefPtr(*stream); // Step 6. Let controller be a new WritableStreamDefaultController. auto controller = MakeRefPtr(aGlobal, *stream); // Step 7. Perform ! SetUpWritableStreamDefaultController(stream, controller, // startAlgorithm, writeAlgorithm, closeAlgorithmWrapper, // abortAlgorithmWrapper, highWaterMark, sizeAlgorithm). IgnoredErrorResult rv; SetUpWritableStreamDefaultController( cx, stream, controller, algorithms, // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream // Step 6. Let highWaterMark be 1. 1, // Step 7. Let sizeAlgorithm be an algorithm that returns 1. // (nullptr returns 1, See WritableStream::Constructor for details) nullptr, rv); if (rv.Failed()) { return nullptr; } return stream.forget(); } NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(FileSystemWritableFileStream, WritableStream) NS_IMPL_CYCLE_COLLECTION_CLASS(FileSystemWritableFileStream) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(FileSystemWritableFileStream, WritableStream) // Per the comment for the FileSystemManager class, don't unlink mManager! tmp->Close(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(FileSystemWritableFileStream, WritableStream) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mManager) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END void FileSystemWritableFileStream::LastRelease() { Close(); if (mActor) { PFileSystemWritableFileStreamChild::Send__delete__(mActor); MOZ_ASSERT(!mActor); } } void FileSystemWritableFileStream::ClearActor() { MOZ_ASSERT(mActor); mActor = nullptr; } void FileSystemWritableFileStream::Close() { // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream // Step 4. Let closeAlgorithm be these steps: if (mClosed) { return; } LOG(("%p: Closing", mFileDesc)); mClosed = true; PR_Close(mFileDesc); mFileDesc = nullptr; if (mActor) { mActor->SendClose(); } } already_AddRefed FileSystemWritableFileStream::Write( JSContext* aCx, JS::Handle aChunk, ErrorResult& aError) { MOZ_ASSERT(!mClosed); // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream // Step 3. Let writeAlgorithm be an algorithm which takes a chunk argument // and returns the result of running the write a chunk algorithm with stream // and chunk. // https://fs.spec.whatwg.org/#write-a-chunk // Step 1. Let input be the result of converting chunk to a // FileSystemWriteChunkType. aError.MightThrowJSException(); ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams data; if (!data.Init(aCx, aChunk)) { aError.StealExceptionFromJSContext(aCx); return nullptr; } // Step 2. Let p be a new promise. RefPtr promise = Promise::Create(GetParentObject(), aError); if (aError.Failed()) { return nullptr; } // Step 3.3. Let command be input.type if input is a WriteParams, ... if (data.IsWriteParams()) { const WriteParams& params = data.GetAsWriteParams(); switch (params.mType) { // Step 3.4. If command is "write": case WriteCommandType::Write: { if (!params.mData.WasPassed()) { promise->MaybeRejectWithSyntaxError("write() requires data"); return promise.forget(); } // Step 3.4.2. If data is undefined, reject p with a TypeError and // abort. if (params.mData.Value().IsNull()) { promise->MaybeRejectWithTypeError("write() of null data"); return promise.forget(); } Maybe position; if (params.mPosition.WasPassed()) { if (params.mPosition.Value().IsNull()) { promise->MaybeRejectWithTypeError("write() with null position"); return promise.forget(); } position = Some(params.mPosition.Value().Value()); } Write(params.mData.Value().Value(), position, promise); return promise.forget(); } // Step 3.5. Otherwise, if command is "seek": case WriteCommandType::Seek: if (!params.mPosition.WasPassed()) { promise->MaybeRejectWithSyntaxError("seek() requires a position"); return promise.forget(); } // Step 3.5.1. If chunk.position is undefined, reject p with a TypeError // and abort. if (params.mPosition.Value().IsNull()) { promise->MaybeRejectWithTypeError("seek() with null position"); return promise.forget(); } Seek(params.mPosition.Value().Value(), promise); return promise.forget(); // Step 3.6. Otherwise, if command is "truncate": case WriteCommandType::Truncate: if (!params.mSize.WasPassed()) { promise->MaybeRejectWithSyntaxError("truncate() requires a size"); return promise.forget(); } // Step 3.6.1. If chunk.size is undefined, reject p with a TypeError // and abort. if (params.mSize.Value().IsNull()) { promise->MaybeRejectWithTypeError("truncate() with null size"); return promise.forget(); } Truncate(params.mSize.Value().Value(), promise); return promise.forget(); default: MOZ_CRASH("Bad WriteParams value!"); } } // Step 3.3. ... and "write" otherwise. // Step 3.4. If command is "write": Write(data, Nothing(), promise); return promise.forget(); } // WebIDL Boilerplate JSObject* FileSystemWritableFileStream::WrapObject( JSContext* aCx, JS::Handle aGivenProto) { return FileSystemWritableFileStream_Binding::Wrap(aCx, this, aGivenProto); } // WebIDL Interface already_AddRefed FileSystemWritableFileStream::Write( const ArrayBufferViewOrArrayBufferOrBlobOrUTF8StringOrWriteParams& aData, ErrorResult& aError) { // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-write // Step 1. Let writer be the result of getting a writer for this. RefPtr writer = GetWriter(aError); if (aError.Failed()) { return nullptr; } // Step 2. Let result be the result of writing a chunk to writer given data. AutoJSAPI jsapi; if (!jsapi.Init(GetParentObject())) { aError.ThrowUnknownError("Internal error"); return nullptr; } JSContext* cx = jsapi.cx(); JS::Rooted global(cx, JS::CurrentGlobalOrNull(cx)); JS::Rooted val(cx); if (!aData.ToJSVal(cx, global, &val)) { aError.ThrowUnknownError("Internal error"); return nullptr; } RefPtr promise = writer->Write(cx, val, aError); // Step 3. Release writer. { IgnoredErrorResult error; writer->ReleaseLock(cx, error); } // Step 4. Return result. return promise.forget(); } already_AddRefed FileSystemWritableFileStream::Seek( uint64_t aPosition, ErrorResult& aError) { // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-seek // Step 1. Let writer be the result of getting a writer for this. RefPtr writer = GetWriter(aError); if (aError.Failed()) { return nullptr; } // Step 2. Let result be the result of writing a chunk to writer given // «[ "type" → "seek", "position" → position ]». AutoJSAPI jsapi; if (!jsapi.Init(GetParentObject())) { aError.ThrowUnknownError("Internal error"); return nullptr; } JSContext* cx = jsapi.cx(); RootedDictionary writeParams(cx); writeParams.mType = WriteCommandType::Seek; writeParams.mPosition.Construct(aPosition); JS::Rooted val(cx); if (!ToJSValue(cx, writeParams, &val)) { aError.ThrowUnknownError("Internal error"); return nullptr; } RefPtr promise = writer->Write(cx, val, aError); // Step 3. Release writer. { IgnoredErrorResult error; writer->ReleaseLock(cx, error); } // Step 4. Return result. return promise.forget(); } already_AddRefed FileSystemWritableFileStream::Truncate( uint64_t aSize, ErrorResult& aError) { // https://fs.spec.whatwg.org/#dom-filesystemwritablefilestream-truncate // Step 1. Let writer be the result of getting a writer for this. RefPtr writer = GetWriter(aError); if (aError.Failed()) { return nullptr; } // Step 2. Let result be the result of writing a chunk to writer given // «[ "type" → "truncate", "size" → size ]». AutoJSAPI jsapi; if (!jsapi.Init(GetParentObject())) { aError.ThrowUnknownError("Internal error"); return nullptr; } JSContext* cx = jsapi.cx(); RootedDictionary writeParams(cx); writeParams.mType = WriteCommandType::Truncate; writeParams.mSize.Construct(aSize); JS::Rooted val(cx); if (!ToJSValue(cx, writeParams, &val)) { aError.ThrowUnknownError("Internal error"); return nullptr; } RefPtr promise = writer->Write(cx, val, aError); // Step 3. Release writer. { IgnoredErrorResult error; writer->ReleaseLock(cx, error); } // Step 4. Return result. return promise.forget(); } template void FileSystemWritableFileStream::Write(const T& aData, const Maybe aPosition, RefPtr aPromise) { MOZ_ASSERT(!mClosed); auto rejectAndReturn = [&aPromise](const nsresult rv) { if (rv == NS_ERROR_FILE_NOT_FOUND) { aPromise->MaybeRejectWithNotFoundError("File not found"); } else { aPromise->MaybeReject(rv); } }; // https://fs.spec.whatwg.org/#write-a-chunk // Step 3.4.6 If data is a BufferSource, let dataBytes be a copy of data. if (aData.IsArrayBuffer() || aData.IsArrayBufferView()) { const auto dataSpan = [&aData]() { if (aData.IsArrayBuffer()) { const ArrayBuffer& buffer = aData.GetAsArrayBuffer(); buffer.ComputeState(); return Span{buffer.Data(), buffer.Length()}; } MOZ_ASSERT(aData.IsArrayBufferView()); const ArrayBufferView& buffer = aData.GetAsArrayBufferView(); buffer.ComputeState(); return Span{buffer.Data(), buffer.Length()}; }(); nsCString dataBuffer; QM_TRY(MOZ_TO_RESULT(dataBuffer.Assign( AsChars(dataSpan).data(), dataSpan.Length(), mozilla::fallible)), rejectAndReturn); QM_TRY_INSPECT(const auto& written, WriteBuffer(dataBuffer, aPosition), rejectAndReturn); LOG_VERBOSE(("WritableFileStream: Wrote %" PRId64, written)); aPromise->MaybeResolve(written); return; } // Step 3.4.7 Otherwise, if data is a Blob ... if (aData.IsBlob()) { Blob& blob = aData.GetAsBlob(); nsCOMPtr stream; ErrorResult error; blob.CreateInputStream(getter_AddRefs(stream), error); QM_TRY((MOZ_TO_RESULT(!error.Failed()).mapErr([&error](const nsresult rv) { return error.StealNSResult(); })), rejectAndReturn); QM_TRY_INSPECT(const auto& written, WriteStream(std::move(stream), aPosition), rejectAndReturn); LOG_VERBOSE(("WritableFileStream: Wrote %" PRId64, written)); aPromise->MaybeResolve(written); return; } // Step 3.4.8 Otherwise ... MOZ_ASSERT(aData.IsUTF8String()); QM_TRY_INSPECT(const auto& written, WriteBuffer(aData.GetAsUTF8String(), aPosition), rejectAndReturn); LOG_VERBOSE(("WritableFileStream: Wrote %" PRId64, written)); aPromise->MaybeResolve(written); } void FileSystemWritableFileStream::Seek(uint64_t aPosition, RefPtr aPromise) { MOZ_ASSERT(!mClosed); LOG_VERBOSE(("%p: Seeking to %" PRIu64, mFileDesc, aPosition)); QM_TRY(SeekPosition(aPosition), [&aPromise](const nsresult rv) { aPromise->MaybeReject(rv); return; }); aPromise->MaybeResolveWithUndefined(); } void FileSystemWritableFileStream::Truncate(uint64_t aSize, RefPtr aPromise) { MOZ_ASSERT(!mClosed); // truncate filehandle (can extend with 0's) LOG_VERBOSE(("%p: Truncate to %" PRIu64, mFileDesc, aSize)); if (NS_WARN_IF(NS_FAILED(TruncFile(mFileDesc, aSize)))) { aPromise->MaybeReject(NS_ErrorAccordingToNSPR()); return; } // We truncated; per non-normative text in the spec (2.5.3) we should // adjust the cursor position to be within the new file size int64_t where = PR_Seek(mFileDesc, 0, PR_SEEK_CUR); if (where == -1) { aPromise->MaybeReject(NS_ErrorAccordingToNSPR()); return; } if (where > (int64_t)aSize) { where = PR_Seek(mFileDesc, 0, PR_SEEK_END); if (where == -1) { aPromise->MaybeReject(NS_ErrorAccordingToNSPR()); return; } } aPromise->MaybeResolveWithUndefined(); } Result FileSystemWritableFileStream::WriteBuffer( const nsACString& aBuffer, const Maybe aPosition) { MOZ_ASSERT(!mClosed); const auto checkedLength = CheckedInt(aBuffer.Length()); QM_TRY(MOZ_TO_RESULT(checkedLength.isValid())); if (aPosition) { QM_TRY(SeekPosition(*aPosition)); } return PR_Write(mFileDesc, aBuffer.BeginReading(), checkedLength.value()); } Result FileSystemWritableFileStream::WriteStream( nsCOMPtr aStream, const Maybe aPosition) { MOZ_ASSERT(aStream); MOZ_ASSERT(!mClosed); nsCString rawBuffer; QM_TRY(MOZ_TO_RESULT(NS_ReadInputStreamToString(aStream, rawBuffer, -1))); QM_TRY_RETURN(WriteBuffer(rawBuffer, aPosition)); } Result FileSystemWritableFileStream::SeekPosition( uint64_t aPosition) { MOZ_ASSERT(!mClosed); const auto checkedPosition = CheckedInt(aPosition); QM_TRY(MOZ_TO_RESULT(checkedPosition.isValid())); int64_t cnt = PR_Seek64(mFileDesc, checkedPosition.value(), PR_SEEK_SET); if (cnt == int64_t(-1)) { LOG(("Failed to seek to %" PRIu64 " (errno %d)", aPosition, errno)); return Err(NS_ErrorAccordingToNSPR()); } if (cnt != checkedPosition.value()) { LOG(("Failed to seek to %" PRIu64 " (errno %d), ended up at %" PRId64, aPosition, errno, cnt)); return Err(NS_ERROR_FAILURE); } return Ok{}; } NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase) NS_IMPL_CYCLE_COLLECTION_INHERITED(WritableFileStreamUnderlyingSinkAlgorithms, UnderlyingSinkAlgorithmsBase, mStream) void WritableFileStreamUnderlyingSinkAlgorithms::StartCallback( JSContext* aCx, WritableStreamDefaultController& aController, JS::MutableHandle aRetVal, ErrorResult& aRv) { // https://streams.spec.whatwg.org/#writablestream-set-up // Step 1. Let startAlgorithm be an algorithm that returns undefined. aRetVal.setUndefined(); } already_AddRefed WritableFileStreamUnderlyingSinkAlgorithms::WriteCallback( JSContext* aCx, JS::Handle aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) { return mStream->Write(aCx, aChunk, aRv); } already_AddRefed WritableFileStreamUnderlyingSinkAlgorithms::CloseCallback(JSContext* aCx, ErrorResult& aRv) { // https://streams.spec.whatwg.org/#writablestream-set-up // Step 2. Let closeAlgorithmWrapper be an algorithm that runs these steps: RefPtr promise = Promise::Create(mStream->GetParentObject(), aRv); if (aRv.Failed()) { return nullptr; } if (mStream->IsClosed()) { promise->MaybeRejectWithTypeError("WritableFileStream closed"); return promise.forget(); } // XXX The close should be async. For now we have to always fallback to the // Step 2.3 below. mStream->Close(); // Step 2.3. Return a promise resolved with undefined. promise->MaybeResolveWithUndefined(); return promise.forget(); } already_AddRefed WritableFileStreamUnderlyingSinkAlgorithms::AbortCallback( JSContext* aCx, const Optional>& aReason, ErrorResult& aRv) { // https://streams.spec.whatwg.org/#writablestream-set-up // Step 3. Let abortAlgorithmWrapper be an algorithm that runs these steps: // XXX The close or rather a dedicated abort should be async. For now we have // to always fall back to the Step 3.3 below. mStream->Close(); // Step 3.3. Return a promise resolved with undefined. return Promise::CreateResolvedWithUndefined(mStream->GetParentObject(), aRv); } void WritableFileStreamUnderlyingSinkAlgorithms::ReleaseObjects() { // XXX We shouldn't be calling close here. We should just release the lock. // However, calling close here is not a big issue for now because we don't // write to a temporary file which would atomically replace the real file // during close. mStream->Close(); } } // namespace mozilla::dom