/* -*- 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 #if defined(XP_UNIX) # include #endif // defined (XP_UNIX) #if defined(XP_WIN) # include #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 { explicit ScopedArrayBufferContents() : Scoped() {} ScopedArrayBufferContents& operator=(ArrayBufferContents ptr) { Scoped::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 aResult) = 0; private: TimeStamp mStartDate; TimeDuration mDispatchDuration; TimeDuration mExecutionDuration; JS::Heap 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 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 aResult) override; private: nsString mContents; }; nsresult StringResult::GetCacheableResult( JSContext* cx, JS::MutableHandle 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 aResult) override; private: ScopedArrayBufferContents mContents; }; nsresult TypedArrayResult::GetCacheableResult( JSContext* cx, JS::MutableHandle 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 arrayBuffer( cx, JS::NewArrayBufferWithContents(cx, contents.nbytes, contents.data)); if (!arrayBuffer) { return NS_ERROR_OUT_OF_MEMORY; } JS::Rooted 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 aResult) override; private: int32_t mContents; }; nsresult Int32Result::GetCacheableResult(JSContext* cx, JS::MutableHandle 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& aOnSuccess, nsMainThreadPtrHandle& aOnError, already_AddRefed& 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 mOnSuccess; nsMainThreadPtrHandle mOnError; RefPtr 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& aOnSuccess, nsMainThreadPtrHandle& aOnError, already_AddRefed& 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 mOnSuccess; nsMainThreadPtrHandle mOnError; RefPtr mResult; }; //////// Action events /** * Base class shared by actions. */ class AbstractDoEvent : public Runnable { public: AbstractDoEvent( nsMainThreadPtrHandle& aOnSuccess, nsMainThreadPtrHandle& 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&& aDiscardedResult, int32_t aOSError = 0) { Resolve(); RefPtr 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&& aResult) { Resolve(); RefPtr 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 mOnSuccess; nsMainThreadPtrHandle 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& aOnSuccess, nsMainThreadPtrHandle& 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(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& aOnSuccess, nsMainThreadPtrHandle& 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 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& aOnSuccess, nsMainThreadPtrHandle& 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 needed = mDecoder->MaxUTF16BufferLength(src.Length()); if (!needed.isValid() || needed.value() > std::numeric_limits::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 mDecoder; RefPtr 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 aBuffer, const uint64_t aBytes, const nsAString& aTmpPath, const nsAString& aBackupTo, const bool aFlush, const bool aNoOverwrite, nsMainThreadPtrHandle& aOnSuccess, nsMainThreadPtrHandle& 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 mBuffer; const int32_t mBytes; const nsString mTmpPath; const nsString mBackupTo; const bool mFlush; const bool mNoOverwrite; private: RefPtr mResult; }; } // namespace // The OS.File service NS_IMPL_ISUPPORTS(NativeOSFileInternalsService, nsINativeOSFileInternalsService); NS_IMETHODIMP NativeOSFileInternalsService::Read(const nsAString& aPath, JS::Handle 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 onSuccess(aOnSuccess); nsMainThreadPtrHandle onSuccessHandle( new nsMainThreadPtrHolder( "nsINativeOSFileSuccessCallback", onSuccess)); nsCOMPtr onError(aOnError); nsMainThreadPtrHandle onErrorHandle( new nsMainThreadPtrHolder( "nsINativeOSFileErrorCallback", onError)); RefPtr event; if (encoding.IsEmpty()) { event = new DoReadToTypedArrayEvent(aPath, bytes, onSuccessHandle, onErrorHandle); } else { event = new DoReadToStringEvent(aPath, encoding, bytes, onSuccessHandle, onErrorHandle); } nsresult rv; nsCOMPtr 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 aBuffer, JS::Handle 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 buffer; int32_t bytes; // The incoming buffer must be an Object. if (!aBuffer.isObject()) { return NS_ERROR_INVALID_ARG; } JS::Rooted 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(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 onSuccess(aOnSuccess); nsMainThreadPtrHandle onSuccessHandle( new nsMainThreadPtrHolder( "nsINativeOSFileSuccessCallback", onSuccess)); nsCOMPtr onError(aOnError); nsMainThreadPtrHandle onErrorHandle( new nsMainThreadPtrHolder( "nsINativeOSFileErrorCallback", onError)); RefPtr event = new DoWriteAtomicEvent( aPath, std::move(buffer), bytes, dict.mTmpPath, dict.mBackupTo, dict.mFlush, dict.mNoOverwrite, onSuccessHandle, onErrorHandle); nsresult rv; nsCOMPtr target = do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); if (NS_FAILED(rv)) { return rv; } return target->Dispatch(event, NS_DISPATCH_NORMAL); } } // namespace mozilla