Bug 1063635 Part 1 - Add native code for OS.File.writeAtomic. r=smaug,Yoric

MozReview-Commit-ID: 2TKZh6jCsq5
This commit is contained in:
MilindL
2017-06-27 13:10:11 +05:30
parent 81aab8ae8c
commit 1534c266e7
3 changed files with 453 additions and 0 deletions

View File

@@ -26,6 +26,7 @@
#include "mozilla/Scoped.h"
#include "mozilla/HoldDropJSObjects.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/UniquePtr.h"
#include "prio.h"
#include "prerror.h"
@@ -33,6 +34,7 @@
#include "jsapi.h"
#include "jsfriendapi.h"
#include "js/Conversions.h"
#include "js/Utility.h"
#include "xpcpublic.h"
@@ -130,11 +132,13 @@ private:
// 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
@@ -381,6 +385,46 @@ TypedArrayResult::GetCacheableResult(JSContext* cx, JS::MutableHandle<JS::Value>
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
/**
@@ -861,6 +905,263 @@ protected:
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
@@ -923,4 +1224,92 @@ NativeOSFileInternalsService::Read(const nsAString& aPath,
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