/* -*- 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/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 { extern LazyLogModule gOPFSLog; } #define LOG(args) MOZ_LOG(mozilla::gOPFSLog, mozilla::LogLevel::Verbose, args) #define LOG_VERBOSE(args) \ MOZ_LOG(mozilla::gOPFSLog, mozilla::LogLevel::Verbose, args) #define LOG_DEBUG(args) \ MOZ_LOG(mozilla::gOPFSLog, mozilla::LogLevel::Debug, args) 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 { // https://streams.spec.whatwg.org/#writablestream-set-up // Step 1. Let startAlgorithm be an algorithm that returns undefined. aRetVal.setUndefined(); } MOZ_CAN_RUN_SCRIPT already_AddRefed WriteCallback( JSContext* aCx, JS::Handle aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) override; MOZ_CAN_RUN_SCRIPT already_AddRefed AbortCallback( JSContext* aCx, const Optional>& aReason, ErrorResult& aRv) override { // https://streams.spec.whatwg.org/#writablestream-set-up // Step 3.3. Return a promise resolved with undefined. // (No abort algorithm is defined for this interface) return Promise::CreateResolvedWithUndefined(mStream->GetParentObject(), aRv); } MOZ_CAN_RUN_SCRIPT already_AddRefed CloseCallback( JSContext* aCx, ErrorResult& aRv) 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), 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)); } FileSystemWritableFileStream::~FileSystemWritableFileStream() { MOZ_ASSERT(!mActor); MOZ_ASSERT(mClosed); } // 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, // default highWaterMark 1, // default sizeAlgorithm // (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() { if (mClosed) { return; } LOG(("%p: Closing", mFileDesc)); mClosed = true; PR_Close(mFileDesc); mFileDesc = nullptr; if (mActor) { mActor->SendClose(); } } // WebIDL Boilerplate JSObject* FileSystemWritableFileStream::WrapObject( JSContext* aCx, JS::Handle aGivenProto) { return FileSystemWritableFileStream_Binding::Wrap(aCx, this, aGivenProto); } // WebIDL Interface already_AddRefed FileSystemWritableFileStream::Write( const ArrayBufferViewOrArrayBufferOrBlobOrUSVStringOrWriteParams& aData, ErrorResult& aError) { RefPtr promise = Promise::Create(GetParentObject(), aError); if (aError.Failed()) { return nullptr; } if (mClosed) { promise->MaybeRejectWithTypeError("WritableFileStream closed"); return promise.forget(); } if (aData.IsWriteParams()) { const WriteParams& params = aData.GetAsWriteParams(); switch (params.mType) { case WriteCommandType::Write: { if (!params.mData.WasPassed()) { aError.ThrowSyntaxError("write() requires data"); return nullptr; } 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(); } case WriteCommandType::Seek: if (!params.mPosition.WasPassed()) { aError.ThrowSyntaxError("seek() requires a position"); return nullptr; } if (params.mPosition.Value().IsNull()) { promise->MaybeRejectWithTypeError("seek() with null position"); return promise.forget(); } Seek(params.mPosition.Value().Value(), promise); return promise.forget(); case WriteCommandType::Truncate: if (!params.mSize.WasPassed()) { aError.ThrowSyntaxError("truncate() requires a size"); return nullptr; } 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!"); } } Write(aData, Nothing(), promise); return promise.forget(); } already_AddRefed FileSystemWritableFileStream::Seek( uint64_t aPosition, ErrorResult& aError) { RefPtr promise = Promise::Create(GetParentObject(), aError); if (aError.Failed()) { return nullptr; } if (mClosed) { promise->MaybeRejectWithTypeError("WritableFileStream closed"); return promise.forget(); } Seek(aPosition, promise); return promise.forget(); } already_AddRefed FileSystemWritableFileStream::Truncate( uint64_t aSize, ErrorResult& aError) { RefPtr promise = Promise::Create(GetParentObject(), aError); if (aError.Failed()) { return nullptr; } if (mClosed) { promise->MaybeRejectWithTypeError("WritableFileStream closed"); return promise.forget(); } Truncate(aSize, promise); return promise.forget(); } template void FileSystemWritableFileStream::Write(const T& aData, const Maybe aPosition, RefPtr aPromise) { 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()}; }(); auto maybeBuffer = Buffer::CopyFrom(AsChars(dataSpan)); QM_TRY(MOZ_TO_RESULT(maybeBuffer.isSome()), rejectAndReturn); QM_TRY_INSPECT(const auto& written, WriteBuffer(maybeBuffer.extract(), aPosition), rejectAndReturn); 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); aPromise->MaybeResolve(written); return; } // Step 3.4.8 Otherwise ... MOZ_ASSERT(aData.IsUSVString()); uint32_t count; UniquePtr string( ToNewUTF8String(aData.GetAsUSVString(), &count, fallible)); QM_TRY((MOZ_TO_RESULT(string.get()).mapErr([](const nsresult) { return NS_ERROR_OUT_OF_MEMORY; })), rejectAndReturn); Buffer buffer(std::move(string), count); QM_TRY_INSPECT(const auto& written, WriteBuffer(std::move(buffer), aPosition), rejectAndReturn); aPromise->MaybeResolve(written); } void FileSystemWritableFileStream::Seek(uint64_t aPosition, RefPtr aPromise) { MOZ_ASSERT(!mClosed); // submit async seek // XXX what happens if we read/write before seek finishes? // Should we block read/write if an async operation is pending? // Handle seek before write ('at') 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); // submit async truncate // XXX what happens if we read/write before seek finishes? // Should we block read/write if an async operation is pending? // What if there's an error, and several operations are pending? // Spec issues raised. // truncate filehandle (can extend with 0's) LOG(("%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( Buffer&& aBuffer, const Maybe aPosition) { Buffer buffer = std::move(aBuffer); const auto checkedLength = CheckedInt(buffer.Length()); QM_TRY(MOZ_TO_RESULT(checkedLength.isValid())); if (aPosition) { QM_TRY(SeekPosition(*aPosition)); } return PR_Write(mFileDesc, buffer.Elements(), checkedLength.value()); } Result FileSystemWritableFileStream::WriteStream( nsCOMPtr aStream, const Maybe aPosition) { MOZ_ASSERT(aStream); void* rawBuffer = nullptr; uint64_t length; QM_TRY(MOZ_TO_RESULT( NS_ReadInputStreamToBuffer(aStream, &rawBuffer, -1, &length))); Buffer buffer(UniquePtr(reinterpret_cast(rawBuffer)), length); QM_TRY_RETURN(WriteBuffer(std::move(buffer), aPosition)); } Result FileSystemWritableFileStream::SeekPosition( uint64_t aPosition) { 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) already_AddRefed WritableFileStreamUnderlyingSinkAlgorithms::WriteCallback( JSContext* aCx, JS::Handle aChunk, WritableStreamDefaultController& aController, ErrorResult& aRv) { // https://fs.spec.whatwg.org/#create-a-new-filesystemwritablefilestream // Step 3. Let writeAlgorithm be an algorithm which takes a chunk argument ... ArrayBufferViewOrArrayBufferOrBlobOrUSVStringOrWriteParams chunkUnion; if (!chunkUnion.Init(aCx, aChunk)) { aRv.MightThrowJSException(); aRv.StealExceptionFromJSContext(aCx); return nullptr; } // Step 3. ... and returns the result of running the write a chunk algorithm // with stream and chunk. return mStream->Write(chunkUnion, aRv); } already_AddRefed WritableFileStreamUnderlyingSinkAlgorithms::CloseCallback(JSContext* aCx, ErrorResult& aRv) { RefPtr promise = Promise::Create(mStream->GetParentObject(), aRv); if (aRv.Failed()) { return nullptr; } if (mStream->IsClosed()) { promise->MaybeRejectWithTypeError("WritableFileStream closed"); return promise.forget(); } mStream->Close(); promise->MaybeResolveWithUndefined(); return promise.forget(); } } // namespace mozilla::dom