--- a/toolkit/components/osfile/NativeOSFileInternals.cpp
+++ b/toolkit/components/osfile/NativeOSFileInternals.cpp
@@ -21,23 +21,25 @@
#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/Conversions.h"
#include "js/Utility.h"
#include "xpcpublic.h"
#include <algorithm>
#if defined(XP_UNIX)
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
@@ -125,21 +127,23 @@ private:
};
///////// 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
@@ -376,16 +380,56 @@ TypedArrayResult::GetCacheableResult(JSC
// have a context, attach the memory to where it belongs.
JS_updateMallocCounter(cx, contents.nbytes);
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::MutableHandleValue aResult) override;
+private:
+ int32_t mContents;
+};
+
+nsresult
+Int32Result::GetCacheableResult(JSContext* cx, JS::MutableHandleValue 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 ErrorEvent final : public Runnable {
public:
/**
@@ -856,16 +900,273 @@ protected:
}
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> 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(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_ReleaseOnMainThreadSystemGroup("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(NS_LITERAL_CSTRING("noOverwrite"), 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(NS_LITERAL_CSTRING("delete"), nullptr, ::GetLastError());
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (::MoveFileW(mPath.get(), mBackupTo.get()) == false) {
+ Fail(NS_LITERAL_CSTRING("rename"), 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(NS_LITERAL_CSTRING("delete"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (PR_Rename(path.get(), backupTo.get()) == PR_FAILURE) {
+ Fail(NS_LITERAL_CSTRING("rename"), 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(NS_LITERAL_CSTRING("open"), nullptr, ::GetLastError());
+ return NS_ERROR_FAILURE;
+ }
+
+ file = PR_ImportFile((PROsfd)handle);
+ if (!file) {
+ // |file| is closed by PR_ImportFile
+ Fail(NS_LITERAL_CSTRING("ImportFile"), 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(NS_LITERAL_CSTRING("open"), 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(NS_LITERAL_CSTRING("write"), 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(NS_LITERAL_CSTRING("delete"), nullptr, ::GetLastError());
+ return NS_ERROR_FAILURE;
+ }
+#else
+ if (PR_Delete(path.get()) == PR_FAILURE) {
+ Fail(NS_LITERAL_CSTRING("delete"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+#endif // defined(XP_WIN)
+ }
+
+#if defined(XP_WIN)
+ if (::MoveFileW(mTmpPath.get(), mPath.get()) == false) {
+ Fail(NS_LITERAL_CSTRING("rename"), nullptr, ::GetLastError());
+ return NS_ERROR_FAILURE;
+ }
+#else
+ if(PR_Rename(tmpPath.get(), path.get()) == PR_FAILURE) {
+ Fail(NS_LITERAL_CSTRING("rename"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+#endif // defined(XP_WIN)
+ }
+
+ if (mFlush) {
+ if (PR_Sync(file) == PR_FAILURE) {
+ Fail(NS_LITERAL_CSTRING("sync"), 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> 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,
@@ -918,9 +1219,97 @@ NativeOSFileInternalsService::Read(const
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::HandleValue aBuffer,
+ JS::HandleValue 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> buffer;
+ int32_t bytes;
+
+ // The incoming buffer must be an Object.
+ if (!aBuffer.isObject()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::RootedObject bufferObject(cx, nullptr);
+ if (!JS_ValueToObject(cx, aBuffer, &bufferObject)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_IsArrayBufferObject(bufferObject.get())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bytes = JS_GetArrayBufferByteLength(bufferObject.get());
+ 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,
+ 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
--- a/toolkit/components/osfile/nsINativeOSFileInternals.idl
+++ b/toolkit/components/osfile/nsINativeOSFileInternals.idl
@@ -77,16 +77,43 @@ interface nsINativeOSFileInternalsServic
* - {string} compression Unimplemented at the moment.
* @param onSuccess The success callback.
* @param onError The error callback.
*/
[implicit_jscontext]
void read(in AString path, in jsval options,
in nsINativeOSFileSuccessCallback onSuccess,
in nsINativeOSFileErrorCallback onError);
+
+ /**
+ * Implementation of OS.File.writeAtomic
+ *
+ * @param path the absolute path of the file to write to.
+ * @param buffer the data as an array buffer to be written to the file.
+ * @param options An object that may contain the following fields
+ * - {number} bytes If provided, the number of bytes written is equal to this.
+ * The default value is the size of the |buffer|.
+ * - {string} tmpPath If provided and not null, first write to this path, and
+ * move to |path| after writing.
+ * - {string} backupPath if provided, backup file at |path| to this path
+ * before overwriting it.
+ * - {bool} flush if provided and true, flush the contents of the buffer after
+ * writing. This is slower, but safer.
+ * - {bool} noOverwrite if provided and true, do not write if a file already
+ * exists at |path|.
+ * @param onSuccess The success callback.
+ * @param onError The error callback.
+ */
+ [implicit_jscontext]
+ void writeAtomic(in AString path,
+ in jsval buffer,
+ in jsval options,
+ in nsINativeOSFileSuccessCallback onSuccess,
+ in nsINativeOSFileErrorCallback onError);
+
};
%{ C++
#define NATIVE_OSFILE_INTERNALS_SERVICE_CID {0x63A69303,0x8A64,0x45A9,{0x84, 0x8C, 0xD4, 0xE2, 0x79, 0x27, 0x94, 0xE6}}
#define NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID "@mozilla.org/toolkit/osfile/native-internals;1"