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

MozReview-Commit-ID: 2TKZh6jCsq5

--HG--
extra : rebase_source : 91972f346b038044cca1a70b8b5ec69cea5cd54e
This commit is contained in:
MilindL 2017-06-27 13:10:11 +05:30
parent 9681e0465c
commit 80419f5161
3 changed files with 453 additions and 0 deletions

View File

@ -19,3 +19,40 @@ dictionary NativeOSFileReadOptions
*/
unsigned long long? bytes;
};
/**
* Options for nsINativeOSFileInternals::WriteAtomic
*/
dictionary NativeOSFileWriteAtomicOptions
{
/**
* If specified, specify the number of bytes to write.
* NOTE: This takes (and should take) a uint64 here but the actual
* value is limited to int32. This needs to be fixed, see Bug 1063635.
*/
unsigned long long? bytes;
/**
* If specified, write all data to a temporary file in the
* |tmpPath|. Else, write to the given path directly.
*/
DOMString? tmpPath = null;
/**
* If specified and true, a failure will occur if the file
* already exists in the given path.
*/
boolean noOverwrite = false;
/**
* If specified and true, this will sync any buffered data
* for the file to disk. This might be slower, but safer.
*/
boolean flush = false;
/**
* If specified, this will backup the destination file as
* specified.
*/
DOMString? backupTo = null;
};

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

View File

@ -82,6 +82,33 @@ interface nsINativeOSFileInternalsService: nsISupports
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);
};