diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/osfile/NativeOSFileInternals.cpp | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/osfile/NativeOSFileInternals.cpp')
-rw-r--r-- | toolkit/components/osfile/NativeOSFileInternals.cpp | 1263 |
1 files changed, 1263 insertions, 0 deletions
diff --git a/toolkit/components/osfile/NativeOSFileInternals.cpp b/toolkit/components/osfile/NativeOSFileInternals.cpp new file mode 100644 index 0000000000..3ff8d46fb9 --- /dev/null +++ b/toolkit/components/osfile/NativeOSFileInternals.cpp @@ -0,0 +1,1263 @@ +/* -*- 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/. */ + +/** + * Native implementation of some OS.File operations. + */ + +#include "NativeOSFileInternals.h" + +#include "nsString.h" +#include "nsNetCID.h" +#include "nsThreadUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsServiceManagerUtils.h" +#include "nsProxyRelease.h" + +#include "mozilla/dom/NativeOSFileInternalsBinding.h" + +#include "mozilla/Encoding.h" +#include "nsIEventTarget.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/Scoped.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" + +#include "prio.h" +#include "prerror.h" +#include "private/pprio.h" + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/ArrayBuffer.h" // JS::GetArrayBufferByteLength,IsArrayBufferObject,NewArrayBufferWithContents,StealArrayBufferContents +#include "js/experimental/TypedData.h" // JS_NewUint8ArrayWithBuffer +#include "js/Utility.h" +#include "xpcpublic.h" + +#include <algorithm> +#if defined(XP_UNIX) +# include <errno.h> +#endif // defined (XP_UNIX) + +#if defined(XP_WIN) +# include <windows.h> +#endif // defined (XP_WIN) + +namespace mozilla { + +MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedPRFileDesc, PRFileDesc, + PR_Close) + +namespace { + +// Utilities for safely manipulating ArrayBuffer contents even in the +// absence of a JSContext. + +/** + * The C buffer underlying to an ArrayBuffer. Throughout the code, we manipulate + * this instead of a void* buffer, as this lets us transfer data across threads + * and into JavaScript without copy. + */ +struct ArrayBufferContents { + /** + * The data of the ArrayBuffer. This is the pointer manipulated to + * read/write the contents of the buffer. + */ + uint8_t* data; + /** + * The number of bytes in the ArrayBuffer. + */ + size_t nbytes; +}; + +/** + * RAII for ArrayBufferContents. + */ +struct ScopedArrayBufferContentsTraits { + typedef ArrayBufferContents type; + const static type empty() { + type result = {0, 0}; + return result; + } + static void release(type ptr) { + js_free(ptr.data); + ptr.data = nullptr; + ptr.nbytes = 0; + } +}; + +struct MOZ_NON_TEMPORARY_CLASS ScopedArrayBufferContents + : public Scoped<ScopedArrayBufferContentsTraits> { + explicit ScopedArrayBufferContents() + : Scoped<ScopedArrayBufferContentsTraits>() {} + + ScopedArrayBufferContents& operator=(ArrayBufferContents ptr) { + Scoped<ScopedArrayBufferContentsTraits>::operator=(ptr); + return *this; + } + + /** + * Request memory for this ArrayBufferContent. This memory may later + * be used to create an ArrayBuffer object (possibly on another + * thread) without copy. + * + * @return true In case of success, false otherwise. + */ + bool Allocate(uint32_t length) { + dispose(); + ArrayBufferContents& value = rwget(); + void* ptr = js_calloc(1, length); + if (ptr) { + value.data = (uint8_t*)ptr; + value.nbytes = length; + return true; + } + return false; + } + + private: + explicit ScopedArrayBufferContents(ScopedArrayBufferContents& source) = + delete; + ScopedArrayBufferContents& operator=(ScopedArrayBufferContents& source) = + delete; +}; + +///////// Cross-platform issues + +// Platform specific constants. As OS.File always uses OS-level +// errors, we need to map a few high-level errors to OS-level +// constants. +#if defined(XP_UNIX) +# define OS_ERROR_FILE_EXISTS EEXIST +# define OS_ERROR_NOMEM ENOMEM +# define OS_ERROR_INVAL EINVAL +# define OS_ERROR_TOO_LARGE EFBIG +# define OS_ERROR_RACE EIO +#elif defined(XP_WIN) +# define OS_ERROR_FILE_EXISTS ERROR_ALREADY_EXISTS +# define OS_ERROR_NOMEM ERROR_NOT_ENOUGH_MEMORY +# define OS_ERROR_INVAL ERROR_BAD_ARGUMENTS +# define OS_ERROR_TOO_LARGE ERROR_FILE_TOO_LARGE +# define OS_ERROR_RACE ERROR_SHARING_VIOLATION +#else +# error "We do not have platform-specific constants for this platform" +#endif + +///////// Results of OS.File operations + +/** + * Base class for results passed to the callbacks. + * + * This base class implements caching of JS values returned to the client. + * We make use of this caching in derived classes e.g. to avoid accidents + * when we transfer data allocated on another thread into JS. Note that + * this caching can lead to cycles (e.g. if a client adds a back-reference + * in the JS value), so we implement all Cycle Collector primitives in + * AbstractResult. + */ +class AbstractResult : public nsINativeOSFileResult { + public: + NS_DECL_NSINATIVEOSFILERESULT + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(AbstractResult) + + /** + * Construct the result object. Must be called on the main thread + * as the AbstractResult is cycle-collected. + * + * @param aStartDate The instant at which the operation was + * requested. Used to collect Telemetry statistics. + */ + explicit AbstractResult(TimeStamp aStartDate) : mStartDate(aStartDate) { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::HoldJSObjects(this); + } + + /** + * Setup the AbstractResult once data is available. + * + * @param aDispatchDate The instant at which the IO thread received + * the operation request. Used to collect Telemetry statistics. + * @param aExecutionDuration The duration of the operation on the + * IO thread. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration) { + MOZ_ASSERT(!NS_IsMainThread()); + + mDispatchDuration = (aDispatchDate - mStartDate); + mExecutionDuration = aExecutionDuration; + } + + /** + * Drop any data that could lead to a cycle. + */ + void DropJSData() { mCachedResult = JS::UndefinedValue(); } + + protected: + virtual ~AbstractResult() { + MOZ_ASSERT(NS_IsMainThread()); + mozilla::DropJSObjects(this); + } + + virtual nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle<JS::Value> aResult) = 0; + + private: + TimeStamp mStartDate; + TimeDuration mDispatchDuration; + TimeDuration mExecutionDuration; + JS::Heap<JS::Value> mCachedResult; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AbstractResult) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AbstractResult) + +NS_IMPL_CYCLE_COLLECTION_CLASS(AbstractResult) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AbstractResult) + NS_INTERFACE_MAP_ENTRY(nsINativeOSFileResult) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AbstractResult) + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mCachedResult) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AbstractResult) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AbstractResult) + tmp->DropJSData(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMETHODIMP +AbstractResult::GetDispatchDurationMS(double* aDispatchDuration) { + *aDispatchDuration = mDispatchDuration.ToMilliseconds(); + return NS_OK; +} + +NS_IMETHODIMP +AbstractResult::GetExecutionDurationMS(double* aExecutionDuration) { + *aExecutionDuration = mExecutionDuration.ToMilliseconds(); + return NS_OK; +} + +NS_IMETHODIMP +AbstractResult::GetResult(JSContext* cx, JS::MutableHandle<JS::Value> aResult) { + if (mCachedResult.isUndefined()) { + nsresult rv = GetCacheableResult(cx, aResult); + if (NS_FAILED(rv)) { + return rv; + } + mCachedResult = aResult; + return NS_OK; + } + aResult.set(mCachedResult); + return NS_OK; +} + +/** + * Return a result as a string. + * + * In this implementation, attribute |result| is a string. Strings are + * passed to JS without copy. + */ +class StringResult final : public AbstractResult { + public: + explicit StringResult(TimeStamp aStartDate) : AbstractResult(aStartDate) {} + + /** + * Initialize the object once the contents of the result as available. + * + * @param aContents The string to pass to JavaScript. Ownership of the + * string and its contents is passed to StringResult. The string must + * be valid UTF-16. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration, + nsString& aContents) { + AbstractResult::Init(aDispatchDate, aExecutionDuration); + mContents = aContents; + } + + protected: + nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle<JS::Value> aResult) override; + + private: + nsString mContents; +}; + +nsresult StringResult::GetCacheableResult( + JSContext* cx, JS::MutableHandle<JS::Value> aResult) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mContents.get()); + + // Convert mContents to a js string without copy. Note that this + // may have the side-effect of stealing the contents of the string + // from XPCOM and into JS. + if (!xpc::StringToJsval(cx, mContents, aResult)) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} + +/** + * Return a result as a Uint8Array. + * + * In this implementation, attribute |result| is a Uint8Array. The array + * is passed to JS without memory copy. + */ +class TypedArrayResult final : public AbstractResult { + public: + explicit TypedArrayResult(TimeStamp aStartDate) + : AbstractResult(aStartDate) {} + + /** + * @param aContents The contents to pass to JS. Calling this method. + * transmits ownership of the ArrayBufferContents to the TypedArrayResult. + * Do not reuse this value anywhere else. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration, + ArrayBufferContents aContents) { + AbstractResult::Init(aDispatchDate, aExecutionDuration); + mContents = aContents; + } + + protected: + nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle<JS::Value> aResult) override; + + private: + ScopedArrayBufferContents mContents; +}; + +nsresult TypedArrayResult::GetCacheableResult( + JSContext* cx, JS::MutableHandle<JS::Value> aResult) { + MOZ_ASSERT(NS_IsMainThread()); + // We cannot simply construct a typed array using contents.data as + // this would allow us to have several otherwise unrelated + // ArrayBuffers with the same underlying C buffer. As this would be + // very unsafe, we need to cache the result once we have it. + + const ArrayBufferContents& contents = mContents.get(); + MOZ_ASSERT(contents.data); + + // This takes ownership of the buffer and notes the memory allocation. + JS::Rooted<JSObject*> arrayBuffer( + cx, JS::NewArrayBufferWithContents(cx, contents.nbytes, contents.data)); + if (!arrayBuffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + JS::Rooted<JSObject*> result( + cx, JS_NewUint8ArrayWithBuffer(cx, arrayBuffer, 0, contents.nbytes)); + if (!result) { + return NS_ERROR_OUT_OF_MEMORY; + } + mContents.forget(); + + aResult.setObject(*result); + return NS_OK; +} + +/** + * Return a result as an int32_t. + * + * In this implementation, attribute |result| is an int32_t. + */ +class Int32Result final : public AbstractResult { + public: + explicit Int32Result(TimeStamp aStartDate) + : AbstractResult(aStartDate), mContents(0) {} + + /** + * Initialize the object once the contents of the result are available. + * + * @param aContents The contents to pass to JS. This is an int32_t. + */ + void Init(TimeStamp aDispatchDate, TimeDuration aExecutionDuration, + int32_t aContents) { + AbstractResult::Init(aDispatchDate, aExecutionDuration); + mContents = aContents; + } + + protected: + nsresult GetCacheableResult(JSContext* cx, + JS::MutableHandle<JS::Value> aResult) override; + + private: + int32_t mContents; +}; + +nsresult Int32Result::GetCacheableResult(JSContext* cx, + JS::MutableHandle<JS::Value> aResult) { + MOZ_ASSERT(NS_IsMainThread()); + aResult.set(JS::NumberValue(mContents)); + return NS_OK; +} + +//////// Callback events + +/** + * An event used to notify asynchronously of an error. + */ +class OSFileErrorEvent final : public Runnable { + public: + /** + * @param aOnSuccess The success callback. + * @param aOnError The error callback. + * @param aDiscardedResult The discarded result. + * @param aOperation The name of the operation, used for error reporting. + * @param aOSError The OS error of the operation, as returned by errno/ + * GetLastError(). + * + * Note that we pass both the success callback and the error + * callback, as well as the discarded result to ensure that they are + * all released on the main thread, rather than on the IO thread + * (which would hopefully segfault). Also, we pass the callbacks as + * alread_AddRefed to ensure that we do not manipulate main-thread + * only refcounters off the main thread. + */ + OSFileErrorEvent( + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError, + already_AddRefed<AbstractResult>& aDiscardedResult, + const nsACString& aOperation, int32_t aOSError) + : Runnable("OSFileErrorEvent"), + mOnSuccess(aOnSuccess), + mOnError(aOnError), + mDiscardedResult(aDiscardedResult), + mOSError(aOSError), + mOperation(aOperation) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + (void)mOnError->Complete(mOperation, mOSError); + + // Ensure that the callbacks are released on the main thread. + mOnSuccess = nullptr; + mOnError = nullptr; + mDiscardedResult = nullptr; + + return NS_OK; + } + + private: + // The callbacks. Maintained as nsMainThreadPtrHandle as they are generally + // xpconnect values, which cannot be manipulated with nsCOMPtr off + // the main thread. We store both the success callback and the + // error callback to ensure that they are safely released on the + // main thread. + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> mOnSuccess; + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> mOnError; + RefPtr<AbstractResult> mDiscardedResult; + int32_t mOSError; + nsCString mOperation; +}; + +/** + * An event used to notify of a success. + */ +class SuccessEvent final : public Runnable { + public: + /** + * @param aOnSuccess The success callback. + * @param aOnError The error callback. + * + * Note that we pass both the success callback and the error + * callback to ensure that they are both released on the main + * thread, rather than on the IO thread (which would hopefully + * segfault). Also, we pass them as alread_AddRefed to ensure that + * we do not manipulate xpconnect refcounters off the main thread + * (which is illegal). + */ + SuccessEvent( + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError, + already_AddRefed<nsINativeOSFileResult>& aResult) + : Runnable("SuccessEvent"), + mOnSuccess(aOnSuccess), + mOnError(aOnError), + mResult(aResult) { + MOZ_ASSERT(!NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + (void)mOnSuccess->Complete(mResult); + + // Ensure that the callbacks are released on the main thread. + mOnSuccess = nullptr; + mOnError = nullptr; + mResult = nullptr; + + return NS_OK; + } + + private: + // The callbacks. Maintained as nsMainThreadPtrHandle as they are generally + // xpconnect values, which cannot be manipulated with nsCOMPtr off + // the main thread. We store both the success callback and the + // error callback to ensure that they are safely released on the + // main thread. + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> mOnSuccess; + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> mOnError; + RefPtr<nsINativeOSFileResult> mResult; +}; + +//////// Action events + +/** + * Base class shared by actions. + */ +class AbstractDoEvent : public Runnable { + public: + AbstractDoEvent( + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError) + : Runnable("AbstractDoEvent"), + mOnSuccess(aOnSuccess), + mOnError(aOnError) +#if defined(DEBUG) + , + mResolved(false) +#endif // defined(DEBUG) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + /** + * Fail, asynchronously. + */ + void Fail(const nsACString& aOperation, + already_AddRefed<AbstractResult>&& aDiscardedResult, + int32_t aOSError = 0) { + Resolve(); + + RefPtr<OSFileErrorEvent> event = new OSFileErrorEvent( + mOnSuccess, mOnError, aDiscardedResult, aOperation, aOSError); + nsresult rv = NS_DispatchToMainThread(event); + if (NS_FAILED(rv)) { + // Last ditch attempt to release on the main thread - some of + // the members of event are not thread-safe, so letting the + // pointer go out of scope would cause a crash. + NS_ReleaseOnMainThread("AbstractDoEvent::OSFileErrorEvent", + event.forget()); + } + } + + /** + * Succeed, asynchronously. + */ + void Succeed(already_AddRefed<nsINativeOSFileResult>&& aResult) { + Resolve(); + RefPtr<SuccessEvent> event = + new SuccessEvent(mOnSuccess, mOnError, aResult); + nsresult rv = NS_DispatchToMainThread(event); + if (NS_FAILED(rv)) { + // Last ditch attempt to release on the main thread - some of + // the members of event are not thread-safe, so letting the + // pointer go out of scope would cause a crash. + NS_ReleaseOnMainThread("AbstractDoEvent::SuccessEvent", event.forget()); + } + } + + private: + /** + * Mark the event as complete, for debugging purposes. + */ + void Resolve() { +#if defined(DEBUG) + MOZ_ASSERT(!mResolved); + mResolved = true; +#endif // defined(DEBUG) + } + + private: + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> mOnSuccess; + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> mOnError; +#if defined(DEBUG) + // |true| once the action is complete + bool mResolved; +#endif // defined(DEBUG) +}; + +/** + * An abstract event implementing reading from a file. + * + * Concrete subclasses are responsible for handling the + * data obtained from the file and possibly post-processing it. + */ +class AbstractReadEvent : public AbstractDoEvent { + public: + /** + * @param aPath The path of the file. + */ + AbstractReadEvent( + const nsAString& aPath, const uint64_t aBytes, + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError) + : AbstractDoEvent(aOnSuccess, aOnError), mPath(aPath), mBytes(aBytes) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + TimeStamp dispatchDate = TimeStamp::Now(); + + nsresult rv = BeforeRead(); + if (NS_FAILED(rv)) { + // Error reporting is handled by BeforeRead(); + return NS_OK; + } + + ScopedArrayBufferContents buffer; + rv = Read(buffer); + if (NS_FAILED(rv)) { + // Error reporting is handled by Read(); + return NS_OK; + } + + AfterRead(dispatchDate, buffer); + return NS_OK; + } + + private: + /** + * Read synchronously. + * + * Must be called off the main thread. + * + * @param aBuffer The destination buffer. + */ + nsresult Read(ScopedArrayBufferContents& aBuffer) { + MOZ_ASSERT(!NS_IsMainThread()); + + ScopedPRFileDesc file; +#if defined(XP_WIN) + // On Windows, we can't use PR_OpenFile because it doesn't + // handle UTF-16 encoding, which is pretty bad. In addition, + // PR_OpenFile opens files without sharing, which is not the + // general semantics of OS.File. + HANDLE handle = + ::CreateFileW(mPath.get(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*Security attributes*/ nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, + /*Template file*/ nullptr); + + if (handle == INVALID_HANDLE_VALUE) { + Fail("open"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } + + file = PR_ImportFile((PROsfd)handle); + if (!file) { + // |file| is closed by PR_ImportFile + Fail("ImportFile"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + +#else + // On other platforms, PR_OpenFile will do. + NS_ConvertUTF16toUTF8 path(mPath); + file = PR_OpenFile(path.get(), PR_RDONLY, 0); + if (!file) { + Fail("open"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + +#endif // defined(XP_XIN) + + PRFileInfo64 stat; + if (PR_GetOpenFileInfo64(file, &stat) != PR_SUCCESS) { + Fail("stat"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + + uint64_t bytes = std::min((uint64_t)stat.size, mBytes); + if (bytes > UINT32_MAX) { + Fail("Arithmetics"_ns, nullptr, OS_ERROR_INVAL); + return NS_ERROR_FAILURE; + } + + if (!aBuffer.Allocate(bytes)) { + Fail("allocate"_ns, nullptr, OS_ERROR_NOMEM); + return NS_ERROR_FAILURE; + } + + uint64_t total_read = 0; + int32_t just_read = 0; + char* dest_chars = reinterpret_cast<char*>(aBuffer.rwget().data); + do { + just_read = PR_Read(file, dest_chars + total_read, + std::min(uint64_t(PR_INT32_MAX), bytes - total_read)); + if (just_read == -1) { + Fail("read"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + total_read += just_read; + } while (just_read != 0 && total_read < bytes); + if (total_read != bytes) { + // We seem to have a race condition here. + Fail("read"_ns, nullptr, OS_ERROR_RACE); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + protected: + /** + * Any steps that need to be taken before reading. + * + * In case of error, this method should call Fail() and return + * a failure code. + */ + virtual nsresult BeforeRead() { return NS_OK; } + + /** + * Proceed after reading. + */ + virtual void AfterRead(TimeStamp aDispatchDate, + ScopedArrayBufferContents& aBuffer) = 0; + + protected: + const nsString mPath; + const uint64_t mBytes; +}; + +/** + * An implementation of a Read event that provides the data + * as a TypedArray. + */ +class DoReadToTypedArrayEvent final : public AbstractReadEvent { + public: + DoReadToTypedArrayEvent( + const nsAString& aPath, const uint32_t aBytes, + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError) + : AbstractReadEvent(aPath, aBytes, aOnSuccess, aOnError), + mResult(new TypedArrayResult(TimeStamp::Now())) {} + + ~DoReadToTypedArrayEvent() override { + // If AbstractReadEvent::Run() has bailed out, we may need to cleanup + // mResult, which is main-thread only data + if (!mResult) { + return; + } + NS_ReleaseOnMainThread("DoReadToTypedArrayEvent::mResult", + mResult.forget()); + } + + protected: + void AfterRead(TimeStamp aDispatchDate, + ScopedArrayBufferContents& aBuffer) override { + MOZ_ASSERT(!NS_IsMainThread()); + mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, + aBuffer.forget()); + Succeed(mResult.forget()); + } + + private: + RefPtr<TypedArrayResult> mResult; +}; + +/** + * An implementation of a Read event that provides the data + * as a JavaScript string. + */ +class DoReadToStringEvent final : public AbstractReadEvent { + public: + DoReadToStringEvent( + const nsAString& aPath, const nsACString& aEncoding, + const uint32_t aBytes, + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError) + : AbstractReadEvent(aPath, aBytes, aOnSuccess, aOnError), + mEncoding(aEncoding), + mResult(new StringResult(TimeStamp::Now())) {} + + ~DoReadToStringEvent() override { + // If AbstraactReadEvent::Run() has bailed out, we may need to cleanup + // mResult, which is main-thread only data + if (!mResult) { + return; + } + NS_ReleaseOnMainThread("DoReadToStringEvent::mResult", mResult.forget()); + } + + protected: + nsresult BeforeRead() override { + // Obtain the decoder. We do this before reading to avoid doing + // any unnecessary I/O in case the name of the encoding is incorrect. + MOZ_ASSERT(!NS_IsMainThread()); + const Encoding* encoding = Encoding::ForLabel(mEncoding); + if (!encoding) { + Fail("Decode"_ns, mResult.forget(), OS_ERROR_INVAL); + return NS_ERROR_FAILURE; + } + mDecoder = encoding->NewDecoderWithBOMRemoval(); + if (!mDecoder) { + Fail("DecoderForEncoding"_ns, mResult.forget(), OS_ERROR_INVAL); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + void AfterRead(TimeStamp aDispatchDate, + ScopedArrayBufferContents& aBuffer) override { + MOZ_ASSERT(!NS_IsMainThread()); + + auto src = Span(aBuffer.get().data, aBuffer.get().nbytes); + + CheckedInt<size_t> needed = mDecoder->MaxUTF16BufferLength(src.Length()); + if (!needed.isValid() || + needed.value() > std::numeric_limits<nsAString::size_type>::max()) { + Fail("arithmetics"_ns, mResult.forget(), OS_ERROR_TOO_LARGE); + return; + } + + nsString resultString; + auto resultSpan = resultString.GetMutableData(needed.value(), fallible); + if (!resultSpan) { + Fail("allocation"_ns, mResult.forget(), OS_ERROR_TOO_LARGE); + return; + } + + // Yoric said on IRC that this method is normally called for the entire + // file, but that's not guaranteed. Retaining the bug that EOF in conversion + // isn't handled anywhere. + uint32_t result; + size_t read; + size_t written; + std::tie(result, read, written, std::ignore) = + mDecoder->DecodeToUTF16(src, *resultSpan, false); + MOZ_ASSERT(result == kInputEmpty); + MOZ_ASSERT(read == src.Length()); + MOZ_ASSERT(written <= needed.value()); + bool ok = resultString.SetLength(written, fallible); + if (!ok) { + Fail("allocation"_ns, mResult.forget(), OS_ERROR_TOO_LARGE); + return; + } + + mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, + resultString); + Succeed(mResult.forget()); + } + + private: + nsCString mEncoding; + mozilla::UniquePtr<mozilla::Decoder> mDecoder; + RefPtr<StringResult> mResult; +}; + +/** + * An event implenting writing atomically to a file. + */ +class DoWriteAtomicEvent : public AbstractDoEvent { + public: + /** + * @param aPath The path of the file. + */ + DoWriteAtomicEvent( + const nsAString& aPath, UniquePtr<char[], JS::FreePolicy> aBuffer, + const uint64_t aBytes, const nsAString& aTmpPath, + const nsAString& aBackupTo, const bool aFlush, const bool aNoOverwrite, + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess, + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError) + : AbstractDoEvent(aOnSuccess, aOnError), + mPath(aPath), + mBuffer(std::move(aBuffer)), + mBytes(aBytes), + mTmpPath(aTmpPath), + mBackupTo(aBackupTo), + mFlush(aFlush), + mNoOverwrite(aNoOverwrite), + mResult(new Int32Result(TimeStamp::Now())) { + MOZ_ASSERT(NS_IsMainThread()); + } + + ~DoWriteAtomicEvent() override { + // If Run() has bailed out, we may need to cleanup + // mResult, which is main-thread only data + if (!mResult) { + return; + } + NS_ReleaseOnMainThread("DoWriteAtomicEvent::mResult", mResult.forget()); + } + + NS_IMETHODIMP Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + TimeStamp dispatchDate = TimeStamp::Now(); + int32_t bytesWritten; + + nsresult rv = WriteAtomic(&bytesWritten); + if (NS_FAILED(rv)) { + return NS_OK; + } + + AfterWriteAtomic(dispatchDate, bytesWritten); + return NS_OK; + } + + private: + /** + * Write atomically to a file. + * Must be called off the main thread. + * @param aBytesWritten will contain the total bytes written. + * This does not support compression in this implementation. + */ + nsresult WriteAtomic(int32_t* aBytesWritten) { + MOZ_ASSERT(!NS_IsMainThread()); + + // Note: In Windows, many NSPR File I/O functions which act on pathnames + // do not handle UTF-16 encoding. Thus, we use the following functions + // to overcome this. + // PR_Access : GetFileAttributesW + // PR_Delete : DeleteFileW + // PR_OpenFile : CreateFileW followed by PR_ImportFile + // PR_Rename : MoveFileW + + ScopedPRFileDesc file; + NS_ConvertUTF16toUTF8 path(mPath); + NS_ConvertUTF16toUTF8 tmpPath(mTmpPath); + NS_ConvertUTF16toUTF8 backupTo(mBackupTo); + bool fileExists = false; + + if (!mTmpPath.IsVoid() || !mBackupTo.IsVoid() || mNoOverwrite) { + // fileExists needs to be computed in the case of tmpPath, since + // the rename behaves differently depending on whether the + // file already exists. It's also computed for backupTo since the + // backup can be skipped if the file does not exist in the first place. +#if defined(XP_WIN) + fileExists = ::GetFileAttributesW(mPath.get()) != INVALID_FILE_ATTRIBUTES; +#else + fileExists = PR_Access(path.get(), PR_ACCESS_EXISTS) == PR_SUCCESS; +#endif // defined(XP_WIN) + } + + // Check noOverwrite. + if (mNoOverwrite && fileExists) { + Fail("noOverwrite"_ns, nullptr, OS_ERROR_FILE_EXISTS); + return NS_ERROR_FAILURE; + } + + // Backup the original file if it exists. + if (!mBackupTo.IsVoid() && fileExists) { +#if defined(XP_WIN) + if (::GetFileAttributesW(mBackupTo.get()) != INVALID_FILE_ATTRIBUTES) { + // The file specified by mBackupTo exists, so we need to delete it + // first. + if (::DeleteFileW(mBackupTo.get()) == false) { + Fail("delete"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } + } + + if (::MoveFileW(mPath.get(), mBackupTo.get()) == false) { + Fail("rename"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } +#else + if (PR_Access(backupTo.get(), PR_ACCESS_EXISTS) == PR_SUCCESS) { + // The file specified by mBackupTo exists, so we need to delete it + // first. + if (PR_Delete(backupTo.get()) == PR_FAILURE) { + Fail("delete"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + } + + if (PR_Rename(path.get(), backupTo.get()) == PR_FAILURE) { + Fail("rename"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + } + +#if defined(XP_WIN) + // In addition to not handling UTF-16 encoding in file paths, + // PR_OpenFile opens files without sharing, which is not the + // general semantics of OS.File. + HANDLE handle; + // if we're dealing with a tmpFile, we need to write there. + if (!mTmpPath.IsVoid()) { + handle = ::CreateFileW( + mTmpPath.get(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*Security attributes*/ nullptr, + // CREATE_ALWAYS is used since since we need to create the temporary + // file, which we don't care about overwriting. + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, + /*Template file*/ nullptr); + } else { + handle = ::CreateFileW( + mPath.get(), GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + /*Security attributes*/ nullptr, + // CREATE_ALWAYS is used since since have already checked the + // noOverwrite condition, and thus can overwrite safely. + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, + /*Template file*/ nullptr); + } + + if (handle == INVALID_HANDLE_VALUE) { + Fail("open"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } + + file = PR_ImportFile((PROsfd)handle); + if (!file) { + // |file| is closed by PR_ImportFile + Fail("ImportFile"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + +#else + // if we're dealing with a tmpFile, we need to write there. + if (!mTmpPath.IsVoid()) { + file = + PR_OpenFile(tmpPath.get(), PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + PR_IRUSR | PR_IWUSR); + } else { + file = PR_OpenFile(path.get(), PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, + PR_IRUSR | PR_IWUSR); + } + + if (!file) { + Fail("open"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + + int32_t bytesWrittenSuccess = + PR_Write(file, (void*)(mBuffer.get()), mBytes); + + if (bytesWrittenSuccess == -1) { + Fail("write"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + + // Apply any tmpPath renames. + if (!mTmpPath.IsVoid()) { + if (mBackupTo.IsVoid() && fileExists) { + // We need to delete the old file first, if it exists and we haven't + // already renamed it as a part of backing it up. +#if defined(XP_WIN) + if (::DeleteFileW(mPath.get()) == false) { + Fail("delete"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } +#else + if (PR_Delete(path.get()) == PR_FAILURE) { + Fail("delete"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + } + +#if defined(XP_WIN) + if (::MoveFileW(mTmpPath.get(), mPath.get()) == false) { + Fail("rename"_ns, nullptr, ::GetLastError()); + return NS_ERROR_FAILURE; + } +#else + if (PR_Rename(tmpPath.get(), path.get()) == PR_FAILURE) { + Fail("rename"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } +#endif // defined(XP_WIN) + } + + if (mFlush) { + if (PR_Sync(file) == PR_FAILURE) { + Fail("sync"_ns, nullptr, PR_GetOSError()); + return NS_ERROR_FAILURE; + } + } + + *aBytesWritten = bytesWrittenSuccess; + return NS_OK; + } + + protected: + nsresult AfterWriteAtomic(TimeStamp aDispatchDate, int32_t aBytesWritten) { + MOZ_ASSERT(!NS_IsMainThread()); + mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, + aBytesWritten); + Succeed(mResult.forget()); + return NS_OK; + } + + const nsString mPath; + const UniquePtr<char[], JS::FreePolicy> mBuffer; + const int32_t mBytes; + const nsString mTmpPath; + const nsString mBackupTo; + const bool mFlush; + const bool mNoOverwrite; + + private: + RefPtr<Int32Result> mResult; +}; + +} // namespace + +// The OS.File service + +NS_IMPL_ISUPPORTS(NativeOSFileInternalsService, + nsINativeOSFileInternalsService); + +NS_IMETHODIMP +NativeOSFileInternalsService::Read(const nsAString& aPath, + JS::Handle<JS::Value> aOptions, + nsINativeOSFileSuccessCallback* aOnSuccess, + nsINativeOSFileErrorCallback* aOnError, + JSContext* cx) { + // Extract options + nsCString encoding; + uint64_t bytes = UINT64_MAX; + + if (aOptions.isObject()) { + dom::NativeOSFileReadOptions dict; + if (!dict.Init(cx, aOptions)) { + return NS_ERROR_INVALID_ARG; + } + + if (dict.mEncoding.WasPassed()) { + CopyUTF16toUTF8(dict.mEncoding.Value(), encoding); + } + + if (dict.mBytes.WasPassed() && !dict.mBytes.Value().IsNull()) { + bytes = dict.mBytes.Value().Value(); + } + } + + // Prepare the off main thread event and dispatch it + nsCOMPtr<nsINativeOSFileSuccessCallback> onSuccess(aOnSuccess); + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> onSuccessHandle( + new nsMainThreadPtrHolder<nsINativeOSFileSuccessCallback>( + "nsINativeOSFileSuccessCallback", onSuccess)); + nsCOMPtr<nsINativeOSFileErrorCallback> onError(aOnError); + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> onErrorHandle( + new nsMainThreadPtrHolder<nsINativeOSFileErrorCallback>( + "nsINativeOSFileErrorCallback", onError)); + + RefPtr<AbstractDoEvent> event; + if (encoding.IsEmpty()) { + event = new DoReadToTypedArrayEvent(aPath, bytes, onSuccessHandle, + onErrorHandle); + } else { + event = new DoReadToStringEvent(aPath, encoding, bytes, onSuccessHandle, + onErrorHandle); + } + + nsresult rv; + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + + if (NS_FAILED(rv)) { + return rv; + } + return target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +// Note: This method steals the contents of `aBuffer`. +NS_IMETHODIMP +NativeOSFileInternalsService::WriteAtomic( + const nsAString& aPath, JS::Handle<JS::Value> aBuffer, + JS::Handle<JS::Value> aOptions, nsINativeOSFileSuccessCallback* aOnSuccess, + nsINativeOSFileErrorCallback* aOnError, JSContext* cx) { + MOZ_ASSERT(NS_IsMainThread()); + // Extract typed-array/string into buffer. We also need to store the length + // of the buffer as that may be required if not provided in `aOptions`. + UniquePtr<char[], JS::FreePolicy> buffer; + int32_t bytes; + + // The incoming buffer must be an Object. + if (!aBuffer.isObject()) { + return NS_ERROR_INVALID_ARG; + } + + JS::Rooted<JSObject*> bufferObject(cx, nullptr); + if (!JS_ValueToObject(cx, aBuffer, &bufferObject)) { + return NS_ERROR_FAILURE; + } + if (!JS::IsArrayBufferObject(bufferObject.get())) { + return NS_ERROR_INVALID_ARG; + } + + { + // Throw for large ArrayBuffers to prevent truncation. + size_t len = JS::GetArrayBufferByteLength(bufferObject.get()); + if (len > INT32_MAX) { + return NS_ERROR_INVALID_ARG; + } + bytes = len; + } + buffer.reset( + static_cast<char*>(JS::StealArrayBufferContents(cx, bufferObject))); + + if (!buffer) { + return NS_ERROR_FAILURE; + } + + // Extract options. + dom::NativeOSFileWriteAtomicOptions dict; + + if (aOptions.isObject()) { + if (!dict.Init(cx, aOptions)) { + return NS_ERROR_INVALID_ARG; + } + } else { + // If an options object is not provided, initializing with a `null` + // value, which will give a set of defaults defined in the WebIDL binding. + if (!dict.Init(cx, JS::NullHandleValue)) { + return NS_ERROR_FAILURE; + } + } + + if (dict.mBytes.WasPassed() && !dict.mBytes.Value().IsNull()) { + // We need to check size and cast because NSPR and WebIDL have different + // types. + if (dict.mBytes.Value().Value() > PR_INT32_MAX) { + return NS_ERROR_INVALID_ARG; + } + bytes = (int32_t)(dict.mBytes.Value().Value()); + } + + // Prepare the off main thread event and dispatch it + nsCOMPtr<nsINativeOSFileSuccessCallback> onSuccess(aOnSuccess); + nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback> onSuccessHandle( + new nsMainThreadPtrHolder<nsINativeOSFileSuccessCallback>( + "nsINativeOSFileSuccessCallback", onSuccess)); + nsCOMPtr<nsINativeOSFileErrorCallback> onError(aOnError); + nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> onErrorHandle( + new nsMainThreadPtrHolder<nsINativeOSFileErrorCallback>( + "nsINativeOSFileErrorCallback", onError)); + + RefPtr<AbstractDoEvent> event = new DoWriteAtomicEvent( + aPath, std::move(buffer), bytes, dict.mTmpPath, dict.mBackupTo, + dict.mFlush, dict.mNoOverwrite, onSuccessHandle, onErrorHandle); + nsresult rv; + nsCOMPtr<nsIEventTarget> target = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + + if (NS_FAILED(rv)) { + return rv; + } + + return target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +} // namespace mozilla |