summaryrefslogtreecommitdiffstats
path: root/toolkit/components/osfile
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/osfile')
-rw-r--r--toolkit/components/osfile/NativeOSFileInternals.cpp916
-rw-r--r--toolkit/components/osfile/NativeOSFileInternals.h25
-rw-r--r--toolkit/components/osfile/modules/moz.build22
-rw-r--r--toolkit/components/osfile/modules/osfile_async_front.jsm1533
-rw-r--r--toolkit/components/osfile/modules/osfile_async_worker.js407
-rw-r--r--toolkit/components/osfile/modules/osfile_native.jsm70
-rw-r--r--toolkit/components/osfile/modules/osfile_shared_allthreads.jsm1315
-rw-r--r--toolkit/components/osfile/modules/osfile_shared_front.jsm567
-rw-r--r--toolkit/components/osfile/modules/osfile_unix_allthreads.jsm375
-rw-r--r--toolkit/components/osfile/modules/osfile_unix_back.jsm735
-rw-r--r--toolkit/components/osfile/modules/osfile_unix_front.jsm1193
-rw-r--r--toolkit/components/osfile/modules/osfile_win_allthreads.jsm425
-rw-r--r--toolkit/components/osfile/modules/osfile_win_back.jsm437
-rw-r--r--toolkit/components/osfile/modules/osfile_win_front.jsm1266
-rw-r--r--toolkit/components/osfile/modules/ospath.jsm45
-rw-r--r--toolkit/components/osfile/modules/ospath_unix.jsm202
-rw-r--r--toolkit/components/osfile/modules/ospath_win.jsm373
-rw-r--r--toolkit/components/osfile/moz.build35
-rw-r--r--toolkit/components/osfile/nsINativeOSFileInternals.idl93
-rw-r--r--toolkit/components/osfile/osfile.jsm32
-rw-r--r--toolkit/components/osfile/tests/mochi/.eslintrc.js7
-rw-r--r--toolkit/components/osfile/tests/mochi/chrome.ini15
-rw-r--r--toolkit/components/osfile/tests/mochi/main_test_osfile_async.js443
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_async.xul23
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_back.xul46
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_comms.xul84
-rw-r--r--toolkit/components/osfile/tests/mochi/test_osfile_front.xul44
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_handler.js34
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js145
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js566
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js32
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js201
-rw-r--r--toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js211
-rw-r--r--toolkit/components/osfile/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/osfile/tests/xpcshell/head.js99
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_available_free_space.js38
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_compression.js98
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_constants.js31
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_creationDate.js31
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_duration.js91
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_exception.js89
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js114
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_loader.js37
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js9
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_logging.js74
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_makeDir.js142
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_open.js70
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async.js16
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js122
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js39
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js113
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js30
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js153
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js211
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js103
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js48
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_error.js63
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js100
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js114
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js143
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js27
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path.js159
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_path_constants.js83
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_queue.js38
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_read_write.js103
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_remove.js56
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_removeDir.js177
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js55
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_reset.js95
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_shutdown.js98
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_telemetry.js63
-rw-r--r--toolkit/components/osfile/tests/xpcshell/test_unique.js88
-rw-r--r--toolkit/components/osfile/tests/xpcshell/xpcshell.ini51
73 files changed, 15195 insertions, 0 deletions
diff --git a/toolkit/components/osfile/NativeOSFileInternals.cpp b/toolkit/components/osfile/NativeOSFileInternals.cpp
new file mode 100644
index 000000000..e4725d390
--- /dev/null
+++ b/toolkit/components/osfile/NativeOSFileInternals.cpp
@@ -0,0 +1,916 @@
+/* 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 "nsString.h"
+#include "nsNetCID.h"
+#include "nsThreadUtils.h"
+#include "nsXPCOMCID.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsServiceManagerUtils.h"
+#include "nsProxyRelease.h"
+
+#include "nsINativeOSFileInternals.h"
+#include "NativeOSFileInternals.h"
+#include "mozilla/dom/NativeOSFileInternalsBinding.h"
+
+#include "nsIUnicodeDecoder.h"
+#include "nsIEventTarget.h"
+
+#include "mozilla/dom/EncodingUtils.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Scoped.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/TimeStamp.h"
+
+#include "prio.h"
+#include "prerror.h"
+#include "private/pprio.h"
+
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Utility.h"
+#include "xpcpublic.h"
+
+#include <algorithm>
+#if defined(XP_UNIX)
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/uio.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(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM):
+ Scoped<ScopedArrayBufferContentsTraits>(MOZ_GUARD_OBJECT_NOTIFIER_ONLY_PARAM_TO_PARENT)
+ { }
+ explicit ScopedArrayBufferContents(const ArrayBufferContents& v
+ MOZ_GUARD_OBJECT_NOTIFIER_PARAM):
+ Scoped<ScopedArrayBufferContentsTraits>(v MOZ_GUARD_OBJECT_NOTIFIER_PARAM_TO_PARENT)
+ { }
+ 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_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_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());
+ DropJSData();
+ mozilla::DropJSObjects(this);
+ }
+
+ virtual nsresult GetCacheableResult(JSContext *cx, JS::MutableHandleValue 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_SCRIPT_OBJECTS
+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::MutableHandleValue 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::MutableHandleValue aResult) override;
+
+private:
+ nsString mContents;
+};
+
+nsresult
+StringResult::GetCacheableResult(JSContext* cx, JS::MutableHandleValue 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::MutableHandleValue 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);
+
+ 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;
+ }
+ // The memory of contents has been allocated on a thread that
+ // doesn't have a JSRuntime, hence without a context. Now that we
+ // have a context, attach the memory to where it belongs.
+ JS_updateMallocCounter(cx, contents.nbytes);
+ mContents.forget();
+
+ aResult.setObject(*result);
+ return NS_OK;
+}
+
+//////// Callback events
+
+/**
+ * An event used to notify asynchronously of an error.
+ */
+class ErrorEvent 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.
+ */
+ ErrorEvent(nsMainThreadPtrHandle<nsINativeOSFileSuccessCallback>& aOnSuccess,
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback>& aOnError,
+ already_AddRefed<AbstractResult>& aDiscardedResult,
+ const nsACString& aOperation,
+ int32_t aOSError)
+ : 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)
+ : 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)
+ : 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<ErrorEvent> event = new ErrorEvent(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(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(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(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
+ // On other platforms, PR_OpenFile will do.
+ NS_ConvertUTF16toUTF8 path(mPath);
+ file = PR_OpenFile(path.get(), PR_RDONLY, 0);
+ if (!file) {
+ Fail(NS_LITERAL_CSTRING("open"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+
+#endif // defined(XP_XIN)
+
+ PRFileInfo64 stat;
+ if (PR_GetOpenFileInfo64(file, &stat) != PR_SUCCESS) {
+ Fail(NS_LITERAL_CSTRING("stat"), nullptr, PR_GetOSError());
+ return NS_ERROR_FAILURE;
+ }
+
+ uint64_t bytes = std::min((uint64_t)stat.size, mBytes);
+ if (bytes > UINT32_MAX) {
+ Fail(NS_LITERAL_CSTRING("Arithmetics"), nullptr, OS_ERROR_INVAL);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!aBuffer.Allocate(bytes)) {
+ Fail(NS_LITERAL_CSTRING("allocate"), 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(NS_LITERAL_CSTRING("read"), 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(NS_LITERAL_CSTRING("read"), 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() {
+ // If AbstractReadEvent::Run() has bailed out, we may need to cleanup
+ // mResult, which is main-thread only data
+ if (!mResult) {
+ return;
+ }
+ NS_ReleaseOnMainThread(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() {
+ // If AbstraactReadEvent::Run() has bailed out, we may need to cleanup
+ // mResult, which is main-thread only data
+ if (!mResult) {
+ return;
+ }
+ NS_ReleaseOnMainThread(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());
+ nsAutoCString encodingName;
+ if (!dom::EncodingUtils::FindEncodingForLabel(mEncoding, encodingName)) {
+ Fail(NS_LITERAL_CSTRING("Decode"), mResult.forget(), OS_ERROR_INVAL);
+ return NS_ERROR_FAILURE;
+ }
+ mDecoder = dom::EncodingUtils::DecoderForEncoding(encodingName);
+ if (!mDecoder) {
+ Fail(NS_LITERAL_CSTRING("DecoderForEncoding"), mResult.forget(), OS_ERROR_INVAL);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+ }
+
+ void AfterRead(TimeStamp aDispatchDate,
+ ScopedArrayBufferContents& aBuffer) override {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ int32_t maxChars;
+ const char* sourceChars = reinterpret_cast<const char*>(aBuffer.get().data);
+ int32_t sourceBytes = aBuffer.get().nbytes;
+ if (sourceBytes < 0) {
+ Fail(NS_LITERAL_CSTRING("arithmetics"), mResult.forget(), OS_ERROR_TOO_LARGE);
+ return;
+ }
+
+ nsresult rv = mDecoder->GetMaxLength(sourceChars, sourceBytes, &maxChars);
+ if (NS_FAILED(rv)) {
+ Fail(NS_LITERAL_CSTRING("GetMaxLength"), mResult.forget(), OS_ERROR_INVAL);
+ return;
+ }
+
+ if (maxChars < 0) {
+ Fail(NS_LITERAL_CSTRING("arithmetics"), mResult.forget(), OS_ERROR_TOO_LARGE);
+ return;
+ }
+
+ nsString resultString;
+ resultString.SetLength(maxChars);
+ if (resultString.Length() != (nsString::size_type)maxChars) {
+ Fail(NS_LITERAL_CSTRING("allocation"), mResult.forget(), OS_ERROR_TOO_LARGE);
+ return;
+ }
+
+
+ rv = mDecoder->Convert(sourceChars, &sourceBytes,
+ resultString.BeginWriting(), &maxChars);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ resultString.SetLength(maxChars);
+
+ mResult->Init(aDispatchDate, TimeStamp::Now() - aDispatchDate, resultString);
+ Succeed(mResult.forget());
+ }
+
+ private:
+ nsCString mEncoding;
+ nsCOMPtr<nsIUnicodeDecoder> mDecoder;
+ RefPtr<StringResult> mResult;
+};
+
+} // namespace
+
+// The OS.File service
+
+NS_IMPL_ISUPPORTS(NativeOSFileInternalsService, nsINativeOSFileInternalsService);
+
+NS_IMETHODIMP
+NativeOSFileInternalsService::Read(const nsAString& aPath,
+ JS::HandleValue 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>(onSuccess));
+ nsCOMPtr<nsINativeOSFileErrorCallback> onError(aOnError);
+ nsMainThreadPtrHandle<nsINativeOSFileErrorCallback> onErrorHandle(
+ new nsMainThreadPtrHolder<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);
+}
+
+} // namespace mozilla
+
diff --git a/toolkit/components/osfile/NativeOSFileInternals.h b/toolkit/components/osfile/NativeOSFileInternals.h
new file mode 100644
index 000000000..2da929357
--- /dev/null
+++ b/toolkit/components/osfile/NativeOSFileInternals.h
@@ -0,0 +1,25 @@
+/* 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/. */
+
+#ifndef mozilla_nativeosfileinternalservice_h__
+#define mozilla_nativeosfileinternalservice_h__
+
+#include "nsINativeOSFileInternals.h"
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+
+class NativeOSFileInternalsService final : public nsINativeOSFileInternalsService {
+public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINATIVEOSFILEINTERNALSSERVICE
+private:
+ ~NativeOSFileInternalsService() {}
+ // Avoid accidental use of built-in operator=
+ void operator=(const NativeOSFileInternalsService& other) = delete;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_finalizationwitnessservice_h__
diff --git a/toolkit/components/osfile/modules/moz.build b/toolkit/components/osfile/modules/moz.build
new file mode 100644
index 000000000..7a0580ca3
--- /dev/null
+++ b/toolkit/components/osfile/modules/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXTRA_JS_MODULES.osfile += [
+ 'osfile_async_front.jsm',
+ 'osfile_async_worker.js',
+ 'osfile_native.jsm',
+ 'osfile_shared_allthreads.jsm',
+ 'osfile_shared_front.jsm',
+ 'osfile_unix_allthreads.jsm',
+ 'osfile_unix_back.jsm',
+ 'osfile_unix_front.jsm',
+ 'osfile_win_allthreads.jsm',
+ 'osfile_win_back.jsm',
+ 'osfile_win_front.jsm',
+ 'ospath.jsm',
+ 'ospath_unix.jsm',
+ 'ospath_win.jsm',
+]
diff --git a/toolkit/components/osfile/modules/osfile_async_front.jsm b/toolkit/components/osfile/modules/osfile_async_front.jsm
new file mode 100644
index 000000000..181471cd8
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -0,0 +1,1533 @@
+/* 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/. */
+
+/**
+ * Asynchronous front-end for OS.File.
+ *
+ * This front-end is meant to be imported from the main thread. In turn,
+ * it spawns one worker (perhaps more in the future) and delegates all
+ * disk I/O to this worker.
+ *
+ * Documentation note: most of the functions and methods in this module
+ * return promises. For clarity, we denote as follows a promise that may resolve
+ * with type |A| and some value |value| or reject with type |B| and some
+ * reason |reason|
+ * @resolves {A} value
+ * @rejects {B} reason
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["OS"];
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+
+var SharedAll = {};
+Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/Timer.jsm", this);
+
+
+// Boilerplate, to simplify the transition to require()
+var LOG = SharedAll.LOG.bind(SharedAll, "Controller");
+var isTypedArray = SharedAll.isTypedArray;
+
+// The constructor for file errors.
+var SysAll = {};
+if (SharedAll.Constants.Win) {
+ Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll);
+} else if (SharedAll.Constants.libc) {
+ Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll);
+} else {
+ throw new Error("I am neither under Windows nor under a Posix system");
+}
+var OSError = SysAll.Error;
+var Type = SysAll.Type;
+
+var Path = {};
+Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
+
+// The library of promises.
+Cu.import("resource://gre/modules/Promise.jsm", this);
+Cu.import("resource://gre/modules/Task.jsm", this);
+
+// The implementation of communications
+Cu.import("resource://gre/modules/PromiseWorker.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
+Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
+var Native = Cu.import("resource://gre/modules/osfile/osfile_native.jsm", {});
+
+
+// It's possible for osfile.jsm to get imported before the profile is
+// set up. In this case, some path constants aren't yet available.
+// Here, we make them lazy loaders.
+
+function lazyPathGetter(constProp, dirKey) {
+ return function () {
+ let path;
+ try {
+ path = Services.dirsvc.get(dirKey, Ci.nsIFile).path;
+ delete SharedAll.Constants.Path[constProp];
+ SharedAll.Constants.Path[constProp] = path;
+ } catch (ex) {
+ // Ignore errors if the value still isn't available. Hopefully
+ // the next access will return it.
+ }
+
+ return path;
+ };
+}
+
+for (let [constProp, dirKey] of [
+ ["localProfileDir", "ProfLD"],
+ ["profileDir", "ProfD"],
+ ["userApplicationDataDir", "UAppData"],
+ ["winAppDataDir", "AppData"],
+ ["winStartMenuProgsDir", "Progs"],
+ ]) {
+
+ if (constProp in SharedAll.Constants.Path) {
+ continue;
+ }
+
+ LOG("Installing lazy getter for OS.Constants.Path." + constProp +
+ " because it isn't defined and profile may not be loaded.");
+ Object.defineProperty(SharedAll.Constants.Path, constProp, {
+ get: lazyPathGetter(constProp, dirKey),
+ });
+}
+
+/**
+ * Return a shallow clone of the enumerable properties of an object.
+ */
+var clone = SharedAll.clone;
+
+/**
+ * Extract a shortened version of an object, fit for logging.
+ *
+ * This function returns a copy of the original object in which all
+ * long strings, Arrays, TypedArrays, ArrayBuffers are removed and
+ * replaced with placeholders. Use this function to sanitize objects
+ * if you wish to log them or to keep them in memory.
+ *
+ * @param {*} obj The obj to shorten.
+ * @return {*} array A shorter object, fit for logging.
+ */
+function summarizeObject(obj) {
+ if (!obj) {
+ return null;
+ }
+ if (typeof obj == "string") {
+ if (obj.length > 1024) {
+ return {"Long string": obj.length};
+ }
+ return obj;
+ }
+ if (typeof obj == "object") {
+ if (Array.isArray(obj)) {
+ if (obj.length > 32) {
+ return {"Long array": obj.length};
+ }
+ return obj.map(summarizeObject);
+ }
+ if ("byteLength" in obj) {
+ // Assume TypedArray or ArrayBuffer
+ return {"Binary Data": obj.byteLength};
+ }
+ let result = {};
+ for (let k of Object.keys(obj)) {
+ result[k] = summarizeObject(obj[k]);
+ }
+ return result;
+ }
+ return obj;
+}
+
+// In order to expose Scheduler to the unfiltered Cu.import return value variant
+// on B2G we need to save it to `this`. This does not make it public;
+// EXPORTED_SYMBOLS still controls that in all cases.
+var Scheduler = this.Scheduler = {
+
+ /**
+ * |true| once we have sent at least one message to the worker.
+ * This field is unaffected by resetting the worker.
+ */
+ launched: false,
+
+ /**
+ * |true| once shutdown has begun i.e. we should reject any
+ * message, including resets.
+ */
+ shutdown: false,
+
+ /**
+ * A promise resolved once all currently pending operations are complete.
+ *
+ * This promise is never rejected and the result is always undefined.
+ */
+ queue: Promise.resolve(),
+
+ /**
+ * A promise resolved once all currently pending `kill` operations
+ * are complete.
+ *
+ * This promise is never rejected and the result is always undefined.
+ */
+ _killQueue: Promise.resolve(),
+
+ /**
+ * Miscellaneous debugging information
+ */
+ Debugging: {
+ /**
+ * The latest message sent and still waiting for a reply.
+ */
+ latestSent: undefined,
+
+ /**
+ * The latest reply received, or null if we are waiting for a reply.
+ */
+ latestReceived: undefined,
+
+ /**
+ * Number of messages sent to the worker. This includes the
+ * initial SET_DEBUG, if applicable.
+ */
+ messagesSent: 0,
+
+ /**
+ * Total number of messages ever queued, including the messages
+ * sent.
+ */
+ messagesQueued: 0,
+
+ /**
+ * Number of messages received from the worker.
+ */
+ messagesReceived: 0,
+ },
+
+ /**
+ * A timer used to automatically shut down the worker after some time.
+ */
+ resetTimer: null,
+
+ /**
+ * The worker to which to send requests.
+ *
+ * If the worker has never been created or has been reset, this is a
+ * fresh worker, initialized with osfile_async_worker.js.
+ *
+ * @type {PromiseWorker}
+ */
+ get worker() {
+ if (!this._worker) {
+ // Either the worker has never been created or it has been
+ // reset. In either case, it is time to instantiate the worker.
+ this._worker = new BasePromiseWorker("resource://gre/modules/osfile/osfile_async_worker.js");
+ this._worker.log = LOG;
+ this._worker.ExceptionHandlers["OS.File.Error"] = OSError.fromMsg;
+ }
+ return this._worker;
+ },
+
+ _worker: null,
+
+ /**
+ * Prepare to kill the OS.File worker after a few seconds.
+ */
+ restartTimer: function(arg) {
+ let delay;
+ try {
+ delay = Services.prefs.getIntPref("osfile.reset_worker_delay");
+ } catch(e) {
+ // Don't auto-shutdown if we don't have a delay preference set.
+ return;
+ }
+
+ if (this.resetTimer) {
+ clearTimeout(this.resetTimer);
+ }
+ this.resetTimer = setTimeout(
+ () => Scheduler.kill({reset: true, shutdown: false}),
+ delay
+ );
+ },
+
+ /**
+ * Shutdown OS.File.
+ *
+ * @param {*} options
+ * - {boolean} shutdown If |true|, reject any further request. Otherwise,
+ * further requests will resurrect the worker.
+ * - {boolean} reset If |true|, instruct the worker to shutdown if this
+ * would not cause leaks. Otherwise, assume that the worker will be shutdown
+ * through some other mean.
+ */
+ kill: function({shutdown, reset}) {
+ // Grab the kill queue to make sure that we
+ // cannot be interrupted by another call to `kill`.
+ let killQueue = this._killQueue;
+
+ // Deactivate the queue, to ensure that no message is sent
+ // to an obsolete worker (we reactivate it in the `finally`).
+ // This needs to be done right now so that we maintain relative
+ // ordering with calls to post(), etc.
+ let deferred = Promise.defer();
+ let savedQueue = this.queue;
+ this.queue = deferred.promise;
+
+ return this._killQueue = Task.spawn(function*() {
+
+ yield killQueue;
+ // From this point, and until the end of the Task, we are the
+ // only call to `kill`, regardless of any `yield`.
+
+ yield savedQueue;
+
+ try {
+ // Enter critical section: no yield in this block
+ // (we want to make sure that we remain the only
+ // request in the queue).
+
+ if (!this.launched || this.shutdown || !this._worker) {
+ // Nothing to kill
+ this.shutdown = this.shutdown || shutdown;
+ this._worker = null;
+ return null;
+ }
+
+ // Exit critical section
+
+ let message = ["Meta_shutdown", [reset]];
+
+ Scheduler.latestReceived = [];
+ Scheduler.latestSent = [Date.now(),
+ Task.Debugging.generateReadableStack(new Error().stack),
+ ...message];
+
+ // Wait for result
+ let resources;
+ try {
+ resources = yield this._worker.post(...message);
+
+ Scheduler.latestReceived = [Date.now(), message];
+ } catch (ex) {
+ LOG("Could not dispatch Meta_reset", ex);
+ // It's most likely a programmer error, but we'll assume that
+ // the worker has been shutdown, as it's less risky than the
+ // opposite stance.
+ resources = {openedFiles: [], openedDirectoryIterators: [], killed: true};
+
+ Scheduler.latestReceived = [Date.now(), message, ex];
+ }
+
+ let {openedFiles, openedDirectoryIterators, killed} = resources;
+ if (!reset
+ && (openedFiles && openedFiles.length
+ || ( openedDirectoryIterators && openedDirectoryIterators.length))) {
+ // The worker still holds resources. Report them.
+
+ let msg = "";
+ if (openedFiles.length > 0) {
+ msg += "The following files are still open:\n" +
+ openedFiles.join("\n");
+ }
+ if (openedDirectoryIterators.length > 0) {
+ msg += "The following directory iterators are still open:\n" +
+ openedDirectoryIterators.join("\n");
+ }
+
+ LOG("WARNING: File descriptors leaks detected.\n" + msg);
+ }
+
+ // Make sure that we do not leave an invalid |worker| around.
+ if (killed || shutdown) {
+ this._worker = null;
+ }
+
+ this.shutdown = shutdown;
+
+ return resources;
+
+ } finally {
+ // Resume accepting messages. If we have set |shutdown| to |true|,
+ // any pending/future request will be rejected. Otherwise, any
+ // pending/future request will spawn a new worker if necessary.
+ deferred.resolve();
+ }
+
+ }.bind(this));
+ },
+
+ /**
+ * Push a task at the end of the queue.
+ *
+ * @param {function} code A function returning a Promise.
+ * This function will be executed once all the previously
+ * pushed tasks have completed.
+ * @return {Promise} A promise with the same behavior as
+ * the promise returned by |code|.
+ */
+ push: function(code) {
+ let promise = this.queue.then(code);
+ // By definition, |this.queue| can never reject.
+ this.queue = promise.then(null, () => undefined);
+ // Fork |promise| to ensure that uncaught errors are reported
+ return promise.then(null, null);
+ },
+
+ /**
+ * Post a message to the worker thread.
+ *
+ * @param {string} method The name of the method to call.
+ * @param {...} args The arguments to pass to the method. These arguments
+ * must be clonable.
+ * @return {Promise} A promise conveying the result/error caused by
+ * calling |method| with arguments |args|.
+ */
+ post: function post(method, args = undefined, closure = undefined) {
+ if (this.shutdown) {
+ LOG("OS.File is not available anymore. The following request has been rejected.",
+ method, args);
+ return Promise.reject(new Error("OS.File has been shut down. Rejecting post to " + method));
+ }
+ let firstLaunch = !this.launched;
+ this.launched = true;
+
+ if (firstLaunch && SharedAll.Config.DEBUG) {
+ // If we have delayed sending SET_DEBUG, do it now.
+ this.worker.post("SET_DEBUG", [true]);
+ Scheduler.Debugging.messagesSent++;
+ }
+
+ Scheduler.Debugging.messagesQueued++;
+ return this.push(Task.async(function*() {
+ if (this.shutdown) {
+ LOG("OS.File is not available anymore. The following request has been rejected.",
+ method, args);
+ throw new Error("OS.File has been shut down. Rejecting request to " + method);
+ }
+
+ // Update debugging information. As |args| may be quite
+ // expensive, we only keep a shortened version of it.
+ Scheduler.Debugging.latestReceived = null;
+ Scheduler.Debugging.latestSent = [Date.now(), method, summarizeObject(args)];
+
+ // Don't kill the worker just yet
+ Scheduler.restartTimer();
+
+
+ let reply;
+ try {
+ try {
+ Scheduler.Debugging.messagesSent++;
+ Scheduler.Debugging.latestSent = Scheduler.Debugging.latestSent.slice(0, 2);
+ reply = yield this.worker.post(method, args, closure);
+ Scheduler.Debugging.latestReceived = [Date.now(), summarizeObject(reply)];
+ return reply;
+ } finally {
+ Scheduler.Debugging.messagesReceived++;
+ }
+ } catch (error) {
+ Scheduler.Debugging.latestReceived = [Date.now(), error.message, error.fileName, error.lineNumber];
+ throw error;
+ } finally {
+ if (firstLaunch) {
+ Scheduler._updateTelemetry();
+ }
+ Scheduler.restartTimer();
+ }
+ }.bind(this)));
+ },
+
+ /**
+ * Post Telemetry statistics.
+ *
+ * This is only useful on first launch.
+ */
+ _updateTelemetry: function() {
+ let worker = this.worker;
+ let workerTimeStamps = worker.workerTimeStamps;
+ if (!workerTimeStamps) {
+ // If the first call to OS.File results in an uncaught errors,
+ // the timestamps are absent. As this case is a developer error,
+ // let's not waste time attempting to extract telemetry from it.
+ return;
+ }
+ let HISTOGRAM_LAUNCH = Services.telemetry.getHistogramById("OSFILE_WORKER_LAUNCH_MS");
+ HISTOGRAM_LAUNCH.add(worker.workerTimeStamps.entered - worker.launchTimeStamp);
+
+ let HISTOGRAM_READY = Services.telemetry.getHistogramById("OSFILE_WORKER_READY_MS");
+ HISTOGRAM_READY.add(worker.workerTimeStamps.loaded - worker.launchTimeStamp);
+ }
+};
+
+const PREF_OSFILE_LOG = "toolkit.osfile.log";
+const PREF_OSFILE_LOG_REDIRECT = "toolkit.osfile.log.redirect";
+
+/**
+ * Safely read a PREF_OSFILE_LOG preference.
+ * Returns a value read or, in case of an error, oldPref or false.
+ *
+ * @param bool oldPref
+ * An optional value that the DEBUG flag was set to previously.
+ */
+function readDebugPref(prefName, oldPref = false) {
+ let pref = oldPref;
+ try {
+ pref = Services.prefs.getBoolPref(prefName);
+ } catch (x) {
+ // In case of an error when reading a pref keep it as is.
+ }
+ // If neither pref nor oldPref were set, default it to false.
+ return pref;
+};
+
+/**
+ * Listen to PREF_OSFILE_LOG changes and update gShouldLog flag
+ * appropriately.
+ */
+Services.prefs.addObserver(PREF_OSFILE_LOG,
+ function prefObserver(aSubject, aTopic, aData) {
+ SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, SharedAll.Config.DEBUG);
+ if (Scheduler.launched) {
+ // Don't start the worker just to set this preference.
+ Scheduler.post("SET_DEBUG", [SharedAll.Config.DEBUG]);
+ }
+ }, false);
+SharedAll.Config.DEBUG = readDebugPref(PREF_OSFILE_LOG, false);
+
+Services.prefs.addObserver(PREF_OSFILE_LOG_REDIRECT,
+ function prefObserver(aSubject, aTopic, aData) {
+ SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, OS.Shared.TEST);
+ }, false);
+SharedAll.Config.TEST = readDebugPref(PREF_OSFILE_LOG_REDIRECT, false);
+
+
+/**
+ * If |true|, use the native implementaiton of OS.File methods
+ * whenever possible. Otherwise, force the use of the JS version.
+ */
+var nativeWheneverAvailable = true;
+const PREF_OSFILE_NATIVE = "toolkit.osfile.native";
+Services.prefs.addObserver(PREF_OSFILE_NATIVE,
+ function prefObserver(aSubject, aTopic, aData) {
+ nativeWheneverAvailable = readDebugPref(PREF_OSFILE_NATIVE, nativeWheneverAvailable);
+ }, false);
+
+
+// Update worker's DEBUG flag if it's true.
+// Don't start the worker just for this, though.
+if (SharedAll.Config.DEBUG && Scheduler.launched) {
+ Scheduler.post("SET_DEBUG", [true]);
+}
+
+// Observer topics used for monitoring shutdown
+const WEB_WORKERS_SHUTDOWN_TOPIC = "web-workers-shutdown";
+
+// Preference used to configure test shutdown observer.
+const PREF_OSFILE_TEST_SHUTDOWN_OBSERVER =
+ "toolkit.osfile.test.shutdown.observer";
+
+AsyncShutdown.webWorkersShutdown.addBlocker(
+ "OS.File: flush pending requests, warn about unclosed files, shut down service.",
+ Task.async(function*() {
+ // Give clients a last chance to enqueue requests.
+ yield Barriers.shutdown.wait({crashAfterMS: null});
+
+ // Wait until all requests are complete and kill the worker.
+ yield Scheduler.kill({reset: false, shutdown: true});
+ }),
+ () => {
+ let details = Barriers.getDetails();
+ details.clients = Barriers.shutdown.state;
+ return details;
+ }
+);
+
+
+// Attaching an observer for PREF_OSFILE_TEST_SHUTDOWN_OBSERVER to enable or
+// disable the test shutdown event observer.
+// Note: By default the PREF_OSFILE_TEST_SHUTDOWN_OBSERVER is unset.
+// Note: This is meant to be used for testing purposes only.
+Services.prefs.addObserver(PREF_OSFILE_TEST_SHUTDOWN_OBSERVER,
+ function prefObserver() {
+ // The temporary phase topic used to trigger the unclosed
+ // phase warning.
+ let TOPIC = null;
+ try {
+ TOPIC = Services.prefs.getCharPref(
+ PREF_OSFILE_TEST_SHUTDOWN_OBSERVER);
+ } catch (x) {
+ }
+ if (TOPIC) {
+ // Generate a phase, add a blocker.
+ // Note that this can work only if AsyncShutdown itself has been
+ // configured for testing by the testsuite.
+ let phase = AsyncShutdown._getPhase(TOPIC);
+ phase.addBlocker(
+ "(for testing purposes) OS.File: warn about unclosed files",
+ () => Scheduler.kill({shutdown: false, reset: false})
+ );
+ }
+ }, false);
+
+/**
+ * Representation of a file, with asynchronous methods.
+ *
+ * @param {*} fdmsg The _message_ representing the platform-specific file
+ * handle.
+ *
+ * @constructor
+ */
+var File = function File(fdmsg) {
+ // FIXME: At the moment, |File| does not close on finalize
+ // (see bug 777715)
+ this._fdmsg = fdmsg;
+ this._closeResult = null;
+ this._closed = null;
+};
+
+
+File.prototype = {
+ /**
+ * Close a file asynchronously.
+ *
+ * This method is idempotent.
+ *
+ * @return {promise}
+ * @resolves {null}
+ * @rejects {OS.File.Error}
+ */
+ close: function close() {
+ if (this._fdmsg != null) {
+ let msg = this._fdmsg;
+ this._fdmsg = null;
+ return this._closeResult =
+ Scheduler.post("File_prototype_close", [msg], this);
+ }
+ return this._closeResult;
+ },
+
+ /**
+ * Fetch information about the file.
+ *
+ * @return {promise}
+ * @resolves {OS.File.Info} The latest information about the file.
+ * @rejects {OS.File.Error}
+ */
+ stat: function stat() {
+ return Scheduler.post("File_prototype_stat", [this._fdmsg], this).then(
+ File.Info.fromMsg
+ );
+ },
+
+ /**
+ * Write bytes from a buffer to this file.
+ *
+ * Note that, by default, this function may perform several I/O
+ * operations to ensure that the buffer is fully written.
+ *
+ * @param {Typed array | C pointer} buffer The buffer in which the
+ * the bytes are stored. The buffer must be large enough to
+ * accomodate |bytes| bytes. Using the buffer before the operation
+ * is complete is a BAD IDEA.
+ * @param {*=} options Optionally, an object that may contain the
+ * following fields:
+ * - {number} bytes The number of |bytes| to write from the buffer. If
+ * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
+ * if |buffer| is a C pointer.
+ *
+ * @return {number} The number of bytes actually written.
+ */
+ write: function write(buffer, options = {}) {
+ // If |buffer| is a typed array and there is no |bytes| options,
+ // we need to extract the |byteLength| now, as it will be lost
+ // by communication.
+ // Options might be a nullish value, so better check for that before using
+ // the |in| operator.
+ if (isTypedArray(buffer) && !(options && "bytes" in options)) {
+ // Preserve reference to option |outExecutionDuration|, if it is passed.
+ options = clone(options, ["outExecutionDuration"]);
+ options.bytes = buffer.byteLength;
+ }
+ return Scheduler.post("File_prototype_write",
+ [this._fdmsg,
+ Type.void_t.in_ptr.toMsg(buffer),
+ options],
+ buffer/*Ensure that |buffer| is not gc-ed*/);
+ },
+
+ /**
+ * Read bytes from this file to a new buffer.
+ *
+ * @param {number=} bytes If unspecified, read all the remaining bytes from
+ * this file. If specified, read |bytes| bytes, or less if the file does not
+ * contain that many bytes.
+ * @param {JSON} options
+ * @return {promise}
+ * @resolves {Uint8Array} An array containing the bytes read.
+ */
+ read: function read(nbytes, options = {}) {
+ let promise = Scheduler.post("File_prototype_read",
+ [this._fdmsg,
+ nbytes, options]);
+ return promise.then(
+ function onSuccess(data) {
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ });
+ },
+
+ /**
+ * Return the current position in the file, as bytes.
+ *
+ * @return {promise}
+ * @resolves {number} The current position in the file,
+ * as a number of bytes since the start of the file.
+ */
+ getPosition: function getPosition() {
+ return Scheduler.post("File_prototype_getPosition",
+ [this._fdmsg]);
+ },
+
+ /**
+ * Set the current position in the file, as bytes.
+ *
+ * @param {number} pos A number of bytes.
+ * @param {number} whence The reference position in the file,
+ * which may be either POS_START (from the start of the file),
+ * POS_END (from the end of the file) or POS_CUR (from the
+ * current position in the file).
+ *
+ * @return {promise}
+ */
+ setPosition: function setPosition(pos, whence) {
+ return Scheduler.post("File_prototype_setPosition",
+ [this._fdmsg, pos, whence]);
+ },
+
+ /**
+ * Flushes the file's buffers and causes all buffered data
+ * to be written.
+ * Disk flushes are very expensive and therefore should be used carefully,
+ * sparingly and only in scenarios where it is vital that data survives
+ * system crashes. Even though the function will be executed off the
+ * main-thread, it might still affect the overall performance of any running
+ * application.
+ *
+ * @return {promise}
+ */
+ flush: function flush() {
+ return Scheduler.post("File_prototype_flush",
+ [this._fdmsg]);
+ },
+
+ /**
+ * Set the file's access permissions. This does nothing on Windows.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+ setPermissions: function setPermissions(options = {}) {
+ return Scheduler.post("File_prototype_setPermissions",
+ [this._fdmsg, options]);
+ }
+};
+
+
+if (SharedAll.Constants.Sys.Name != "Android" && SharedAll.Constants.Sys.Name != "Gonk") {
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * WARNING: This method is not implemented on Android/B2G. On Android/B2G,
+ * you should use File.setDates instead.
+ *
+ * @return {promise}
+ * @rejects {TypeError}
+ * @rejects {OS.File.Error}
+ */
+ File.prototype.setDates = function(accessDate, modificationDate) {
+ return Scheduler.post("File_prototype_setDates",
+ [this._fdmsg, accessDate, modificationDate], this);
+ };
+}
+
+
+/**
+ * Open a file asynchronously.
+ *
+ * @return {promise}
+ * @resolves {OS.File}
+ * @rejects {OS.Error}
+ */
+File.open = function open(path, mode, options) {
+ return Scheduler.post(
+ "open", [Type.path.toMsg(path), mode, options],
+ path
+ ).then(
+ function onSuccess(msg) {
+ return new File(msg);
+ }
+ );
+};
+
+/**
+ * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name.
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
+ * If |false| use HEX numbers ie: filename-A65BC0.ext
+ * - {number} maxReadableNumber Used to limit the amount of tries after a failed
+ * file creation. Default is 20.
+ *
+ * @return {Object} contains A file object{file} and the path{path}.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+File.openUnique = function openUnique(path, options) {
+ return Scheduler.post(
+ "openUnique", [Type.path.toMsg(path), options],
+ path
+ ).then(
+ function onSuccess(msg) {
+ return {
+ path: msg.path,
+ file: new File(msg.file)
+ };
+ }
+ );
+};
+
+/**
+ * Get the information on the file.
+ *
+ * @return {promise}
+ * @resolves {OS.File.Info}
+ * @rejects {OS.Error}
+ */
+File.stat = function stat(path, options) {
+ return Scheduler.post(
+ "stat", [Type.path.toMsg(path), options],
+ path).then(File.Info.fromMsg);
+};
+
+
+/**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * @return {promise}
+ * @rejects {TypeError}
+ * @rejects {OS.File.Error}
+ */
+File.setDates = function setDates(path, accessDate, modificationDate) {
+ return Scheduler.post("setDates",
+ [Type.path.toMsg(path), accessDate, modificationDate],
+ this);
+};
+
+/**
+ * Set the file's access permissions. This does nothing on Windows.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+File.setPermissions = function setPermissions(path, options = {}) {
+ return Scheduler.post("setPermissions",
+ [Type.path.toMsg(path), options]);
+};
+
+/**
+ * Fetch the current directory
+ *
+ * @return {promise}
+ * @resolves {string} The current directory, as a path usable with OS.Path
+ * @rejects {OS.Error}
+ */
+File.getCurrentDirectory = function getCurrentDirectory() {
+ return Scheduler.post(
+ "getCurrentDirectory"
+ ).then(Type.path.fromMsg);
+};
+
+/**
+ * Change the current directory
+ *
+ * @param {string} path The OS-specific path to the current directory.
+ * You should use the methods of OS.Path and the constants of OS.Constants.Path
+ * to build OS-specific paths in a portable manner.
+ *
+ * @return {promise}
+ * @resolves {null}
+ * @rejects {OS.Error}
+ */
+File.setCurrentDirectory = function setCurrentDirectory(path) {
+ return Scheduler.post(
+ "setCurrentDirectory", [Type.path.toMsg(path)], path
+ );
+};
+
+/**
+ * Copy a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be copied.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If true, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @rejects {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be copied with the file. The
+ * behavior may not be the same across all platforms.
+*/
+File.copy = function copy(sourcePath, destPath, options) {
+ return Scheduler.post("copy", [Type.path.toMsg(sourcePath),
+ Type.path.toMsg(destPath), options], [sourcePath, destPath]);
+};
+
+/**
+ * Move a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be moved.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @returns {Promise}
+ * @rejects {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be moved with the file. The
+ * behavior may not be the same across all platforms.
+ */
+File.move = function move(sourcePath, destPath, options) {
+ return Scheduler.post("move", [Type.path.toMsg(sourcePath),
+ Type.path.toMsg(destPath), options], [sourcePath, destPath]);
+};
+
+/**
+ * Create a symbolic link to a source.
+ *
+ * @param {string} sourcePath The platform-specific path to which
+ * the symbolic link should point.
+ * @param {string} destPath The platform-specific path at which the
+ * symbolic link should be created.
+ *
+ * @returns {Promise}
+ * @rejects {OS.File.Error} In case of any error.
+ */
+if (!SharedAll.Constants.Win) {
+ File.unixSymLink = function unixSymLink(sourcePath, destPath) {
+ return Scheduler.post("unixSymLink", [Type.path.toMsg(sourcePath),
+ Type.path.toMsg(destPath)], [sourcePath, destPath]);
+ };
+}
+
+/**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} Platform-specific path to a directory on the disk to
+ * query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+File.getAvailableFreeSpace = function getAvailableFreeSpace(sourcePath) {
+ return Scheduler.post("getAvailableFreeSpace",
+ [Type.path.toMsg(sourcePath)], sourcePath
+ ).then(Type.uint64_t.fromMsg);
+};
+
+/**
+ * Remove an empty directory.
+ *
+ * @param {string} path The name of the directory to remove.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |true|, do not fail if the
+ * directory does not exist yet.
+ */
+File.removeEmptyDir = function removeEmptyDir(path, options) {
+ return Scheduler.post("removeEmptyDir",
+ [Type.path.toMsg(path), options], path);
+};
+
+/**
+ * Remove an existing file.
+ *
+ * @param {string} path The name of the file.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the file does
+ * not exist. |true| by default.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+File.remove = function remove(path, options) {
+ return Scheduler.post("remove",
+ [Type.path.toMsg(path), options], path);
+};
+
+
+
+/**
+ * Create a directory and, optionally, its parent directories.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ *
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |path|
+ * must be a descendant of |from|, and that |from| and its existing
+ * subdirectories present in |path| must be user-writeable.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ * - {bool} ignoreExisting If |false|, throw an error if the directory
+ * already exists. |true| by default. Ignored if |from| is specified.
+ * - {number} unixMode Under Unix, if specified, a file creation mode,
+ * as per libc function |mkdir|. If unspecified, dirs are
+ * created with a default mode of 0700 (dir is private to
+ * the user, the user can read, write and execute). Ignored under Windows
+ * or if the file system does not support file creation modes.
+ * - {C pointer} winSecurity Under Windows, if specified, security
+ * attributes as per winapi function |CreateDirectory|. If
+ * unspecified, use the default security descriptor, inherited from
+ * the parent directory. Ignored under Unix or if the file system
+ * does not support security descriptors.
+ */
+File.makeDir = function makeDir(path, options) {
+ return Scheduler.post("makeDir",
+ [Type.path.toMsg(path), options], path);
+};
+
+/**
+ * Return the contents of a file
+ *
+ * @param {string} path The path to the file.
+ * @param {number=} bytes Optionally, an upper bound to the number of bytes
+ * to read. DEPRECATED - please use options.bytes instead.
+ * @param {JSON} options Additional options.
+ * - {boolean} sequential A flag that triggers a population of the page cache
+ * with data from a file so that subsequent reads from that file would not
+ * block on disk I/O. If |true| or unspecified, inform the system that the
+ * contents of the file will be read in order. Otherwise, make no such
+ * assumption. |true| by default.
+ * - {number} bytes An upper bound to the number of bytes to read.
+ * - {string} compression If "lz4" and if the file is compressed using the lz4
+ * compression algorithm, decompress the file contents on the fly.
+ *
+ * @resolves {Uint8Array} A buffer holding the bytes
+ * read from the file.
+ */
+File.read = function read(path, bytes, options = {}) {
+ if (typeof bytes == "object") {
+ // Passing |bytes| as an argument is deprecated.
+ // We should now be passing it as a field of |options|.
+ options = bytes || {};
+ } else {
+ options = clone(options, ["outExecutionDuration"]);
+ if (typeof bytes != "undefined") {
+ options.bytes = bytes;
+ }
+ }
+
+ if (options.compression || !nativeWheneverAvailable) {
+ // We need to use the JS implementation.
+ let promise = Scheduler.post("read",
+ [Type.path.toMsg(path), bytes, options], path);
+ return promise.then(
+ function onSuccess(data) {
+ if (typeof data == "string") {
+ return data;
+ }
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
+ });
+ }
+
+ // Otherwise, use the native implementation.
+ return Scheduler.push(() => Native.read(path, options));
+};
+
+/**
+ * Find outs if a file exists.
+ *
+ * @param {string} path The path to the file.
+ *
+ * @return {bool} true if the file exists, false otherwise.
+ */
+File.exists = function exists(path) {
+ return Scheduler.post("exists",
+ [Type.path.toMsg(path)], path);
+};
+
+/**
+ * Write a file, atomically.
+ *
+ * By opposition to a regular |write|, this operation ensures that,
+ * until the contents are fully written, the destination file is
+ * not modified.
+ *
+ * Limitation: In a few extreme cases (hardware failure during the
+ * write, user unplugging disk during the write, etc.), data may be
+ * corrupted. If your data is user-critical (e.g. preferences,
+ * application data, etc.), you may wish to consider adding options
+ * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
+ * detailed below. Note that no combination of options can be
+ * guaranteed to totally eliminate the risk of corruption.
+ *
+ * @param {string} path The path of the file to modify.
+ * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
+ * @param {*=} options Optionally, an object determining the behavior
+ * of this function. This object may contain the following fields:
+ * - {number} bytes The number of bytes to write. If unspecified,
+ * |buffer.byteLength|. Required if |buffer| is a C pointer.
+ * - {string} tmpPath If |null| or unspecified, write all data directly
+ * to |path|. If specified, write all data to a temporary file called
+ * |tmpPath| and, once this write is complete, rename the file to
+ * replace |path|. Performing this additional operation is a little
+ * slower but also a little safer.
+ * - {bool} noOverwrite - If set, this function will fail if a file already
+ * exists at |path|.
+ * - {bool} flush - If |false| or unspecified, return immediately once the
+ * write is complete. If |true|, before writing, force the operating system
+ * to write its internal disk buffers to the disk. This is considerably slower
+ * (not just for the application but for the whole system) but also safer:
+ * if the system shuts down improperly (typically due to a kernel freeze
+ * or a power failure) or if the device is disconnected before the buffer
+ * is flushed, the file has more chances of not being corrupted.
+ * - {string} backupTo - If specified, backup the destination file as |backupTo|.
+ * Note that this function renames the destination file before overwriting it.
+ * If the process or the operating system freezes or crashes
+ * during the short window between these operations,
+ * the destination file will have been moved to its backup.
+ *
+ * @return {promise}
+ * @resolves {number} The number of bytes actually written.
+ */
+File.writeAtomic = function writeAtomic(path, buffer, options = {}) {
+ // Copy |options| to avoid modifying the original object but preserve the
+ // reference to |outExecutionDuration| option if it is passed.
+ options = clone(options, ["outExecutionDuration"]);
+ // As options.tmpPath is a path, we need to encode it as |Type.path| message
+ if ("tmpPath" in options) {
+ options.tmpPath = Type.path.toMsg(options.tmpPath);
+ };
+ if (isTypedArray(buffer) && (!("bytes" in options))) {
+ options.bytes = buffer.byteLength;
+ };
+ let refObj = {};
+ TelemetryStopwatch.start("OSFILE_WRITEATOMIC_JANK_MS", refObj);
+ let promise = Scheduler.post("writeAtomic",
+ [Type.path.toMsg(path),
+ Type.void_t.in_ptr.toMsg(buffer),
+ options], [options, buffer, path]);
+ TelemetryStopwatch.finish("OSFILE_WRITEATOMIC_JANK_MS", refObj);
+ return promise;
+};
+
+File.removeDir = function(path, options = {}) {
+ return Scheduler.post("removeDir",
+ [Type.path.toMsg(path), options], path);
+};
+
+/**
+ * Information on a file, as returned by OS.File.stat or
+ * OS.File.prototype.stat
+ *
+ * @constructor
+ */
+File.Info = function Info(value) {
+ // Note that we can't just do this[k] = value[k] because our
+ // prototype defines getters for all of these fields.
+ for (let k in value) {
+ if (k != "creationDate") {
+ Object.defineProperty(this, k, {value: value[k]});
+ }
+ }
+ Object.defineProperty(this, "_deprecatedCreationDate", {value: value["creationDate"]});
+};
+File.Info.prototype = SysAll.AbstractInfo.prototype;
+
+// Deprecated
+Object.defineProperty(File.Info.prototype, "creationDate", {
+ get: function creationDate() {
+ let {Deprecated} = Cu.import("resource://gre/modules/Deprecated.jsm", {});
+ Deprecated.warning("Field 'creationDate' is deprecated.", "https://developer.mozilla.org/en-US/docs/JavaScript_OS.File/OS.File.Info#Cross-platform_Attributes");
+ return this._deprecatedCreationDate;
+ }
+});
+
+File.Info.fromMsg = function fromMsg(value) {
+ return new File.Info(value);
+};
+
+/**
+ * Get worker's current DEBUG flag.
+ * Note: This is used for testing purposes.
+ */
+File.GET_DEBUG = function GET_DEBUG() {
+ return Scheduler.post("GET_DEBUG");
+};
+
+/**
+ * Iterate asynchronously through a directory
+ *
+ * @constructor
+ */
+var DirectoryIterator = function DirectoryIterator(path, options) {
+ /**
+ * Open the iterator on the worker thread
+ *
+ * @type {Promise}
+ * @resolves {*} A message accepted by the methods of DirectoryIterator
+ * in the worker thread
+ * @rejects {StopIteration} If all entries have already been visited
+ * or the iterator has been closed.
+ */
+ this.__itmsg = Scheduler.post(
+ "new_DirectoryIterator", [Type.path.toMsg(path), options],
+ path
+ );
+ this._isClosed = false;
+};
+DirectoryIterator.prototype = {
+ iterator: function () {
+ return this;
+ },
+ __iterator__: function () {
+ return this;
+ },
+
+ // Once close() is called, _itmsg should reject with a
+ // StopIteration. However, we don't want to create the promise until
+ // it's needed because it might never be used. In that case, we
+ // would get a warning on the console.
+ get _itmsg() {
+ if (!this.__itmsg) {
+ this.__itmsg = Promise.reject(StopIteration);
+ }
+ return this.__itmsg;
+ },
+
+ /**
+ * Determine whether the directory exists.
+ *
+ * @resolves {boolean}
+ */
+ exists: function exists() {
+ return this._itmsg.then(
+ function onSuccess(iterator) {
+ return Scheduler.post("DirectoryIterator_prototype_exists", [iterator]);
+ }
+ );
+ },
+ /**
+ * Get the next entry in the directory.
+ *
+ * @return {Promise}
+ * @resolves {OS.File.Entry}
+ * @rejects {StopIteration} If all entries have already been visited.
+ */
+ next: function next() {
+ let self = this;
+ let promise = this._itmsg;
+
+ // Get the iterator, call _next
+ promise = promise.then(
+ function withIterator(iterator) {
+ return self._next(iterator);
+ });
+
+ return promise;
+ },
+ /**
+ * Get several entries at once.
+ *
+ * @param {number=} length If specified, the number of entries
+ * to return. If unspecified, return all remaining entries.
+ * @return {Promise}
+ * @resolves {Array} An array containing the |length| next entries.
+ */
+ nextBatch: function nextBatch(size) {
+ if (this._isClosed) {
+ return Promise.resolve([]);
+ }
+ let promise = this._itmsg;
+ promise = promise.then(
+ function withIterator(iterator) {
+ return Scheduler.post("DirectoryIterator_prototype_nextBatch", [iterator, size]);
+ });
+ promise = promise.then(
+ function withEntries(array) {
+ return array.map(DirectoryIterator.Entry.fromMsg);
+ });
+ return promise;
+ },
+ /**
+ * Apply a function to all elements of the directory sequentially.
+ *
+ * @param {Function} cb This function will be applied to all entries
+ * of the directory. It receives as arguments
+ * - the OS.File.Entry corresponding to the entry;
+ * - the index of the entry in the enumeration;
+ * - the iterator itself - return |iterator.close()| to stop the loop.
+ *
+ * If the callback returns a promise, iteration waits until the
+ * promise is resolved before proceeding.
+ *
+ * @return {Promise} A promise resolved once the loop has reached
+ * its end.
+ */
+ forEach: function forEach(cb, options) {
+ if (this._isClosed) {
+ return Promise.resolve();
+ }
+
+ let self = this;
+ let position = 0;
+ let iterator;
+
+ // Grab iterator
+ let promise = this._itmsg.then(
+ function(aIterator) {
+ iterator = aIterator;
+ }
+ );
+
+ // Then iterate
+ let loop = function loop() {
+ if (self._isClosed) {
+ return Promise.resolve();
+ }
+ return self._next(iterator).then(
+ function onSuccess(value) {
+ return Promise.resolve(cb(value, position++, self)).then(loop);
+ },
+ function onFailure(reason) {
+ if (reason == StopIteration) {
+ return;
+ }
+ throw reason;
+ }
+ );
+ };
+
+ return promise.then(loop);
+ },
+ /**
+ * Auxiliary method: fetch the next item
+ *
+ * @rejects {StopIteration} If all entries have already been visited
+ * or the iterator has been closed.
+ */
+ _next: function _next(iterator) {
+ if (this._isClosed) {
+ return this._itmsg;
+ }
+ let self = this;
+ let promise = Scheduler.post("DirectoryIterator_prototype_next", [iterator]);
+ promise = promise.then(
+ DirectoryIterator.Entry.fromMsg,
+ function onReject(reason) {
+ if (reason == StopIteration) {
+ self.close();
+ throw StopIteration;
+ }
+ throw reason;
+ });
+ return promise;
+ },
+ /**
+ * Close the iterator
+ */
+ close: function close() {
+ if (this._isClosed) {
+ return Promise.resolve();
+ }
+ this._isClosed = true;
+ let self = this;
+ return this._itmsg.then(
+ function withIterator(iterator) {
+ // Set __itmsg to null so that the _itmsg getter returns a
+ // rejected StopIteration promise if it's ever used.
+ self.__itmsg = null;
+ return Scheduler.post("DirectoryIterator_prototype_close", [iterator]);
+ }
+ );
+ }
+};
+
+DirectoryIterator.Entry = function Entry(value) {
+ return value;
+};
+DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
+
+DirectoryIterator.Entry.fromMsg = function fromMsg(value) {
+ return new DirectoryIterator.Entry(value);
+};
+
+File.resetWorker = function() {
+ return Task.spawn(function*() {
+ let resources = yield Scheduler.kill({shutdown: false, reset: true});
+ if (resources && !resources.killed) {
+ throw new Error("Could not reset worker, this would leak file descriptors: " + JSON.stringify(resources));
+ }
+ });
+};
+
+// Constants
+File.POS_START = SysAll.POS_START;
+File.POS_CURRENT = SysAll.POS_CURRENT;
+File.POS_END = SysAll.POS_END;
+
+// Exports
+File.Error = OSError;
+File.DirectoryIterator = DirectoryIterator;
+
+this.OS = {};
+this.OS.File = File;
+this.OS.Constants = SharedAll.Constants;
+this.OS.Shared = {
+ LOG: SharedAll.LOG,
+ Type: SysAll.Type,
+ get DEBUG() {
+ return SharedAll.Config.DEBUG;
+ },
+ set DEBUG(x) {
+ return SharedAll.Config.DEBUG = x;
+ }
+};
+Object.freeze(this.OS.Shared);
+this.OS.Path = Path;
+
+// Returns a resolved promise when all the queued operation have been completed.
+Object.defineProperty(OS.File, "queue", {
+ get: function() {
+ return Scheduler.queue;
+ }
+});
+
+// `true` if this is a content process, `false` otherwise.
+// It would be nicer to go through `Services.appInfo`, but some tests need to be
+// able to replace that field with a custom implementation before it is first
+// called.
+const isContent = Components.classes["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
+/**
+ * Shutdown barriers, to let clients register to be informed during shutdown.
+ */
+var Barriers = {
+ shutdown: new AsyncShutdown.Barrier("OS.File: Waiting for clients before full shutdown"),
+ /**
+ * Return the shutdown state of OS.File
+ */
+ getDetails: function() {
+ let result = {
+ launched: Scheduler.launched,
+ shutdown: Scheduler.shutdown,
+ worker: !!Scheduler._worker,
+ pendingReset: !!Scheduler.resetTimer,
+ latestSent: Scheduler.Debugging.latestSent,
+ latestReceived: Scheduler.Debugging.latestReceived,
+ messagesSent: Scheduler.Debugging.messagesSent,
+ messagesReceived: Scheduler.Debugging.messagesReceived,
+ messagesQueued: Scheduler.Debugging.messagesQueued,
+ DEBUG: SharedAll.Config.DEBUG,
+ };
+ // Convert dates to strings for better readability
+ for (let key of ["latestSent", "latestReceived"]) {
+ if (result[key] && typeof result[key][0] == "number") {
+ result[key][0] = Date(result[key][0]);
+ }
+ }
+ return result;
+ }
+};
+
+function setupShutdown(phaseName) {
+ Barriers[phaseName] = new AsyncShutdown.Barrier(`OS.File: Waiting for clients before ${phaseName}`),
+ File[phaseName] = Barriers[phaseName].client;
+
+ // Auto-flush OS.File during `phaseName`. This ensures that any I/O
+ // that has been queued *before* `phaseName` is properly completed.
+ // To ensure that I/O queued *during* `phaseName` change is completed,
+ // clients should register using AsyncShutdown.addBlocker.
+ AsyncShutdown[phaseName].addBlocker(
+ `OS.File: flush I/O queued before ${phaseName}`,
+ Task.async(function*() {
+ // Give clients a last chance to enqueue requests.
+ yield Barriers[phaseName].wait({crashAfterMS: null});
+
+ // Wait until all currently enqueued requests are completed.
+ yield Scheduler.queue;
+ }),
+ () => {
+ let details = Barriers.getDetails();
+ details.clients = Barriers[phaseName].state;
+ return details;
+ }
+ );
+}
+
+// profile-before-change only exists in the parent, and OS.File should
+// not be used in the child process anyways.
+if (!isContent) {
+ setupShutdown("profileBeforeChange")
+}
+File.shutdown = Barriers.shutdown.client;
diff --git a/toolkit/components/osfile/modules/osfile_async_worker.js b/toolkit/components/osfile/modules/osfile_async_worker.js
new file mode 100644
index 000000000..84287c75e
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_async_worker.js
@@ -0,0 +1,407 @@
+/* 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/. */
+
+
+if (this.Components) {
+ throw new Error("This worker can only be loaded from a worker thread");
+}
+
+// Worker thread for osfile asynchronous front-end
+
+(function(exports) {
+ "use strict";
+
+ // Timestamps, for use in Telemetry.
+ // The object is set to |null| once it has been sent
+ // to the main thread.
+ let timeStamps = {
+ entered: Date.now(),
+ loaded: null
+ };
+
+ importScripts("resource://gre/modules/osfile.jsm");
+
+ let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let LOG = SharedAll.LOG.bind(SharedAll, "Agent");
+
+ let worker = new PromiseWorker.AbstractWorker();
+ worker.dispatch = function(method, args = []) {
+ return Agent[method](...args);
+ },
+ worker.log = LOG;
+ worker.postMessage = function(message, ...transfers) {
+ if (timeStamps) {
+ message.timeStamps = timeStamps;
+ timeStamps = null;
+ }
+ self.postMessage(message, ...transfers);
+ };
+ worker.close = function() {
+ self.close();
+ };
+ let Meta = PromiseWorker.Meta;
+
+ self.addEventListener("message", msg => worker.handleMessage(msg));
+
+ /**
+ * A data structure used to track opened resources
+ */
+ let ResourceTracker = function ResourceTracker() {
+ // A number used to generate ids
+ this._idgen = 0;
+ // A map from id to resource
+ this._map = new Map();
+ };
+ ResourceTracker.prototype = {
+ /**
+ * Get a resource from its unique identifier.
+ */
+ get: function(id) {
+ let result = this._map.get(id);
+ if (result == null) {
+ return result;
+ }
+ return result.resource;
+ },
+ /**
+ * Remove a resource from its unique identifier.
+ */
+ remove: function(id) {
+ if (!this._map.has(id)) {
+ throw new Error("Cannot find resource id " + id);
+ }
+ this._map.delete(id);
+ },
+ /**
+ * Add a resource, return a new unique identifier
+ *
+ * @param {*} resource A resource.
+ * @param {*=} info Optional information. For debugging purposes.
+ *
+ * @return {*} A unique identifier. For the moment, this is a number,
+ * but this might not remain the case forever.
+ */
+ add: function(resource, info) {
+ let id = this._idgen++;
+ this._map.set(id, {resource:resource, info:info});
+ return id;
+ },
+ /**
+ * Return a list of all open resources i.e. the ones still present in
+ * ResourceTracker's _map.
+ */
+ listOpenedResources: function listOpenedResources() {
+ return Array.from(this._map, ([id, resource]) => resource.info.path);
+ }
+ };
+
+ /**
+ * A map of unique identifiers to opened files.
+ */
+ let OpenedFiles = new ResourceTracker();
+
+ /**
+ * Execute a function in the context of a given file.
+ *
+ * @param {*} id A unique identifier, as used by |OpenFiles|.
+ * @param {Function} f A function to call.
+ * @param {boolean} ignoreAbsent If |true|, the error is ignored. Otherwise, the error causes an exception.
+ * @return The return value of |f()|
+ *
+ * This function attempts to get the file matching |id|. If
+ * the file exists, it executes |f| within the |this| set
+ * to the corresponding file. Otherwise, it throws an error.
+ */
+ let withFile = function withFile(id, f, ignoreAbsent) {
+ let file = OpenedFiles.get(id);
+ if (file == null) {
+ if (!ignoreAbsent) {
+ throw OS.File.Error.closed("accessing file");
+ }
+ return undefined;
+ }
+ return f.call(file);
+ };
+
+ let OpenedDirectoryIterators = new ResourceTracker();
+ let withDir = function withDir(fd, f, ignoreAbsent) {
+ let file = OpenedDirectoryIterators.get(fd);
+ if (file == null) {
+ if (!ignoreAbsent) {
+ throw OS.File.Error.closed("accessing directory");
+ }
+ return undefined;
+ }
+ if (!(file instanceof File.DirectoryIterator)) {
+ throw new Error("file is not a directory iterator " + file.__proto__.toSource());
+ }
+ return f.call(file);
+ };
+
+ let Type = exports.OS.Shared.Type;
+
+ let File = exports.OS.File;
+
+ /**
+ * The agent.
+ *
+ * It is in charge of performing method-specific deserialization
+ * of messages, calling the function/method of OS.File and serializing
+ * back the results.
+ */
+ let Agent = {
+ // Update worker's OS.Shared.DEBUG flag message from controller.
+ SET_DEBUG: function(aDEBUG) {
+ SharedAll.Config.DEBUG = aDEBUG;
+ },
+ // Return worker's current OS.Shared.DEBUG value to controller.
+ // Note: This is used for testing purposes.
+ GET_DEBUG: function() {
+ return SharedAll.Config.DEBUG;
+ },
+ /**
+ * Execute shutdown sequence, returning data on leaked file descriptors.
+ *
+ * @param {bool} If |true|, kill the worker if this would not cause
+ * leaks.
+ */
+ Meta_shutdown: function(kill) {
+ let result = {
+ openedFiles: OpenedFiles.listOpenedResources(),
+ openedDirectoryIterators: OpenedDirectoryIterators.listOpenedResources(),
+ killed: false // Placeholder
+ };
+
+ // Is it safe to kill the worker?
+ let safe = result.openedFiles.length == 0
+ && result.openedDirectoryIterators.length == 0;
+ result.killed = safe && kill;
+
+ return new Meta(result, {shutdown: result.killed});
+ },
+ // Functions of OS.File
+ stat: function stat(path, options) {
+ return exports.OS.File.Info.toMsg(
+ exports.OS.File.stat(Type.path.fromMsg(path), options));
+ },
+ setPermissions: function setPermissions(path, options = {}) {
+ return exports.OS.File.setPermissions(Type.path.fromMsg(path), options);
+ },
+ setDates: function setDates(path, accessDate, modificationDate) {
+ return exports.OS.File.setDates(Type.path.fromMsg(path), accessDate,
+ modificationDate);
+ },
+ getCurrentDirectory: function getCurrentDirectory() {
+ return exports.OS.Shared.Type.path.toMsg(File.getCurrentDirectory());
+ },
+ setCurrentDirectory: function setCurrentDirectory(path) {
+ File.setCurrentDirectory(exports.OS.Shared.Type.path.fromMsg(path));
+ },
+ copy: function copy(sourcePath, destPath, options) {
+ return File.copy(Type.path.fromMsg(sourcePath),
+ Type.path.fromMsg(destPath), options);
+ },
+ move: function move(sourcePath, destPath, options) {
+ return File.move(Type.path.fromMsg(sourcePath),
+ Type.path.fromMsg(destPath), options);
+ },
+ getAvailableFreeSpace: function getAvailableFreeSpace(sourcePath) {
+ return Type.uint64_t.toMsg(
+ File.getAvailableFreeSpace(Type.path.fromMsg(sourcePath)));
+ },
+ makeDir: function makeDir(path, options) {
+ return File.makeDir(Type.path.fromMsg(path), options);
+ },
+ removeEmptyDir: function removeEmptyDir(path, options) {
+ return File.removeEmptyDir(Type.path.fromMsg(path), options);
+ },
+ remove: function remove(path, options) {
+ return File.remove(Type.path.fromMsg(path), options);
+ },
+ open: function open(path, mode, options) {
+ let filePath = Type.path.fromMsg(path);
+ let file = File.open(filePath, mode, options);
+ return OpenedFiles.add(file, {
+ // Adding path information to keep track of opened files
+ // to report leaks when debugging.
+ path: filePath
+ });
+ },
+ openUnique: function openUnique(path, options) {
+ let filePath = Type.path.fromMsg(path);
+ let openedFile = OS.Shared.AbstractFile.openUnique(filePath, options);
+ let resourceId = OpenedFiles.add(openedFile.file, {
+ // Adding path information to keep track of opened files
+ // to report leaks when debugging.
+ path: openedFile.path
+ });
+
+ return {
+ path: openedFile.path,
+ file: resourceId
+ };
+ },
+ read: function read(path, bytes, options) {
+ let data = File.read(Type.path.fromMsg(path), bytes, options);
+ if (typeof data == "string") {
+ return data;
+ }
+ return new Meta({
+ buffer: data.buffer,
+ byteOffset: data.byteOffset,
+ byteLength: data.byteLength
+ }, {
+ transfers: [data.buffer]
+ });
+ },
+ exists: function exists(path) {
+ return File.exists(Type.path.fromMsg(path));
+ },
+ writeAtomic: function writeAtomic(path, buffer, options) {
+ if (options.tmpPath) {
+ options.tmpPath = Type.path.fromMsg(options.tmpPath);
+ }
+ return File.writeAtomic(Type.path.fromMsg(path),
+ Type.voidptr_t.fromMsg(buffer),
+ options
+ );
+ },
+ removeDir: function(path, options) {
+ return File.removeDir(Type.path.fromMsg(path), options);
+ },
+ new_DirectoryIterator: function new_DirectoryIterator(path, options) {
+ let directoryPath = Type.path.fromMsg(path);
+ let iterator = new File.DirectoryIterator(directoryPath, options);
+ return OpenedDirectoryIterators.add(iterator, {
+ // Adding path information to keep track of opened directory
+ // iterators to report leaks when debugging.
+ path: directoryPath
+ });
+ },
+ // Methods of OS.File
+ File_prototype_close: function close(fd) {
+ return withFile(fd,
+ function do_close() {
+ try {
+ return this.close();
+ } finally {
+ OpenedFiles.remove(fd);
+ }
+ });
+ },
+ File_prototype_stat: function stat(fd) {
+ return withFile(fd,
+ function do_stat() {
+ return exports.OS.File.Info.toMsg(this.stat());
+ });
+ },
+ File_prototype_setPermissions: function setPermissions(fd, options = {}) {
+ return withFile(fd,
+ function do_setPermissions() {
+ return this.setPermissions(options);
+ });
+ },
+ File_prototype_setDates: function setDates(fd, accessTime, modificationTime) {
+ return withFile(fd,
+ function do_setDates() {
+ return this.setDates(accessTime, modificationTime);
+ });
+ },
+ File_prototype_read: function read(fd, nbytes, options) {
+ return withFile(fd,
+ function do_read() {
+ let data = this.read(nbytes, options);
+ return new Meta({
+ buffer: data.buffer,
+ byteOffset: data.byteOffset,
+ byteLength: data.byteLength
+ }, {
+ transfers: [data.buffer]
+ });
+ }
+ );
+ },
+ File_prototype_readTo: function readTo(fd, buffer, options) {
+ return withFile(fd,
+ function do_readTo() {
+ return this.readTo(exports.OS.Shared.Type.voidptr_t.fromMsg(buffer),
+ options);
+ });
+ },
+ File_prototype_write: function write(fd, buffer, options) {
+ return withFile(fd,
+ function do_write() {
+ return this.write(exports.OS.Shared.Type.voidptr_t.fromMsg(buffer),
+ options);
+ });
+ },
+ File_prototype_setPosition: function setPosition(fd, pos, whence) {
+ return withFile(fd,
+ function do_setPosition() {
+ return this.setPosition(pos, whence);
+ });
+ },
+ File_prototype_getPosition: function getPosition(fd) {
+ return withFile(fd,
+ function do_getPosition() {
+ return this.getPosition();
+ });
+ },
+ File_prototype_flush: function flush(fd) {
+ return withFile(fd,
+ function do_flush() {
+ return this.flush();
+ });
+ },
+ // Methods of OS.File.DirectoryIterator
+ DirectoryIterator_prototype_next: function next(dir) {
+ return withDir(dir,
+ function do_next() {
+ try {
+ return File.DirectoryIterator.Entry.toMsg(this.next());
+ } catch (x) {
+ if (x == StopIteration) {
+ OpenedDirectoryIterators.remove(dir);
+ }
+ throw x;
+ }
+ }, false);
+ },
+ DirectoryIterator_prototype_nextBatch: function nextBatch(dir, size) {
+ return withDir(dir,
+ function do_nextBatch() {
+ let result;
+ try {
+ result = this.nextBatch(size);
+ } catch (x) {
+ OpenedDirectoryIterators.remove(dir);
+ throw x;
+ }
+ return result.map(File.DirectoryIterator.Entry.toMsg);
+ }, false);
+ },
+ DirectoryIterator_prototype_close: function close(dir) {
+ return withDir(dir,
+ function do_close() {
+ this.close();
+ OpenedDirectoryIterators.remove(dir);
+ }, true);// ignore error to support double-closing |DirectoryIterator|
+ },
+ DirectoryIterator_prototype_exists: function exists(dir) {
+ return withDir(dir,
+ function do_exists() {
+ return this.exists();
+ });
+ }
+ };
+ if (!SharedAll.Constants.Win) {
+ Agent.unixSymLink = function unixSymLink(sourcePath, destPath) {
+ return File.unixSymLink(Type.path.fromMsg(sourcePath),
+ Type.path.fromMsg(destPath));
+ };
+ }
+
+ timeStamps.loaded = Date.now();
+})(this);
diff --git a/toolkit/components/osfile/modules/osfile_native.jsm b/toolkit/components/osfile/modules/osfile_native.jsm
new file mode 100644
index 000000000..16cd3c92a
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_native.jsm
@@ -0,0 +1,70 @@
+/* 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 (xpcom) implementation of key OS.File functions
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["read"];
+
+var {results: Cr, utils: Cu, interfaces: Ci} = Components;
+
+var SharedAll = Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", {});
+
+var SysAll = {};
+if (SharedAll.Constants.Win) {
+ Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", SysAll);
+} else if (SharedAll.Constants.libc) {
+ Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", SysAll);
+} else {
+ throw new Error("I am neither under Windows nor under a Posix system");
+}
+var {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+/**
+ * The native service holding the implementation of the functions.
+ */
+XPCOMUtils.defineLazyServiceGetter(this,
+ "Internals",
+ "@mozilla.org/toolkit/osfile/native-internals;1",
+ "nsINativeOSFileInternalsService");
+
+/**
+ * Native implementation of OS.File.read
+ *
+ * This implementation does not handle option |compression|.
+ */
+this.read = function(path, options = {}) {
+ // Sanity check on types of options
+ if ("encoding" in options && typeof options.encoding != "string") {
+ return Promise.reject(new TypeError("Invalid type for option encoding"));
+ }
+ if ("compression" in options && typeof options.compression != "string") {
+ return Promise.reject(new TypeError("Invalid type for option compression"));
+ }
+ if ("bytes" in options && typeof options.bytes != "number") {
+ return Promise.reject(new TypeError("Invalid type for option bytes"));
+ }
+
+ let deferred = Promise.defer();
+ Internals.read(path,
+ options,
+ function onSuccess(success) {
+ success.QueryInterface(Ci.nsINativeOSFileResult);
+ if ("outExecutionDuration" in options) {
+ options.outExecutionDuration =
+ success.executionDurationMS +
+ (options.outExecutionDuration || 0);
+ }
+ deferred.resolve(success.result);
+ },
+ function onError(operation, oserror) {
+ deferred.reject(new SysAll.Error(operation, oserror, path));
+ }
+ );
+ return deferred.promise;
+};
diff --git a/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm
new file mode 100644
index 000000000..c5c505102
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_shared_allthreads.jsm
@@ -0,0 +1,1315 @@
+/* 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/. */
+
+"use strict";
+
+/**
+ * OS.File utilities used by all threads.
+ *
+ * This module defines:
+ * - logging;
+ * - the base constants;
+ * - base types and primitives for declaring new types;
+ * - primitives for importing C functions;
+ * - primitives for dealing with integers, pointers, typed arrays;
+ * - the base class OSError;
+ * - a few additional utilities.
+ */
+
+// Boilerplate used to be able to import this module both from the main
+// thread and from worker threads.
+
+// Since const is lexically scoped, hoist the
+// conditionally-useful definition ourselves.
+const Cu = typeof Components != "undefined" ? Components.utils : undefined;
+const Ci = typeof Components != "undefined" ? Components.interfaces : undefined;
+const Cc = typeof Components != "undefined" ? Components.classes : undefined;
+
+/**
+ * A constructor for messages that require transfers instead of copies.
+ *
+ * See BasePromiseWorker.Meta.
+ *
+ * @constructor
+ */
+var Meta;
+if (typeof Components != "undefined") {
+ // Global definition of |exports|, to keep everybody happy.
+ // In non-main thread, |exports| is provided by the module
+ // loader.
+ this.exports = {};
+
+ Cu.import("resource://gre/modules/Services.jsm", this);
+ Meta = Cu.import("resource://gre/modules/PromiseWorker.jsm", {}).BasePromiseWorker.Meta;
+} else {
+ importScripts("resource://gre/modules/workers/require.js");
+ Meta = require("resource://gre/modules/workers/PromiseWorker.js").Meta;
+}
+
+var EXPORTED_SYMBOLS = [
+ "LOG",
+ "clone",
+ "Config",
+ "Constants",
+ "Type",
+ "HollowStructure",
+ "OSError",
+ "Library",
+ "declareFFI",
+ "declareLazy",
+ "declareLazyFFI",
+ "normalizeBufferArgs",
+ "projectValue",
+ "isArrayBuffer",
+ "isTypedArray",
+ "defineLazyGetter",
+ "OS" // Warning: this exported symbol will disappear
+];
+
+////////////////////// Configuration of OS.File
+
+var Config = {
+ /**
+ * If |true|, calls to |LOG| are shown. Otherwise, they are hidden.
+ *
+ * This configuration option is controlled by preference "toolkit.osfile.log".
+ */
+ DEBUG: false,
+
+ /**
+ * TEST
+ */
+ TEST: false
+};
+exports.Config = Config;
+
+////////////////////// OS Constants
+
+if (typeof Components != "undefined") {
+ // On the main thread, OS.Constants is defined by a xpcom
+ // component. On other threads, it is available automatically
+ Cu.import("resource://gre/modules/ctypes.jsm");
+ Cc["@mozilla.org/net/osfileconstantsservice;1"].
+ getService(Ci.nsIOSFileConstantsService).init();
+}
+
+exports.Constants = OS.Constants;
+
+///////////////////// Utilities
+
+// Define a lazy getter for a property
+var defineLazyGetter = function defineLazyGetter(object, name, getter) {
+ Object.defineProperty(object, name, {
+ configurable: true,
+ get: function lazy() {
+ delete this[name];
+ let value = getter.call(this);
+ Object.defineProperty(object, name, {
+ value: value
+ });
+ return value;
+ }
+ });
+};
+exports.defineLazyGetter = defineLazyGetter;
+
+
+///////////////////// Logging
+
+/**
+ * The default implementation of the logger.
+ *
+ * The choice of logger can be overridden with Config.TEST.
+ */
+var gLogger;
+if (typeof window != "undefined" && window.console && console.log) {
+ gLogger = console.log.bind(console, "OS");
+} else {
+ gLogger = function(...args) {
+ dump("OS " + args.join(" ") + "\n");
+ };
+}
+
+/**
+ * Attempt to stringify an argument into something useful for
+ * debugging purposes, by using |.toString()| or |JSON.stringify|
+ * if available.
+ *
+ * @param {*} arg An argument to be stringified if possible.
+ * @return {string} A stringified version of |arg|.
+ */
+var stringifyArg = function stringifyArg(arg) {
+ if (typeof arg === "string") {
+ return arg;
+ }
+ if (arg && typeof arg === "object") {
+ let argToString = "" + arg;
+
+ /**
+ * The only way to detect whether this object has a non-default
+ * implementation of |toString| is to check whether it returns
+ * '[object Object]'. Unfortunately, we cannot simply compare |arg.toString|
+ * and |Object.prototype.toString| as |arg| typically comes from another
+ * compartment.
+ */
+ if (argToString === "[object Object]") {
+ return JSON.stringify(arg, function(key, value) {
+ if (isTypedArray(value)) {
+ return "["+ value.constructor.name + " " + value.byteOffset + " " + value.byteLength + "]";
+ }
+ if (isArrayBuffer(arg)) {
+ return "[" + value.constructor.name + " " + value.byteLength + "]";
+ }
+ return value;
+ });
+ } else {
+ return argToString;
+ }
+ }
+ return arg;
+};
+
+var LOG = function (...args) {
+ if (!Config.DEBUG) {
+ // If logging is deactivated, don't log
+ return;
+ }
+
+ let logFunc = gLogger;
+ if (Config.TEST && typeof Components != "undefined") {
+ // If _TESTING_LOGGING is set, and if we are on the main thread,
+ // redirect logs to Services.console, for testing purposes
+ logFunc = function logFunc(...args) {
+ let message = ["TEST", "OS"].concat(args).join(" ");
+ Services.console.logStringMessage(message + "\n");
+ };
+ }
+ logFunc.apply(null, args.map(stringifyArg));
+};
+
+exports.LOG = LOG;
+
+/**
+ * Return a shallow clone of the enumerable properties of an object.
+ *
+ * Utility used whenever normalizing options requires making (shallow)
+ * changes to an option object. The copy ensures that we do not modify
+ * a client-provided object by accident.
+ *
+ * Note: to reference and not copy specific fields, provide an optional
+ * |refs| argument containing their names.
+ *
+ * @param {JSON} object Options to be cloned.
+ * @param {Array} refs An optional array of field names to be passed by
+ * reference instead of copying.
+ */
+var clone = function (object, refs = []) {
+ let result = {};
+ // Make a reference between result[key] and object[key].
+ let refer = function refer(result, key, object) {
+ Object.defineProperty(result, key, {
+ enumerable: true,
+ get: function() {
+ return object[key];
+ },
+ set: function(value) {
+ object[key] = value;
+ }
+ });
+ };
+ for (let k in object) {
+ if (refs.indexOf(k) < 0) {
+ result[k] = object[k];
+ } else {
+ refer(result, k, object);
+ }
+ }
+ return result;
+};
+
+exports.clone = clone;
+
+///////////////////// Abstractions above js-ctypes
+
+/**
+ * Abstraction above js-ctypes types.
+ *
+ * Use values of this type to register FFI functions. In addition to the
+ * usual features of js-ctypes, values of this type perform the necessary
+ * transformations to ensure that C errors are handled nicely, to connect
+ * resources with their finalizer, etc.
+ *
+ * @param {string} name The name of the type. Must be unique.
+ * @param {CType} implementation The js-ctypes implementation of the type.
+ *
+ * @constructor
+ */
+function Type(name, implementation) {
+ if (!(typeof name == "string")) {
+ throw new TypeError("Type expects as first argument a name, got: "
+ + name);
+ }
+ if (!(implementation instanceof ctypes.CType)) {
+ throw new TypeError("Type expects as second argument a ctypes.CType"+
+ ", got: " + implementation);
+ }
+ Object.defineProperty(this, "name", { value: name });
+ Object.defineProperty(this, "implementation", { value: implementation });
+}
+Type.prototype = {
+ /**
+ * Serialize a value of |this| |Type| into a format that can
+ * be transmitted as a message (not necessarily a string).
+ *
+ * In the default implementation, the method returns the
+ * value unchanged.
+ */
+ toMsg: function default_toMsg(value) {
+ return value;
+ },
+ /**
+ * Deserialize a message to a value of |this| |Type|.
+ *
+ * In the default implementation, the method returns the
+ * message unchanged.
+ */
+ fromMsg: function default_fromMsg(msg) {
+ return msg;
+ },
+ /**
+ * Import a value from C.
+ *
+ * In this default implementation, return the value
+ * unchanged.
+ */
+ importFromC: function default_importFromC(value) {
+ return value;
+ },
+
+ /**
+ * A pointer/array used to pass data to the foreign function.
+ */
+ get in_ptr() {
+ delete this.in_ptr;
+ let ptr_t = new PtrType(
+ "[in] " + this.name + "*",
+ this.implementation.ptr,
+ this);
+ Object.defineProperty(this, "in_ptr",
+ {
+ get: function() {
+ return ptr_t;
+ }
+ });
+ return ptr_t;
+ },
+
+ /**
+ * A pointer/array used to receive data from the foreign function.
+ */
+ get out_ptr() {
+ delete this.out_ptr;
+ let ptr_t = new PtrType(
+ "[out] " + this.name + "*",
+ this.implementation.ptr,
+ this);
+ Object.defineProperty(this, "out_ptr",
+ {
+ get: function() {
+ return ptr_t;
+ }
+ });
+ return ptr_t;
+ },
+
+ /**
+ * A pointer/array used to both pass data to the foreign function
+ * and receive data from the foreign function.
+ *
+ * Whenever possible, prefer using |in_ptr| or |out_ptr|, which
+ * are generally faster.
+ */
+ get inout_ptr() {
+ delete this.inout_ptr;
+ let ptr_t = new PtrType(
+ "[inout] " + this.name + "*",
+ this.implementation.ptr,
+ this);
+ Object.defineProperty(this, "inout_ptr",
+ {
+ get: function() {
+ return ptr_t;
+ }
+ });
+ return ptr_t;
+ },
+
+ /**
+ * Attach a finalizer to a type.
+ */
+ releaseWith: function releaseWith(finalizer) {
+ let parent = this;
+ let type = this.withName("[auto " + this.name + ", " + finalizer + "] ");
+ type.importFromC = function importFromC(value, operation) {
+ return ctypes.CDataFinalizer(
+ parent.importFromC(value, operation),
+ finalizer);
+ };
+ return type;
+ },
+
+ /**
+ * Lazy variant of releaseWith.
+ * Attach a finalizer lazily to a type.
+ *
+ * @param {function} getFinalizer The function that
+ * returns finalizer lazily.
+ */
+ releaseWithLazy: function releaseWithLazy(getFinalizer) {
+ let parent = this;
+ let type = this.withName("[auto " + this.name + ", (lazy)] ");
+ type.importFromC = function importFromC(value, operation) {
+ return ctypes.CDataFinalizer(
+ parent.importFromC(value, operation),
+ getFinalizer());
+ };
+ return type;
+ },
+
+ /**
+ * Return an alias to a type with a different name.
+ */
+ withName: function withName(name) {
+ return Object.create(this, {name: {value: name}});
+ },
+
+ /**
+ * Cast a C value to |this| type.
+ *
+ * Throw an error if the value cannot be casted.
+ */
+ cast: function cast(value) {
+ return ctypes.cast(value, this.implementation);
+ },
+
+ /**
+ * Return the number of bytes in a value of |this| type.
+ *
+ * This may not be defined, e.g. for |void_t|, array types
+ * without length, etc.
+ */
+ get size() {
+ return this.implementation.size;
+ }
+};
+
+/**
+ * Utility function used to determine whether an object is a typed array
+ */
+var isTypedArray = function isTypedArray(obj) {
+ return obj != null && typeof obj == "object"
+ && "byteOffset" in obj;
+};
+exports.isTypedArray = isTypedArray;
+
+/**
+ * Utility function used to determine whether an object is an ArrayBuffer.
+ */
+var isArrayBuffer = function(obj) {
+ return obj != null && typeof obj == "object" &&
+ obj.constructor.name == "ArrayBuffer";
+};
+exports.isArrayBuffer = isArrayBuffer;
+
+/**
+ * A |Type| of pointers.
+ *
+ * @param {string} name The name of this type.
+ * @param {CType} implementation The type of this pointer.
+ * @param {Type} targetType The target type.
+ */
+function PtrType(name, implementation, targetType) {
+ Type.call(this, name, implementation);
+ if (targetType == null || !targetType instanceof Type) {
+ throw new TypeError("targetType must be an instance of Type");
+ }
+ /**
+ * The type of values targeted by this pointer type.
+ */
+ Object.defineProperty(this, "targetType", {
+ value: targetType
+ });
+}
+PtrType.prototype = Object.create(Type.prototype);
+
+/**
+ * Convert a value to a pointer.
+ *
+ * Protocol:
+ * - |null| returns |null|
+ * - a string returns |{string: value}|
+ * - a typed array returns |{ptr: address_of_buffer}|
+ * - a C array returns |{ptr: address_of_buffer}|
+ * everything else raises an error
+ */
+PtrType.prototype.toMsg = function ptr_toMsg(value) {
+ if (value == null) {
+ return null;
+ }
+ if (typeof value == "string") {
+ return { string: value };
+ }
+ if (isTypedArray(value)) {
+ // Automatically transfer typed arrays
+ return new Meta({data: value}, {transfers: [value.buffer]});
+ }
+ if (isArrayBuffer(value)) {
+ // Automatically transfer array buffers
+ return new Meta({data: value}, {transfers: [value]});
+ }
+ let normalized;
+ if ("addressOfElement" in value) { // C array
+ normalized = value.addressOfElement(0);
+ } else if ("isNull" in value) { // C pointer
+ normalized = value;
+ } else {
+ throw new TypeError("Value " + value +
+ " cannot be converted to a pointer");
+ }
+ let cast = Type.uintptr_t.cast(normalized);
+ return {ptr: cast.value.toString()};
+};
+
+/**
+ * Convert a message back to a pointer.
+ */
+PtrType.prototype.fromMsg = function ptr_fromMsg(msg) {
+ if (msg == null) {
+ return null;
+ }
+ if ("string" in msg) {
+ return msg.string;
+ }
+ if ("data" in msg) {
+ return msg.data;
+ }
+ if ("ptr" in msg) {
+ let address = ctypes.uintptr_t(msg.ptr);
+ return this.cast(address);
+ }
+ throw new TypeError("Message " + msg.toSource() +
+ " does not represent a pointer");
+};
+
+exports.Type = Type;
+
+
+/*
+ * Some values are large integers on 64 bit platforms. Unfortunately,
+ * in practice, 64 bit integers cannot be manipulated in JS. We
+ * therefore project them to regular numbers whenever possible.
+ */
+
+var projectLargeInt = function projectLargeInt(x) {
+ let str = x.toString();
+ let rv = parseInt(str, 10);
+ if (rv.toString() !== str) {
+ throw new TypeError("Number " + str + " cannot be projected to a double");
+ }
+ return rv;
+};
+var projectLargeUInt = function projectLargeUInt(x) {
+ return projectLargeInt(x);
+};
+var projectValue = function projectValue(x) {
+ if (!(x instanceof ctypes.CData)) {
+ return x;
+ }
+ if (!("value" in x)) { // Sanity check
+ throw new TypeError("Number " + x.toSource() + " has no field |value|");
+ }
+ return x.value;
+};
+
+function projector(type, signed) {
+ LOG("Determining best projection for", type,
+ "(size: ", type.size, ")", signed?"signed":"unsigned");
+ if (type instanceof Type) {
+ type = type.implementation;
+ }
+ if (!type.size) {
+ throw new TypeError("Argument is not a proper C type");
+ }
+ // Determine if type is projected to Int64/Uint64
+ if (type.size == 8 // Usual case
+ // The following cases have special treatment in js-ctypes
+ // Regardless of their size, the value getter returns
+ // a Int64/Uint64
+ || type == ctypes.size_t // Special cases
+ || type == ctypes.ssize_t
+ || type == ctypes.intptr_t
+ || type == ctypes.uintptr_t
+ || type == ctypes.off_t) {
+ if (signed) {
+ LOG("Projected as a large signed integer");
+ return projectLargeInt;
+ } else {
+ LOG("Projected as a large unsigned integer");
+ return projectLargeUInt;
+ }
+ }
+ LOG("Projected as a regular number");
+ return projectValue;
+};
+exports.projectValue = projectValue;
+
+/**
+ * Get the appropriate type for an unsigned int of the given size.
+ *
+ * This function is useful to define types such as |mode_t| whose
+ * actual width depends on the OS/platform.
+ *
+ * @param {number} size The number of bytes requested.
+ */
+Type.uintn_t = function uintn_t(size) {
+ switch (size) {
+ case 1: return Type.uint8_t;
+ case 2: return Type.uint16_t;
+ case 4: return Type.uint32_t;
+ case 8: return Type.uint64_t;
+ default:
+ throw new Error("Cannot represent unsigned integers of " + size + " bytes");
+ }
+};
+
+/**
+ * Get the appropriate type for an signed int of the given size.
+ *
+ * This function is useful to define types such as |mode_t| whose
+ * actual width depends on the OS/platform.
+ *
+ * @param {number} size The number of bytes requested.
+ */
+Type.intn_t = function intn_t(size) {
+ switch (size) {
+ case 1: return Type.int8_t;
+ case 2: return Type.int16_t;
+ case 4: return Type.int32_t;
+ case 8: return Type.int64_t;
+ default:
+ throw new Error("Cannot represent integers of " + size + " bytes");
+ }
+};
+
+/**
+ * Actual implementation of common C types.
+ */
+
+/**
+ * The void value.
+ */
+Type.void_t =
+ new Type("void",
+ ctypes.void_t);
+
+/**
+ * Shortcut for |void*|.
+ */
+Type.voidptr_t =
+ new PtrType("void*",
+ ctypes.voidptr_t,
+ Type.void_t);
+
+// void* is a special case as we can cast any pointer to/from it
+// so we have to shortcut |in_ptr|/|out_ptr|/|inout_ptr| and
+// ensure that js-ctypes' casting mechanism is invoked directly
+["in_ptr", "out_ptr", "inout_ptr"].forEach(function(key) {
+ Object.defineProperty(Type.void_t, key,
+ {
+ value: Type.voidptr_t
+ });
+});
+
+/**
+ * A Type of integers.
+ *
+ * @param {string} name The name of this type.
+ * @param {CType} implementation The underlying js-ctypes implementation.
+ * @param {bool} signed |true| if this is a type of signed integers,
+ * |false| otherwise.
+ *
+ * @constructor
+ */
+function IntType(name, implementation, signed) {
+ Type.call(this, name, implementation);
+ this.importFromC = projector(implementation, signed);
+ this.project = this.importFromC;
+};
+IntType.prototype = Object.create(Type.prototype);
+IntType.prototype.toMsg = function toMsg(value) {
+ if (typeof value == "number") {
+ return value;
+ }
+ return this.project(value);
+};
+
+/**
+ * A C char (one byte)
+ */
+Type.char =
+ new Type("char",
+ ctypes.char);
+
+/**
+ * A C wide char (two bytes)
+ */
+Type.char16_t =
+ new Type("char16_t",
+ ctypes.char16_t);
+
+ /**
+ * Base string types.
+ */
+Type.cstring = Type.char.in_ptr.withName("[in] C string");
+Type.wstring = Type.char16_t.in_ptr.withName("[in] wide string");
+Type.out_cstring = Type.char.out_ptr.withName("[out] C string");
+Type.out_wstring = Type.char16_t.out_ptr.withName("[out] wide string");
+
+/**
+ * A C integer (8-bits).
+ */
+Type.int8_t =
+ new IntType("int8_t", ctypes.int8_t, true);
+
+Type.uint8_t =
+ new IntType("uint8_t", ctypes.uint8_t, false);
+
+/**
+ * A C integer (16-bits).
+ *
+ * Also known as WORD under Windows.
+ */
+Type.int16_t =
+ new IntType("int16_t", ctypes.int16_t, true);
+
+Type.uint16_t =
+ new IntType("uint16_t", ctypes.uint16_t, false);
+
+/**
+ * A C integer (32-bits).
+ *
+ * Also known as DWORD under Windows.
+ */
+Type.int32_t =
+ new IntType("int32_t", ctypes.int32_t, true);
+
+Type.uint32_t =
+ new IntType("uint32_t", ctypes.uint32_t, false);
+
+/**
+ * A C integer (64-bits).
+ */
+Type.int64_t =
+ new IntType("int64_t", ctypes.int64_t, true);
+
+Type.uint64_t =
+ new IntType("uint64_t", ctypes.uint64_t, false);
+
+ /**
+ * A C integer
+ *
+ * Size depends on the platform.
+ */
+Type.int = Type.intn_t(ctypes.int.size).
+ withName("int");
+
+Type.unsigned_int = Type.intn_t(ctypes.unsigned_int.size).
+ withName("unsigned int");
+
+/**
+ * A C long integer.
+ *
+ * Size depends on the platform.
+ */
+Type.long =
+ Type.intn_t(ctypes.long.size).withName("long");
+
+Type.unsigned_long =
+ Type.intn_t(ctypes.unsigned_long.size).withName("unsigned long");
+
+/**
+ * An unsigned integer with the same size as a pointer.
+ *
+ * Used to cast a pointer to an integer, whenever necessary.
+ */
+Type.uintptr_t =
+ Type.uintn_t(ctypes.uintptr_t.size).withName("uintptr_t");
+
+/**
+ * A boolean.
+ * Implemented as a C integer.
+ */
+Type.bool = Type.int.withName("bool");
+Type.bool.importFromC = function projectBool(x) {
+ return !!(x.value);
+};
+
+/**
+ * A user identifier.
+ *
+ * Implemented as a C integer.
+ */
+Type.uid_t =
+ Type.int.withName("uid_t");
+
+/**
+ * A group identifier.
+ *
+ * Implemented as a C integer.
+ */
+Type.gid_t =
+ Type.int.withName("gid_t");
+
+/**
+ * An offset (positive or negative).
+ *
+ * Implemented as a C integer.
+ */
+Type.off_t =
+ new IntType("off_t", ctypes.off_t, true);
+
+/**
+ * A size (positive).
+ *
+ * Implemented as a C size_t.
+ */
+Type.size_t =
+ new IntType("size_t", ctypes.size_t, false);
+
+/**
+ * An offset (positive or negative).
+ * Implemented as a C integer.
+ */
+Type.ssize_t =
+ new IntType("ssize_t", ctypes.ssize_t, true);
+
+/**
+ * Encoding/decoding strings
+ */
+Type.uencoder =
+ new Type("uencoder", ctypes.StructType("uencoder"));
+Type.udecoder =
+ new Type("udecoder", ctypes.StructType("udecoder"));
+
+/**
+ * Utility class, used to build a |struct| type
+ * from a set of field names, types and offsets.
+ *
+ * @param {string} name The name of the |struct| type.
+ * @param {number} size The total size of the |struct| type in bytes.
+ */
+function HollowStructure(name, size) {
+ if (!name) {
+ throw new TypeError("HollowStructure expects a name");
+ }
+ if (!size || size < 0) {
+ throw new TypeError("HollowStructure expects a (positive) size");
+ }
+
+ // A mapping from offsets in the struct to name/type pairs
+ // (or nothing if no field starts at that offset).
+ this.offset_to_field_info = [];
+
+ // The name of the struct
+ this.name = name;
+
+ // The size of the struct, in bytes
+ this.size = size;
+
+ // The number of paddings inserted so far.
+ // Used to give distinct names to padding fields.
+ this._paddings = 0;
+}
+HollowStructure.prototype = {
+ /**
+ * Add a field at a given offset.
+ *
+ * @param {number} offset The offset at which to insert the field.
+ * @param {string} name The name of the field.
+ * @param {CType|Type} type The type of the field.
+ */
+ add_field_at: function add_field_at(offset, name, type) {
+ if (offset == null) {
+ throw new TypeError("add_field_at requires a non-null offset");
+ }
+ if (!name) {
+ throw new TypeError("add_field_at requires a non-null name");
+ }
+ if (!type) {
+ throw new TypeError("add_field_at requires a non-null type");
+ }
+ if (type instanceof Type) {
+ type = type.implementation;
+ }
+ if (this.offset_to_field_info[offset]) {
+ throw new Error("HollowStructure " + this.name +
+ " already has a field at offset " + offset);
+ }
+ if (offset + type.size > this.size) {
+ throw new Error("HollowStructure " + this.name +
+ " cannot place a value of type " + type +
+ " at offset " + offset +
+ " without exceeding its size of " + this.size);
+ }
+ let field = {name: name, type:type};
+ this.offset_to_field_info[offset] = field;
+ },
+
+ /**
+ * Create a pseudo-field that will only serve as padding.
+ *
+ * @param {number} size The number of bytes in the field.
+ * @return {Object} An association field-name => field-type,
+ * as expected by |ctypes.StructType|.
+ */
+ _makePaddingField: function makePaddingField(size) {
+ let field = ({});
+ field["padding_" + this._paddings] =
+ ctypes.ArrayType(ctypes.uint8_t, size);
+ this._paddings++;
+ return field;
+ },
+
+ /**
+ * Convert this |HollowStructure| into a |Type|.
+ */
+ getType: function getType() {
+ // Contents of the structure, in the format expected
+ // by ctypes.StructType.
+ let struct = [];
+
+ let i = 0;
+ while (i < this.size) {
+ let currentField = this.offset_to_field_info[i];
+ if (!currentField) {
+ // No field was specified at this offset, we need to
+ // introduce some padding.
+
+ // Firstly, determine how many bytes of padding
+ let padding_length = 1;
+ while (i + padding_length < this.size
+ && !this.offset_to_field_info[i + padding_length]) {
+ ++padding_length;
+ }
+
+ // Then add the padding
+ struct.push(this._makePaddingField(padding_length));
+
+ // And proceed
+ i += padding_length;
+ } else {
+ // We have a field at this offset.
+
+ // Firstly, ensure that we do not have two overlapping fields
+ for (let j = 1; j < currentField.type.size; ++j) {
+ let candidateField = this.offset_to_field_info[i + j];
+ if (candidateField) {
+ throw new Error("Fields " + currentField.name +
+ " and " + candidateField.name +
+ " overlap at position " + (i + j));
+ }
+ }
+
+ // Then add the field
+ let field = ({});
+ field[currentField.name] = currentField.type;
+ struct.push(field);
+
+ // And proceed
+ i += currentField.type.size;
+ }
+ }
+ let result = new Type(this.name, ctypes.StructType(this.name, struct));
+ if (result.implementation.size != this.size) {
+ throw new Error("Wrong size for type " + this.name +
+ ": expected " + this.size +
+ ", found " + result.implementation.size +
+ " (" + result.implementation.toSource() + ")");
+ }
+ return result;
+ }
+};
+exports.HollowStructure = HollowStructure;
+
+/**
+ * Representation of a native library.
+ *
+ * The native library is opened lazily, during the first call to its
+ * field |library| or whenever accessing one of the methods imported
+ * with declareLazyFFI.
+ *
+ * @param {string} name A human-readable name for the library. Used
+ * for debugging and error reporting.
+ * @param {string...} candidates A list of system libraries that may
+ * represent this library. Used e.g. to try different library names
+ * on distinct operating systems ("libxul", "XUL", etc.).
+ *
+ * @constructor
+ */
+function Library(name, ...candidates) {
+ this.name = name;
+ this._candidates = candidates;
+};
+Library.prototype = Object.freeze({
+ /**
+ * The native library as a js-ctypes object.
+ *
+ * @throws {Error} If none of the candidate libraries could be opened.
+ */
+ get library() {
+ let library;
+ delete this.library;
+ for (let candidate of this._candidates) {
+ try {
+ library = ctypes.open(candidate);
+ break;
+ } catch (ex) {
+ LOG("Could not open library", candidate, ex);
+ }
+ }
+ this._candidates = null;
+ if (library) {
+ Object.defineProperty(this, "library", {
+ value: library
+ });
+ Object.freeze(this);
+ return library;
+ }
+ let error = new Error("Could not open library " + this.name);
+ Object.defineProperty(this, "library", {
+ get: function() {
+ throw error;
+ }
+ });
+ Object.freeze(this);
+ throw error;
+ },
+
+ /**
+ * Declare a function, lazily.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {Type} returnType The type of values returned by the function.
+ * @param {...Type} argTypes The type of arguments to the function.
+ */
+ declareLazyFFI: function(object, field, ...args) {
+ let lib = this;
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ let ffi = declareFFI(lib.library, ...args);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ /**
+ * Define a js-ctypes function lazily using ctypes method declare.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {ctypes.CType} returnType The type of values returned by the function.
+ * @param {...ctypes.CType} argTypes The type of arguments to the function.
+ */
+ declareLazy: function(object, field, ...args) {
+ let lib = this;
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ let ffi = lib.library.declare(...args);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ /**
+ * Define a js-ctypes function lazily using ctypes method declare,
+ * with a fallback library to use if this library can't be opened
+ * or the function cannot be declared.
+ *
+ * @param {fallbacklibrary} The fallback Library object.
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {ctypes.CType} returnType The type of values returned by the function.
+ * @param {...ctypes.CType} argTypes The type of arguments to the function.
+ */
+ declareLazyWithFallback: function(fallbacklibrary, object, field, ...args) {
+ let lib = this;
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ try {
+ let ffi = lib.library.declare(...args);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ } catch (ex) {
+ // Use the fallback library and get the symbol from there.
+ fallbacklibrary.declareLazy(object, field, ...args);
+ return object[field];
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+ },
+
+ toString: function() {
+ return "[Library " + this.name + "]";
+ }
+});
+exports.Library = Library;
+
+/**
+ * Declare a function through js-ctypes
+ *
+ * @param {ctypes.library} lib The ctypes library holding the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {Type} returnType The type of values returned by the function.
+ * @param {...Type} argTypes The type of arguments to the function.
+ *
+ * @return null if the function could not be defined (generally because
+ * it does not exist), or a JavaScript wrapper performing the call to C
+ * and any type conversion required.
+ */
+var declareFFI = function declareFFI(lib, symbol, abi,
+ returnType /*, argTypes ...*/) {
+ LOG("Attempting to declare FFI ", symbol);
+ // We guard agressively, to avoid any late surprise
+ if (typeof symbol != "string") {
+ throw new TypeError("declareFFI expects as first argument a string");
+ }
+ abi = abi || ctypes.default_abi;
+ if (Object.prototype.toString.call(abi) != "[object CABI]") {
+ // Note: This is the only known manner of checking whether an object
+ // is an abi.
+ throw new TypeError("declareFFI expects as second argument an abi or null");
+ }
+ if (!returnType.importFromC) {
+ throw new TypeError("declareFFI expects as third argument an instance of Type");
+ }
+ let signature = [symbol, abi];
+ let argtypes = [];
+ for (let i = 3; i < arguments.length; ++i) {
+ let current = arguments[i];
+ if (!current) {
+ throw new TypeError("Missing type for argument " + ( i - 3 ) +
+ " of symbol " + symbol);
+ }
+ if (!current.implementation) {
+ throw new TypeError("Missing implementation for argument " + (i - 3)
+ + " of symbol " + symbol
+ + " ( " + current.name + " )" );
+ }
+ signature.push(current.implementation);
+ }
+ try {
+ let fun = lib.declare.apply(lib, signature);
+ let result = function ffi(...args) {
+ for (let i = 0; i < args.length; i++) {
+ if (typeof args[i] == "undefined") {
+ throw new TypeError("Argument " + i + " of " + symbol + " is undefined");
+ }
+ }
+ let result = fun.apply(fun, args);
+ return returnType.importFromC(result, symbol);
+ };
+ LOG("Function", symbol, "declared");
+ return result;
+ } catch (x) {
+ // Note: Not being able to declare a function is normal.
+ // Some functions are OS (or OS version)-specific.
+ LOG("Could not declare function ", symbol, x);
+ return null;
+ }
+};
+exports.declareFFI = declareFFI;
+
+/**
+ * Define a lazy getter to a js-ctypes function using declareFFI.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {ctypes.library} lib The ctypes library holding the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {Type} returnType The type of values returned by the function.
+ * @param {...Type} argTypes The type of arguments to the function.
+ */
+function declareLazyFFI(object, field, ...declareFFIArgs) {
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ let ffi = declareFFI(...declareFFIArgs);
+ if (ffi) {
+ return this[field] = ffi;
+ }
+ return undefined;
+ },
+ configurable: true,
+ enumerable: true
+ });
+}
+exports.declareLazyFFI = declareLazyFFI;
+
+/**
+ * Define a lazy getter to a js-ctypes function using ctypes method declare.
+ *
+ * @param {object} The object containing the function as a field.
+ * @param {string} The name of the field containing the function.
+ * @param {ctypes.library} lib The ctypes library holding the function.
+ * @param {string} symbol The name of the function, as defined in the
+ * library.
+ * @param {ctypes.abi} abi The abi to use, or |null| for default.
+ * @param {ctypes.CType} returnType The type of values returned by the function.
+ * @param {...ctypes.CType} argTypes The type of arguments to the function.
+ */
+function declareLazy(object, field, lib, ...declareArgs) {
+ Object.defineProperty(object, field, {
+ get: function() {
+ delete this[field];
+ try {
+ let ffi = lib.declare(...declareArgs);
+ return this[field] = ffi;
+ } catch (ex) {
+ // The symbol doesn't exist
+ return undefined;
+ }
+ },
+ configurable: true
+ });
+}
+exports.declareLazy = declareLazy;
+
+/**
+ * Utility function used to sanity check buffer and length arguments. The
+ * buffer must be a Typed Array.
+ *
+ * @param {Typed array} candidate The buffer.
+ * @param {number} bytes The number of bytes that |candidate| should contain.
+ *
+ * @return number The bytes argument clamped to the length of the buffer.
+ */
+function normalizeBufferArgs(candidate, bytes) {
+ if (!candidate) {
+ throw new TypeError("Expecting a Typed Array");
+ }
+ if (!isTypedArray(candidate)) {
+ throw new TypeError("Expecting a Typed Array");
+ }
+ if (bytes == null) {
+ bytes = candidate.byteLength;
+ } else if (candidate.byteLength < bytes) {
+ throw new TypeError("Buffer is too short. I need at least " +
+ bytes +
+ " bytes but I have only " +
+ candidate.byteLength +
+ "bytes");
+ }
+ return bytes;
+};
+exports.normalizeBufferArgs = normalizeBufferArgs;
+
+///////////////////// OS interactions
+
+/**
+ * An OS error.
+ *
+ * This class is provided mostly for type-matching. If you need more
+ * details about an error, you should use the platform-specific error
+ * codes provided by subclasses of |OS.Shared.Error|.
+ *
+ * @param {string} operation The operation that failed.
+ * @param {string=} path The path of the file on which the operation failed,
+ * or nothing if there was no file involved in the failure.
+ *
+ * @constructor
+ */
+function OSError(operation, path = "") {
+ Error.call(this);
+ this.operation = operation;
+ this.path = path;
+}
+OSError.prototype = Object.create(Error.prototype);
+exports.OSError = OSError;
+
+
+///////////////////// Temporary boilerplate
+// Boilerplate, to simplify the transition to require()
+// Do not rely upon this symbol, it will disappear with
+// bug 883050.
+exports.OS = {
+ Constants: exports.Constants,
+ Shared: {
+ LOG: LOG,
+ clone: clone,
+ Type: Type,
+ HollowStructure: HollowStructure,
+ Error: OSError,
+ declareFFI: declareFFI,
+ projectValue: projectValue,
+ isTypedArray: isTypedArray,
+ defineLazyGetter: defineLazyGetter
+ }
+};
+
+Object.defineProperty(exports.OS.Shared, "DEBUG", {
+ get: function() {
+ return Config.DEBUG;
+ },
+ set: function(x) {
+ return Config.DEBUG = x;
+ }
+});
+Object.defineProperty(exports.OS.Shared, "TEST", {
+ get: function() {
+ return Config.TEST;
+ },
+ set: function(x) {
+ return Config.TEST = x;
+ }
+});
+
+
+///////////////////// Permanent boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/osfile_shared_front.jsm b/toolkit/components/osfile/modules/osfile_shared_front.jsm
new file mode 100644
index 000000000..a2971991d
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm
@@ -0,0 +1,567 @@
+/* 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/. */
+
+/**
+ * Code shared by OS.File front-ends.
+ *
+ * This code is meant to be included by another library. It is also meant to
+ * be executed only on a worker thread.
+ */
+
+if (typeof Components != "undefined") {
+ throw new Error("osfile_shared_front.jsm cannot be used from the main thread");
+}
+(function(exports) {
+
+var SharedAll =
+ require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+var Path = require("resource://gre/modules/osfile/ospath.jsm");
+var Lz4 =
+ require("resource://gre/modules/lz4.js");
+var LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
+var clone = SharedAll.clone;
+
+/**
+ * Code shared by implementations of File.
+ *
+ * @param {*} fd An OS-specific file handle.
+ * @param {string} path File path of the file handle, used for error-reporting.
+ * @constructor
+ */
+var AbstractFile = function AbstractFile(fd, path) {
+ this._fd = fd;
+ if (!path) {
+ throw new TypeError("path is expected");
+ }
+ this._path = path;
+};
+
+AbstractFile.prototype = {
+ /**
+ * Return the file handle.
+ *
+ * @throw OS.File.Error if the file has been closed.
+ */
+ get fd() {
+ if (this._fd) {
+ return this._fd;
+ }
+ throw OS.File.Error.closed("accessing file", this._path);
+ },
+ /**
+ * Read bytes from this file to a new buffer.
+ *
+ * @param {number=} maybeBytes (deprecated, please use options.bytes)
+ * @param {JSON} options
+ * @return {Uint8Array} An array containing the bytes read.
+ */
+ read: function read(maybeBytes, options = {}) {
+ if (typeof maybeBytes === "object") {
+ // Caller has skipped `maybeBytes` and provided an options object.
+ options = clone(maybeBytes);
+ maybeBytes = null;
+ } else {
+ options = options || {};
+ }
+ let bytes = options.bytes || undefined;
+ if (bytes === undefined) {
+ bytes = maybeBytes == null ? this.stat().size : maybeBytes;
+ }
+ let buffer = new Uint8Array(bytes);
+ let pos = 0;
+ while (pos < bytes) {
+ let length = bytes - pos;
+ let view = new DataView(buffer.buffer, pos, length);
+ let chunkSize = this._read(view, length, options);
+ if (chunkSize == 0) {
+ break;
+ }
+ pos += chunkSize;
+ }
+ if (pos == bytes) {
+ return buffer;
+ } else {
+ return buffer.subarray(0, pos);
+ }
+ },
+
+ /**
+ * Write bytes from a buffer to this file.
+ *
+ * Note that, by default, this function may perform several I/O
+ * operations to ensure that the buffer is fully written.
+ *
+ * @param {Typed array} buffer The buffer in which the the bytes are
+ * stored. The buffer must be large enough to accomodate |bytes| bytes.
+ * @param {*=} options Optionally, an object that may contain the
+ * following fields:
+ * - {number} bytes The number of |bytes| to write from the buffer. If
+ * unspecified, this is |buffer.byteLength|.
+ *
+ * @return {number} The number of bytes actually written.
+ */
+ write: function write(buffer, options = {}) {
+ let bytes =
+ SharedAll.normalizeBufferArgs(buffer, ("bytes" in options) ? options.bytes : undefined);
+ let pos = 0;
+ while (pos < bytes) {
+ let length = bytes - pos;
+ let view = new DataView(buffer.buffer, buffer.byteOffset + pos, length);
+ let chunkSize = this._write(view, length, options);
+ pos += chunkSize;
+ }
+ return pos;
+ }
+};
+
+/**
+ * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name.
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
+ * If |false| use HEX numbers ie: filename-A65BC0.ext
+ * - {number} maxReadableNumber Used to limit the amount of tries after a failed
+ * file creation. Default is 20.
+ *
+ * @return {Object} contains A file object{file} and the path{path}.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+AbstractFile.openUnique = function openUnique(path, options = {}) {
+ let mode = {
+ create : true
+ };
+
+ let dirName = Path.dirname(path);
+ let leafName = Path.basename(path);
+ let lastDotCharacter = leafName.lastIndexOf('.');
+ let fileName = leafName.substring(0, lastDotCharacter != -1 ? lastDotCharacter : leafName.length);
+ let suffix = (lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : "");
+ let uniquePath = "";
+ let maxAttempts = options.maxAttempts || 99;
+ let humanReadable = !!options.humanReadable;
+ const HEX_RADIX = 16;
+ // We produce HEX numbers between 0 and 2^24 - 1.
+ const MAX_HEX_NUMBER = 16777215;
+
+ try {
+ return {
+ path: path,
+ file: OS.File.open(path, mode)
+ };
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
+ for (let i = 0; i < maxAttempts; ++i) {
+ try {
+ if (humanReadable) {
+ uniquePath = Path.join(dirName, fileName + "-" + (i + 1) + suffix);
+ } else {
+ let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX);
+ uniquePath = Path.join(dirName, fileName + "-" + hexNumber + suffix);
+ }
+ return {
+ path: uniquePath,
+ file: OS.File.open(uniquePath, mode)
+ };
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
+ // keep trying ...
+ }
+ }
+ throw OS.File.Error.exists("could not find an unused file name.", path);
+ }
+};
+
+/**
+ * Code shared by iterators.
+ */
+AbstractFile.AbstractIterator = function AbstractIterator() {
+};
+AbstractFile.AbstractIterator.prototype = {
+ /**
+ * Allow iterating with |for|
+ */
+ __iterator__: function __iterator__() {
+ return this;
+ },
+ /**
+ * Apply a function to all elements of the directory sequentially.
+ *
+ * @param {Function} cb This function will be applied to all entries
+ * of the directory. It receives as arguments
+ * - the OS.File.Entry corresponding to the entry;
+ * - the index of the entry in the enumeration;
+ * - the iterator itself - calling |close| on the iterator stops
+ * the loop.
+ */
+ forEach: function forEach(cb) {
+ let index = 0;
+ for (let entry in this) {
+ cb(entry, index++, this);
+ }
+ },
+ /**
+ * Return several entries at once.
+ *
+ * Entries are returned in the same order as a walk with |forEach| or
+ * |for(...)|.
+ *
+ * @param {number=} length If specified, the number of entries
+ * to return. If unspecified, return all remaining entries.
+ * @return {Array} An array containing the next |length| entries, or
+ * less if the iteration contains less than |length| entries left.
+ */
+ nextBatch: function nextBatch(length) {
+ let array = [];
+ let i = 0;
+ for (let entry in this) {
+ array.push(entry);
+ if (++i >= length) {
+ return array;
+ }
+ }
+ return array;
+ }
+};
+
+/**
+ * Utility function shared by implementations of |OS.File.open|:
+ * extract read/write/trunc/create/existing flags from a |mode|
+ * object.
+ *
+ * @param {*=} mode An object that may contain fields |read|,
+ * |write|, |truncate|, |create|, |existing|. These fields
+ * are interpreted only if true-ish.
+ * @return {{read:bool, write:bool, trunc:bool, create:bool,
+ * existing:bool}} an object recapitulating the options set
+ * by |mode|.
+ * @throws {TypeError} If |mode| contains other fields, or
+ * if it contains both |create| and |truncate|, or |create|
+ * and |existing|.
+ */
+AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
+ let result = {
+ read: false,
+ write: false,
+ trunc: false,
+ create: false,
+ existing: false,
+ append: true
+ };
+ for (let key in mode) {
+ let val = !!mode[key]; // bool cast.
+ switch (key) {
+ case "read":
+ result.read = val;
+ break;
+ case "write":
+ result.write = val;
+ break;
+ case "truncate": // fallthrough
+ case "trunc":
+ result.trunc = val;
+ result.write |= val;
+ break;
+ case "create":
+ result.create = val;
+ result.write |= val;
+ break;
+ case "existing": // fallthrough
+ case "exist":
+ result.existing = val;
+ break;
+ case "append":
+ result.append = val;
+ break;
+ default:
+ throw new TypeError("Mode " + key + " not understood");
+ }
+ }
+ // Reject opposite modes
+ if (result.existing && result.create) {
+ throw new TypeError("Cannot specify both existing:true and create:true");
+ }
+ if (result.trunc && result.create) {
+ throw new TypeError("Cannot specify both trunc:true and create:true");
+ }
+ // Handle read/write
+ if (!result.write) {
+ result.read = true;
+ }
+ return result;
+};
+
+/**
+ * Return the contents of a file.
+ *
+ * @param {string} path The path to the file.
+ * @param {number=} bytes Optionally, an upper bound to the number of bytes
+ * to read. DEPRECATED - please use options.bytes instead.
+ * @param {object=} options Optionally, an object with some of the following
+ * fields:
+ * - {number} bytes An upper bound to the number of bytes to read.
+ * - {string} compression If "lz4" and if the file is compressed using the lz4
+ * compression algorithm, decompress the file contents on the fly.
+ *
+ * @return {Uint8Array} A buffer holding the bytes
+ * and the number of bytes read from the file.
+ */
+AbstractFile.read = function read(path, bytes, options = {}) {
+ if (bytes && typeof bytes == "object") {
+ options = bytes;
+ bytes = options.bytes || null;
+ }
+ if ("encoding" in options && typeof options.encoding != "string") {
+ throw new TypeError("Invalid type for option encoding");
+ }
+ if ("compression" in options && typeof options.compression != "string") {
+ throw new TypeError("Invalid type for option compression: " + options.compression);
+ }
+ if ("bytes" in options && typeof options.bytes != "number") {
+ throw new TypeError("Invalid type for option bytes");
+ }
+ let file = exports.OS.File.open(path);
+ try {
+ let buffer = file.read(bytes, options);
+ if ("compression" in options) {
+ if (options.compression == "lz4") {
+ options = Object.create(options);
+ options.path = path;
+ buffer = Lz4.decompressFileContent(buffer, options);
+ } else {
+ throw OS.File.Error.invalidArgument("Compression");
+ }
+ }
+ if (!("encoding" in options)) {
+ return buffer;
+ }
+ let decoder;
+ try {
+ decoder = new TextDecoder(options.encoding);
+ } catch (ex if ex instanceof RangeError) {
+ throw OS.File.Error.invalidArgument("Decode");
+ }
+ return decoder.decode(buffer);
+ } finally {
+ file.close();
+ }
+};
+
+/**
+ * Write a file, atomically.
+ *
+ * By opposition to a regular |write|, this operation ensures that,
+ * until the contents are fully written, the destination file is
+ * not modified.
+ *
+ * Limitation: In a few extreme cases (hardware failure during the
+ * write, user unplugging disk during the write, etc.), data may be
+ * corrupted. If your data is user-critical (e.g. preferences,
+ * application data, etc.), you may wish to consider adding options
+ * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
+ * detailed below. Note that no combination of options can be
+ * guaranteed to totally eliminate the risk of corruption.
+ *
+ * @param {string} path The path of the file to modify.
+ * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
+ * @param {*=} options Optionally, an object determining the behavior
+ * of this function. This object may contain the following fields:
+ * - {number} bytes The number of bytes to write. If unspecified,
+ * |buffer.byteLength|. Required if |buffer| is a C pointer.
+ * - {string} tmpPath If |null| or unspecified, write all data directly
+ * to |path|. If specified, write all data to a temporary file called
+ * |tmpPath| and, once this write is complete, rename the file to
+ * replace |path|. Performing this additional operation is a little
+ * slower but also a little safer.
+ * - {bool} noOverwrite - If set, this function will fail if a file already
+ * exists at |path|.
+ * - {bool} flush - If |false| or unspecified, return immediately once the
+ * write is complete. If |true|, before writing, force the operating system
+ * to write its internal disk buffers to the disk. This is considerably slower
+ * (not just for the application but for the whole system) but also safer:
+ * if the system shuts down improperly (typically due to a kernel freeze
+ * or a power failure) or if the device is disconnected before the buffer
+ * is flushed, the file has more chances of not being corrupted.
+ * - {string} compression - If empty or unspecified, do not compress the file.
+ * If "lz4", compress the contents of the file atomically using lz4. For the
+ * time being, the container format is specific to Mozilla and cannot be read
+ * by means other than OS.File.read(..., { compression: "lz4"})
+ * - {string} backupTo - If specified, backup the destination file as |backupTo|.
+ * Note that this function renames the destination file before overwriting it.
+ * If the process or the operating system freezes or crashes
+ * during the short window between these operations,
+ * the destination file will have been moved to its backup.
+ *
+ * @return {number} The number of bytes actually written.
+ */
+AbstractFile.writeAtomic =
+ function writeAtomic(path, buffer, options = {}) {
+
+ // Verify that path is defined and of the correct type
+ if (typeof path != "string" || path == "") {
+ throw new TypeError("File path should be a (non-empty) string");
+ }
+ let noOverwrite = options.noOverwrite;
+ if (noOverwrite && OS.File.exists(path)) {
+ throw OS.File.Error.exists("writeAtomic", path);
+ }
+
+ if (typeof buffer == "string") {
+ // Normalize buffer to a C buffer by encoding it
+ let encoding = options.encoding || "utf-8";
+ buffer = new TextEncoder(encoding).encode(buffer);
+ }
+
+ if ("compression" in options && options.compression == "lz4") {
+ buffer = Lz4.compressFileContent(buffer, options);
+ options = Object.create(options);
+ options.bytes = buffer.byteLength;
+ }
+
+ let bytesWritten = 0;
+
+ if (!options.tmpPath) {
+ if (options.backupTo) {
+ try {
+ OS.File.move(path, options.backupTo, {noCopy: true});
+ } catch (ex if ex.becauseNoSuchFile) {
+ // The file doesn't exist, nothing to backup.
+ }
+ }
+ // Just write, without any renaming trick
+ let dest = OS.File.open(path, {write: true, truncate: true});
+ try {
+ bytesWritten = dest.write(buffer, options);
+ if (options.flush) {
+ dest.flush();
+ }
+ } finally {
+ dest.close();
+ }
+ return bytesWritten;
+ }
+
+ let tmpFile = OS.File.open(options.tmpPath, {write: true, truncate: true});
+ try {
+ bytesWritten = tmpFile.write(buffer, options);
+ if (options.flush) {
+ tmpFile.flush();
+ }
+ } catch (x) {
+ OS.File.remove(options.tmpPath);
+ throw x;
+ } finally {
+ tmpFile.close();
+ }
+
+ if (options.backupTo) {
+ try {
+ OS.File.move(path, options.backupTo, {noCopy: true});
+ } catch (ex if ex.becauseNoSuchFile) {
+ // The file doesn't exist, nothing to backup.
+ }
+ }
+
+ OS.File.move(options.tmpPath, path, {noCopy: true});
+ return bytesWritten;
+};
+
+/**
+ * This function is used by removeDir to avoid calling lstat for each
+ * files under the specified directory. External callers should not call
+ * this function directly.
+ */
+AbstractFile.removeRecursive = function(path, options = {}) {
+ let iterator = new OS.File.DirectoryIterator(path);
+ if (!iterator.exists()) {
+ if (!("ignoreAbsent" in options) || options.ignoreAbsent) {
+ return;
+ }
+ }
+
+ try {
+ for (let entry in iterator) {
+ if (entry.isDir) {
+ if (entry.isLink) {
+ // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
+ // directories are directories themselves. OS.File.remove()
+ // will not work for them.
+ OS.File.removeEmptyDir(entry.path, options);
+ } else {
+ // Normal directories.
+ AbstractFile.removeRecursive(entry.path, options);
+ }
+ } else {
+ // NTFS symlinks to files, Unix symlinks, or regular files.
+ OS.File.remove(entry.path, options);
+ }
+ }
+ } finally {
+ iterator.close();
+ }
+
+ OS.File.removeEmptyDir(path);
+};
+
+/**
+ * Create a directory and, optionally, its parent directories.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ *
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |path|
+ * must be a descendant of |from|, and that |from| and its existing
+ * subdirectories present in |path| must be user-writeable.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ * - {bool} ignoreExisting If |false|, throw an error if the directory
+ * already exists. |true| by default. Ignored if |from| is specified.
+ * - {number} unixMode Under Unix, if specified, a file creation mode,
+ * as per libc function |mkdir|. If unspecified, dirs are
+ * created with a default mode of 0700 (dir is private to
+ * the user, the user can read, write and execute). Ignored under Windows
+ * or if the file system does not support file creation modes.
+ * - {C pointer} winSecurity Under Windows, if specified, security
+ * attributes as per winapi function |CreateDirectory|. If
+ * unspecified, use the default security descriptor, inherited from
+ * the parent directory. Ignored under Unix or if the file system
+ * does not support security descriptors.
+ */
+AbstractFile.makeDir = function(path, options = {}) {
+ let from = options.from;
+ if (!from) {
+ OS.File._makeDir(path, options);
+ return;
+ }
+ if (!path.startsWith(from)) {
+ // Apparently, `from` is not a parent of `path`. However, we may
+ // have false negatives due to non-normalized paths, e.g.
+ // "foo//bar" is a parent of "foo/bar/sna".
+ path = Path.normalize(path);
+ from = Path.normalize(from);
+ if (!path.startsWith(from)) {
+ throw new Error("Incorrect use of option |from|: " + path + " is not a descendant of " + from);
+ }
+ }
+ let innerOptions = Object.create(options, {
+ ignoreExisting: {
+ value: true
+ }
+ });
+ // Compute the elements that appear in |path| but not in |from|.
+ let items = Path.split(path).components.slice(Path.split(from).components.length);
+ let current = from;
+ for (let item of items) {
+ current = Path.join(current, item);
+ OS.File._makeDir(current, innerOptions);
+ }
+};
+
+if (!exports.OS.Shared) {
+ exports.OS.Shared = {};
+}
+exports.OS.Shared.AbstractFile = AbstractFile;
+})(this);
diff --git a/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
new file mode 100644
index 000000000..632f9c40b
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_unix_allthreads.jsm
@@ -0,0 +1,375 @@
+/* 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/. */
+
+/**
+ * This module defines the thread-agnostic components of the Unix version
+ * of OS.File. It depends on the thread-agnostic cross-platform components
+ * of OS.File.
+ *
+ * It serves the following purposes:
+ * - open libc;
+ * - define OS.Unix.Error;
+ * - define a few constants and types that need to be defined on all platforms.
+ *
+ * This module can be:
+ * - opened from the main thread as a jsm module;
+ * - opened from a chrome worker through require().
+ */
+
+"use strict";
+
+var SharedAll;
+if (typeof Components != "undefined") {
+ let Cu = Components.utils;
+ // Module is opened as a jsm module
+ Cu.import("resource://gre/modules/ctypes.jsm", this);
+
+ SharedAll = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+ this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+ // Module is loaded with require()
+ SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+} else {
+ throw new Error("Please open this module with Component.utils.import or with require()");
+}
+
+var LOG = SharedAll.LOG.bind(SharedAll, "Unix", "allthreads");
+var Const = SharedAll.Constants.libc;
+
+// Open libc
+var libc = new SharedAll.Library("libc",
+ "libc.so", "libSystem.B.dylib", "a.out");
+exports.libc = libc;
+
+// Define declareFFI
+var declareFFI = SharedAll.declareFFI.bind(null, libc);
+exports.declareFFI = declareFFI;
+
+// Define lazy binding
+var LazyBindings = {};
+libc.declareLazy(LazyBindings, "strerror",
+ "strerror", ctypes.default_abi,
+ /*return*/ ctypes.char.ptr,
+ /*errnum*/ ctypes.int);
+
+/**
+ * A File-related error.
+ *
+ * To obtain a human-readable error message, use method |toString|.
+ * To determine the cause of the error, use the various |becauseX|
+ * getters. To determine the operation that failed, use field
+ * |operation|.
+ *
+ * Additionally, this implementation offers a field
+ * |unixErrno|, which holds the OS-specific error
+ * constant. If you need this level of detail, you may match the value
+ * of this field against the error constants of |OS.Constants.libc|.
+ *
+ * @param {string=} operation The operation that failed. If unspecified,
+ * the name of the calling function is taken to be the operation that
+ * failed.
+ * @param {number=} lastError The OS-specific constant detailing the
+ * reason of the error. If unspecified, this is fetched from the system
+ * status.
+ * @param {string=} path The file path that manipulated. If unspecified,
+ * assign the empty string.
+ *
+ * @constructor
+ * @extends {OS.Shared.Error}
+ */
+var OSError = function OSError(operation = "unknown operation",
+ errno = ctypes.errno, path = "") {
+ SharedAll.OSError.call(this, operation, path);
+ this.unixErrno = errno;
+};
+OSError.prototype = Object.create(SharedAll.OSError.prototype);
+OSError.prototype.toString = function toString() {
+ return "Unix error " + this.unixErrno +
+ " during operation " + this.operation +
+ (this.path? " on file " + this.path : "") +
+ " (" + LazyBindings.strerror(this.unixErrno).readString() + ")";
+};
+OSError.prototype.toMsg = function toMsg() {
+ return OSError.toMsg(this);
+};
+
+/**
+ * |true| if the error was raised because a file or directory
+ * already exists, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseExists", {
+ get: function becauseExists() {
+ return this.unixErrno == Const.EEXIST;
+ }
+});
+/**
+ * |true| if the error was raised because a file or directory
+ * does not exist, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseNoSuchFile", {
+ get: function becauseNoSuchFile() {
+ return this.unixErrno == Const.ENOENT;
+ }
+});
+
+/**
+ * |true| if the error was raised because a directory is not empty
+ * does not exist, |false| otherwise.
+ */
+ Object.defineProperty(OSError.prototype, "becauseNotEmpty", {
+ get: function becauseNotEmpty() {
+ return this.unixErrno == Const.ENOTEMPTY;
+ }
+ });
+/**
+ * |true| if the error was raised because a file or directory
+ * is closed, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseClosed", {
+ get: function becauseClosed() {
+ return this.unixErrno == Const.EBADF;
+ }
+});
+/**
+ * |true| if the error was raised because permission is denied to
+ * access a file or directory, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseAccessDenied", {
+ get: function becauseAccessDenied() {
+ return this.unixErrno == Const.EACCES;
+ }
+});
+/**
+ * |true| if the error was raised because some invalid argument was passed,
+ * |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseInvalidArgument", {
+ get: function becauseInvalidArgument() {
+ return this.unixErrno == Const.EINVAL;
+ }
+});
+
+/**
+ * Serialize an instance of OSError to something that can be
+ * transmitted across threads (not necessarily a string).
+ */
+OSError.toMsg = function toMsg(error) {
+ return {
+ exn: "OS.File.Error",
+ fileName: error.moduleName,
+ lineNumber: error.lineNumber,
+ stack: error.moduleStack,
+ operation: error.operation,
+ unixErrno: error.unixErrno,
+ path: error.path
+ };
+};
+
+/**
+ * Deserialize a message back to an instance of OSError
+ */
+OSError.fromMsg = function fromMsg(msg) {
+ let error = new OSError(msg.operation, msg.unixErrno, msg.path);
+ error.stack = msg.stack;
+ error.fileName = msg.fileName;
+ error.lineNumber = msg.lineNumber;
+ return error;
+};
+exports.Error = OSError;
+
+/**
+ * Code shared by implementations of File.Info on Unix
+ *
+ * @constructor
+*/
+var AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size, lastAccessDate,
+ lastModificationDate, unixLastStatusChangeDate,
+ unixOwner, unixGroup, unixMode) {
+ this._path = path;
+ this._isDir = isDir;
+ this._isSymlLink = isSymLink;
+ this._size = size;
+ this._lastAccessDate = lastAccessDate;
+ this._lastModificationDate = lastModificationDate;
+ this._unixLastStatusChangeDate = unixLastStatusChangeDate;
+ this._unixOwner = unixOwner;
+ this._unixGroup = unixGroup;
+ this._unixMode = unixMode;
+};
+
+AbstractInfo.prototype = {
+ /**
+ * The path of the file, used for error-reporting.
+ *
+ * @type {string}
+ */
+ get path() {
+ return this._path;
+ },
+ /**
+ * |true| if this file is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if this file is a symbolink link, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymlLink;
+ },
+ /**
+ * The size of the file, in bytes.
+ *
+ * Note that the result may be |NaN| if the size of the file cannot be
+ * represented in JavaScript.
+ *
+ * @type {number}
+ */
+ get size() {
+ return this._size;
+ },
+ /**
+ * The date of last access to this file.
+ *
+ * Note that the definition of last access may depend on the
+ * underlying operating system and file system.
+ *
+ * @type {Date}
+ */
+ get lastAccessDate() {
+ return this._lastAccessDate;
+ },
+ /**
+ * Return the date of last modification of this file.
+ */
+ get lastModificationDate() {
+ return this._lastModificationDate;
+ },
+ /**
+ * Return the date at which the status of this file was last modified
+ * (this is the date of the latest write/renaming/mode change/...
+ * of the file)
+ */
+ get unixLastStatusChangeDate() {
+ return this._unixLastStatusChangeDate;
+ },
+ /*
+ * Return the Unix owner of this file
+ */
+ get unixOwner() {
+ return this._unixOwner;
+ },
+ /*
+ * Return the Unix group of this file
+ */
+ get unixGroup() {
+ return this._unixGroup;
+ },
+ /*
+ * Return the Unix group of this file
+ */
+ get unixMode() {
+ return this._unixMode;
+ }
+};
+exports.AbstractInfo = AbstractInfo;
+
+/**
+ * Code shared by implementations of File.DirectoryIterator.Entry on Unix
+ *
+ * @constructor
+*/
+var AbstractEntry = function AbstractEntry(isDir, isSymLink, name, path) {
+ this._isDir = isDir;
+ this._isSymlLink = isSymLink;
+ this._name = name;
+ this._path = path;
+};
+
+AbstractEntry.prototype = {
+ /**
+ * |true| if the entry is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if the entry is a directory, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymlLink;
+ },
+ /**
+ * The name of the entry
+ * @type {string}
+ */
+ get name() {
+ return this._name;
+ },
+ /**
+ * The full path to the entry
+ */
+ get path() {
+ return this._path;
+ }
+};
+exports.AbstractEntry = AbstractEntry;
+
+// Special constants that need to be defined on all platforms
+
+exports.POS_START = Const.SEEK_SET;
+exports.POS_CURRENT = Const.SEEK_CUR;
+exports.POS_END = Const.SEEK_END;
+
+// Special types that need to be defined for communication
+// between threads
+var Type = Object.create(SharedAll.Type);
+exports.Type = Type;
+
+/**
+ * Native paths
+ *
+ * Under Unix, expressed as C strings
+ */
+Type.path = Type.cstring.withName("[in] path");
+Type.out_path = Type.out_cstring.withName("[out] path");
+
+// Special constructors that need to be defined on all threads
+OSError.closed = function closed(operation, path) {
+ return new OSError(operation, Const.EBADF, path);
+};
+
+OSError.exists = function exists(operation, path) {
+ return new OSError(operation, Const.EEXIST, path);
+};
+
+OSError.noSuchFile = function noSuchFile(operation, path) {
+ return new OSError(operation, Const.ENOENT, path);
+};
+
+OSError.invalidArgument = function invalidArgument(operation) {
+ return new OSError(operation, Const.EINVAL);
+};
+
+var EXPORTED_SYMBOLS = [
+ "declareFFI",
+ "libc",
+ "Error",
+ "AbstractInfo",
+ "AbstractEntry",
+ "Type",
+ "POS_START",
+ "POS_CURRENT",
+ "POS_END"
+];
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/osfile_unix_back.jsm b/toolkit/components/osfile/modules/osfile_unix_back.jsm
new file mode 100644
index 000000000..a028dda7d
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_unix_back.jsm
@@ -0,0 +1,735 @@
+/* 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/. */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_unix_back.jsm to be used directly as a main thread
+ // module yet. When time comes, it will be loaded by a combination of
+ // a main thread front-end/worker thread implementation that makes sure
+ // that we are not executing synchronous IO code in the main thread.
+
+ throw new Error("osfile_unix_back.jsm cannot be used from the main thread yet");
+ }
+ (function(exports) {
+ "use strict";
+ if (exports.OS && exports.OS.Unix && exports.OS.Unix.File) {
+ return; // Avoid double initialization
+ }
+
+ let SharedAll =
+ require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let SysAll =
+ require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm");
+ let LOG = SharedAll.LOG.bind(SharedAll, "Unix", "back");
+ let libc = SysAll.libc;
+ let Const = SharedAll.Constants.libc;
+
+ /**
+ * Initialize the Unix module.
+ *
+ * @param {function=} declareFFI
+ */
+ // FIXME: Both |init| and |aDeclareFFI| are deprecated, we should remove them
+ let init = function init(aDeclareFFI) {
+ let declareFFI;
+ if (aDeclareFFI) {
+ declareFFI = aDeclareFFI.bind(null, libc);
+ } else {
+ declareFFI = SysAll.declareFFI;
+ }
+ let declareLazyFFI = SharedAll.declareLazyFFI;
+
+ // Initialize types that require additional OS-specific
+ // support - either finalization or matching against
+ // OS-specific constants.
+ let Type = Object.create(SysAll.Type);
+ let SysFile = exports.OS.Unix.File = { Type: Type };
+
+ /**
+ * A file descriptor.
+ */
+ Type.fd = Type.int.withName("fd");
+ Type.fd.importFromC = function importFromC(fd_int) {
+ return ctypes.CDataFinalizer(fd_int, SysFile._close);
+ };
+
+
+ /**
+ * A C integer holding -1 in case of error or a file descriptor
+ * in case of success.
+ */
+ Type.negativeone_or_fd = Type.fd.withName("negativeone_or_fd");
+ Type.negativeone_or_fd.importFromC =
+ function importFromC(fd_int) {
+ if (fd_int == -1) {
+ return -1;
+ }
+ return ctypes.CDataFinalizer(fd_int, SysFile._close);
+ };
+
+ /**
+ * A C integer holding -1 in case of error or a meaningless value
+ * in case of success.
+ */
+ Type.negativeone_or_nothing =
+ Type.int.withName("negativeone_or_nothing");
+
+ /**
+ * A C integer holding -1 in case of error or a positive integer
+ * in case of success.
+ */
+ Type.negativeone_or_ssize_t =
+ Type.ssize_t.withName("negativeone_or_ssize_t");
+
+ /**
+ * Various libc integer types
+ */
+ Type.mode_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_MODE_T).withName("mode_t");
+ Type.uid_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_UID_T).withName("uid_t");
+ Type.gid_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_GID_T).withName("gid_t");
+
+ /**
+ * Type |time_t|
+ */
+ Type.time_t =
+ Type.intn_t(Const.OSFILE_SIZEOF_TIME_T).withName("time_t");
+
+ // Structure |dirent|
+ // Building this type is rather complicated, as its layout varies between
+ // variants of Unix. For this reason, we rely on a number of constants
+ // (computed in C from the C data structures) that give us the layout.
+ // The structure we compute looks like
+ // { int8_t[...] before_d_type; // ignored content
+ // int8_t d_type ;
+ // int8_t[...] before_d_name; // ignored content
+ // char[...] d_name;
+ // };
+ {
+ let d_name_extra_size = 0;
+ if (Const.OSFILE_SIZEOF_DIRENT_D_NAME < 8) {
+ // d_name is defined like "char d_name[1];" on some platforms
+ // (e.g. Solaris), we need to give it more size for our structure.
+ d_name_extra_size = 256;
+ }
+
+ let dirent = new SharedAll.HollowStructure("dirent",
+ Const.OSFILE_SIZEOF_DIRENT + d_name_extra_size);
+ if (Const.OSFILE_OFFSETOF_DIRENT_D_TYPE != undefined) {
+ // |dirent| doesn't have d_type on some platforms (e.g. Solaris).
+ dirent.add_field_at(Const.OSFILE_OFFSETOF_DIRENT_D_TYPE,
+ "d_type", ctypes.uint8_t);
+ }
+ dirent.add_field_at(Const.OSFILE_OFFSETOF_DIRENT_D_NAME,
+ "d_name", ctypes.ArrayType(ctypes.char,
+ Const.OSFILE_SIZEOF_DIRENT_D_NAME + d_name_extra_size));
+
+ // We now have built |dirent|.
+ Type.dirent = dirent.getType();
+ }
+ Type.null_or_dirent_ptr =
+ new SharedAll.Type("null_of_dirent",
+ Type.dirent.out_ptr.implementation);
+
+ // Structure |stat|
+ // Same technique
+ {
+ let stat = new SharedAll.HollowStructure("stat",
+ Const.OSFILE_SIZEOF_STAT);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_MODE,
+ "st_mode", Type.mode_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_UID,
+ "st_uid", Type.uid_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_GID,
+ "st_gid", Type.gid_t.implementation);
+
+ // Here, things get complicated with different data structures.
+ // Some platforms have |time_t st_atime| and some platforms have
+ // |timespec st_atimespec|. However, since |timespec| starts with
+ // a |time_t|, followed by nanoseconds, we just cheat and pretend
+ // that everybody has |time_t st_atime|, possibly followed by padding
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_ATIME,
+ "st_atime", Type.time_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_MTIME,
+ "st_mtime", Type.time_t.implementation);
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_CTIME,
+ "st_ctime", Type.time_t.implementation);
+
+ // To complicate further, MacOS and some BSDs have a field |birthtime|
+ if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) {
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_BIRTHTIME,
+ "st_birthtime", Type.time_t.implementation);
+ }
+
+ stat.add_field_at(Const.OSFILE_OFFSETOF_STAT_ST_SIZE,
+ "st_size", Type.off_t.implementation);
+ Type.stat = stat.getType();
+ }
+
+ // Structure |DIR|
+ if ("OSFILE_SIZEOF_DIR" in Const) {
+ // On platforms for which we need to access the fields of DIR
+ // directly (e.g. because certain functions are implemented
+ // as macros), we need to define DIR as a hollow structure.
+ let DIR = new SharedAll.HollowStructure(
+ "DIR",
+ Const.OSFILE_SIZEOF_DIR);
+
+ DIR.add_field_at(
+ Const.OSFILE_OFFSETOF_DIR_DD_FD,
+ "dd_fd",
+ Type.fd.implementation);
+
+ Type.DIR = DIR.getType();
+ } else {
+ // On other platforms, we keep DIR as a blackbox
+ Type.DIR =
+ new SharedAll.Type("DIR",
+ ctypes.StructType("DIR"));
+ }
+
+ Type.null_or_DIR_ptr =
+ Type.DIR.out_ptr.withName("null_or_DIR*");
+ Type.null_or_DIR_ptr.importFromC = function importFromC(dir) {
+ if (dir == null || dir.isNull()) {
+ return null;
+ }
+ return ctypes.CDataFinalizer(dir, SysFile._close_dir);
+ };
+
+ // Structure |timeval|
+ {
+ let timeval = new SharedAll.HollowStructure(
+ "timeval",
+ Const.OSFILE_SIZEOF_TIMEVAL);
+ timeval.add_field_at(
+ Const.OSFILE_OFFSETOF_TIMEVAL_TV_SEC,
+ "tv_sec",
+ Type.long.implementation);
+ timeval.add_field_at(
+ Const.OSFILE_OFFSETOF_TIMEVAL_TV_USEC,
+ "tv_usec",
+ Type.long.implementation);
+ Type.timeval = timeval.getType();
+ Type.timevals = new SharedAll.Type("two timevals",
+ ctypes.ArrayType(Type.timeval.implementation, 2));
+ }
+
+ // Types fsblkcnt_t and fsfilcnt_t, used by structure |statvfs|
+ Type.fsblkcnt_t =
+ Type.uintn_t(Const.OSFILE_SIZEOF_FSBLKCNT_T).withName("fsblkcnt_t");
+
+ // Structure |statvfs|
+ // Use an hollow structure
+ {
+ let statvfs = new SharedAll.HollowStructure("statvfs",
+ Const.OSFILE_SIZEOF_STATVFS);
+
+ statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BSIZE,
+ "f_bsize", Type.unsigned_long.implementation);
+ statvfs.add_field_at(Const.OSFILE_OFFSETOF_STATVFS_F_BAVAIL,
+ "f_bavail", Type.fsblkcnt_t.implementation);
+
+ Type.statvfs = statvfs.getType();
+ }
+
+ // Declare libc functions as functions of |OS.Unix.File|
+
+ // Finalizer-related functions
+ libc.declareLazy(SysFile, "_close",
+ "close", ctypes.default_abi,
+ /*return */ctypes.int,
+ /*fd*/ ctypes.int);
+
+ SysFile.close = function close(fd) {
+ // Detach the finalizer and call |_close|.
+ return fd.dispose();
+ };
+
+ libc.declareLazy(SysFile, "_close_dir",
+ "closedir", ctypes.default_abi,
+ /*return */ctypes.int,
+ /*dirp*/ Type.DIR.in_ptr.implementation);
+
+ SysFile.closedir = function closedir(fd) {
+ // Detach the finalizer and call |_close_dir|.
+ return fd.dispose();
+ };
+
+ {
+ // Symbol free() is special.
+ // We override the definition of free() on several platforms.
+ let default_lib = new SharedAll.Library("default_lib",
+ "a.out");
+
+ // On platforms for which we override free(), nspr defines
+ // a special library name "a.out" that will resolve to the
+ // correct implementation free().
+ // If it turns out we don't have an a.out library or a.out
+ // doesn't contain free, use the ordinary libc free.
+
+ default_lib.declareLazyWithFallback(libc, SysFile, "free",
+ "free", ctypes.default_abi,
+ /*return*/ ctypes.void_t,
+ /*ptr*/ ctypes.voidptr_t);
+ }
+
+
+ // Other functions
+ libc.declareLazyFFI(SysFile, "access",
+ "access", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*mode*/ Type.int);
+
+ libc.declareLazyFFI(SysFile, "chdir",
+ "chdir", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "chmod",
+ "chmod", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*mode*/ Type.mode_t);
+
+ libc.declareLazyFFI(SysFile, "chown",
+ "chown", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*uid*/ Type.uid_t,
+ /*gid*/ Type.gid_t);
+
+ libc.declareLazyFFI(SysFile, "copyfile",
+ "copyfile", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*source*/ Type.path,
+ /*dest*/ Type.path,
+ /*state*/ Type.void_t.in_ptr, // Ignored atm
+ /*flags*/ Type.uint32_t);
+
+ libc.declareLazyFFI(SysFile, "dup",
+ "dup", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_fd,
+ /*fd*/ Type.fd);
+
+ if ("OSFILE_SIZEOF_DIR" in Const) {
+ // On platforms for which |dirfd| is a macro
+ SysFile.dirfd =
+ function dirfd(DIRp) {
+ return Type.DIR.in_ptr.implementation(DIRp).contents.dd_fd;
+ };
+ } else {
+ // On platforms for which |dirfd| is a function
+ libc.declareLazyFFI(SysFile, "dirfd",
+ "dirfd", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_fd,
+ /*dir*/ Type.DIR.in_ptr);
+ }
+
+ libc.declareLazyFFI(SysFile, "chdir",
+ "chdir", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "fchdir",
+ "fchdir", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd);
+
+ libc.declareLazyFFI(SysFile, "fchmod",
+ "fchmod", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*mode*/ Type.mode_t);
+
+ libc.declareLazyFFI(SysFile, "fchown",
+ "fchown", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*uid_t*/ Type.uid_t,
+ /*gid_t*/ Type.gid_t);
+
+ libc.declareLazyFFI(SysFile, "fsync",
+ "fsync", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd);
+
+ libc.declareLazyFFI(SysFile, "getcwd",
+ "getcwd", ctypes.default_abi,
+ /*return*/ Type.out_path,
+ /*buf*/ Type.out_path,
+ /*size*/ Type.size_t);
+
+ libc.declareLazyFFI(SysFile, "getwd",
+ "getwd", ctypes.default_abi,
+ /*return*/ Type.out_path,
+ /*buf*/ Type.out_path);
+
+ // Two variants of |getwd| which allocate the memory
+ // dynamically.
+
+ // Linux/Android version
+ libc.declareLazyFFI(SysFile, "get_current_dir_name",
+ "get_current_dir_name", ctypes.default_abi,
+ /*return*/ Type.out_path.releaseWithLazy(() =>
+ SysFile.free
+ ));
+
+ // MacOS/BSD version (will return NULL on Linux/Android)
+ libc.declareLazyFFI(SysFile, "getwd_auto",
+ "getwd", ctypes.default_abi,
+ /*return*/ Type.out_path.releaseWithLazy(() =>
+ SysFile.free
+ ),
+ /*buf*/ Type.void_t.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "fdatasync",
+ "fdatasync", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd); // Note: MacOS/BSD-specific
+
+ libc.declareLazyFFI(SysFile, "ftruncate",
+ "ftruncate", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*length*/ Type.off_t);
+
+
+ libc.declareLazyFFI(SysFile, "lchown",
+ "lchown", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*uid_t*/ Type.uid_t,
+ /*gid_t*/ Type.gid_t);
+
+ libc.declareLazyFFI(SysFile, "link",
+ "link", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*source*/ Type.path,
+ /*dest*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "lseek",
+ "lseek", ctypes.default_abi,
+ /*return*/ Type.off_t,
+ /*fd*/ Type.fd,
+ /*offset*/ Type.off_t,
+ /*whence*/ Type.int);
+
+ libc.declareLazyFFI(SysFile, "mkdir",
+ "mkdir", ctypes.default_abi,
+ /*return*/ Type.int,
+ /*path*/ Type.path,
+ /*mode*/ Type.int);
+
+ libc.declareLazyFFI(SysFile, "mkstemp",
+ "mkstemp", ctypes.default_abi,
+ /*return*/ Type.fd,
+ /*template*/ Type.out_path);
+
+ libc.declareLazyFFI(SysFile, "open",
+ "open", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_fd,
+ /*path*/ Type.path,
+ /*oflags*/ Type.int,
+ /*mode*/ Type.int);
+
+ if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "opendir",
+ "__opendir30", ctypes.default_abi,
+ /*return*/ Type.null_or_DIR_ptr,
+ /*path*/ Type.path);
+ } else {
+ libc.declareLazyFFI(SysFile, "opendir",
+ "opendir", ctypes.default_abi,
+ /*return*/ Type.null_or_DIR_ptr,
+ /*path*/ Type.path);
+ }
+
+ libc.declareLazyFFI(SysFile, "pread",
+ "pread", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.out_ptr,
+ /*nbytes*/ Type.size_t,
+ /*offset*/ Type.off_t);
+
+ libc.declareLazyFFI(SysFile, "pwrite",
+ "pwrite", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.in_ptr,
+ /*nbytes*/ Type.size_t,
+ /*offset*/ Type.off_t);
+
+ libc.declareLazyFFI(SysFile, "read",
+ "read", ctypes.default_abi,
+ /*return*/Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.out_ptr,
+ /*nbytes*/Type.size_t);
+
+ libc.declareLazyFFI(SysFile, "posix_fadvise",
+ "posix_fadvise", ctypes.default_abi,
+ /*return*/ Type.int,
+ /*fd*/ Type.fd,
+ /*offset*/ Type.off_t,
+ /*len*/ Type.off_t,
+ /*advise*/ Type.int);
+
+ if (Const._DARWIN_FEATURE_64_BIT_INODE) {
+ // Special case for MacOS X 10.5+
+ // Symbol name "readdir" still exists but is used for a
+ // deprecated function that does not match the
+ // constants of |Const|.
+ libc.declareLazyFFI(SysFile, "readdir",
+ "readdir$INODE64", ctypes.default_abi,
+ /*return*/ Type.null_or_dirent_ptr,
+ /*dir*/ Type.DIR.in_ptr); // For MacOS X
+ } else if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "readdir",
+ "__readdir30", ctypes.default_abi,
+ /*return*/Type.null_or_dirent_ptr,
+ /*dir*/ Type.DIR.in_ptr); // Other Unices
+ } else {
+ libc.declareLazyFFI(SysFile, "readdir",
+ "readdir", ctypes.default_abi,
+ /*return*/Type.null_or_dirent_ptr,
+ /*dir*/ Type.DIR.in_ptr); // Other Unices
+ }
+
+ libc.declareLazyFFI(SysFile, "rename",
+ "rename", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*old*/ Type.path,
+ /*new*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "rmdir",
+ "rmdir", ctypes.default_abi,
+ /*return*/ Type.int,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "splice",
+ "splice", ctypes.default_abi,
+ /*return*/ Type.long,
+ /*fd_in*/ Type.fd,
+ /*off_in*/ Type.off_t.in_ptr,
+ /*fd_out*/ Type.fd,
+ /*off_out*/Type.off_t.in_ptr,
+ /*len*/ Type.size_t,
+ /*flags*/ Type.unsigned_int); // Linux/Android-specific
+
+ libc.declareLazyFFI(SysFile, "statfs",
+ "statfs", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.statvfs.out_ptr); // Android,B2G
+
+ libc.declareLazyFFI(SysFile, "statvfs",
+ "statvfs", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.statvfs.out_ptr); // Other platforms
+
+ libc.declareLazyFFI(SysFile, "symlink",
+ "symlink", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*source*/ Type.path,
+ /*dest*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "truncate",
+ "truncate", ctypes.default_abi,
+ /*return*/Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*length*/ Type.off_t);
+
+ libc.declareLazyFFI(SysFile, "unlink",
+ "unlink", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "write",
+ "write", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_ssize_t,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.void_t.in_ptr,
+ /*nbytes*/ Type.size_t);
+
+ // Weird cases that require special treatment
+
+ // OSes use a variety of hacks to differentiate between
+ // 32-bits and 64-bits versions of |stat|, |lstat|, |fstat|.
+ if (Const._DARWIN_FEATURE_64_BIT_INODE) {
+ // MacOS X 64-bits
+ libc.declareLazyFFI(SysFile, "stat",
+ "stat$INODE64", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "lstat",
+ "lstat$INODE64", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "fstat",
+ "fstat$INODE64", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr
+ );
+ } else if (Const._STAT_VER != undefined) {
+ const ver = Const._STAT_VER;
+ let xstat_name, lxstat_name, fxstat_name;
+ if (OS.Constants.Sys.Name == "SunOS") {
+ // Solaris
+ xstat_name = "_xstat";
+ lxstat_name = "_lxstat";
+ fxstat_name = "_fxstat";
+ } else {
+ // Linux, all widths
+ xstat_name = "__xstat";
+ lxstat_name = "__lxstat";
+ fxstat_name = "__fxstat";
+ }
+
+ let Stat = {};
+ libc.declareLazyFFI(Stat, "xstat",
+ xstat_name, ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*_stat_ver*/ Type.int,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr);
+ libc.declareLazyFFI(Stat, "lxstat",
+ lxstat_name, ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*_stat_ver*/ Type.int,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr);
+ libc.declareLazyFFI(Stat, "fxstat",
+ fxstat_name, ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*_stat_ver*/ Type.int,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr);
+
+
+ SysFile.stat = function stat(path, buf) {
+ return Stat.xstat(ver, path, buf);
+ };
+
+ SysFile.lstat = function lstat(path, buf) {
+ return Stat.lxstat(ver, path, buf);
+ };
+
+ SysFile.fstat = function fstat(fd, buf) {
+ return Stat.fxstat(ver, fd, buf);
+ };
+ } else if (OS.Constants.Sys.Name == "NetBSD") {
+ // NetBSD 5.0 and newer
+ libc.declareLazyFFI(SysFile, "stat",
+ "__stat50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "lstat",
+ "__lstat50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "fstat",
+ "__fstat50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr
+ );
+ } else {
+ // Mac OS X 32-bits, other Unix
+ libc.declareLazyFFI(SysFile, "stat",
+ "stat", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "lstat",
+ "lstat", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*buf*/ Type.stat.out_ptr
+ );
+ libc.declareLazyFFI(SysFile, "fstat",
+ "fstat", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*buf*/ Type.stat.out_ptr
+ );
+ }
+
+ // We cannot make a C array of CDataFinalizer, so
+ // pipe cannot be directly defined as a C function.
+
+ let Pipe = {};
+ libc.declareLazyFFI(Pipe, "_pipe",
+ "pipe", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fds*/ new SharedAll.Type("two file descriptors",
+ ctypes.ArrayType(ctypes.int, 2)));
+
+ // A shared per-thread buffer used to communicate with |pipe|
+ let _pipebuf = new (ctypes.ArrayType(ctypes.int, 2))();
+
+ SysFile.pipe = function pipe(array) {
+ let result = Pipe._pipe(_pipebuf);
+ if (result == -1) {
+ return result;
+ }
+ array[0] = ctypes.CDataFinalizer(_pipebuf[0], SysFile._close);
+ array[1] = ctypes.CDataFinalizer(_pipebuf[1], SysFile._close);
+ return result;
+ };
+
+ if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "utimes",
+ "__utimes50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ } else {
+ libc.declareLazyFFI(SysFile, "utimes",
+ "utimes", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*path*/ Type.path,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ }
+ if (OS.Constants.Sys.Name == "NetBSD") {
+ libc.declareLazyFFI(SysFile, "futimes",
+ "__futimes50", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ } else {
+ libc.declareLazyFFI(SysFile, "futimes",
+ "futimes", ctypes.default_abi,
+ /*return*/ Type.negativeone_or_nothing,
+ /*fd*/ Type.fd,
+ /*timeval[2]*/ Type.timevals.out_ptr
+ );
+ }
+ };
+
+ exports.OS.Unix = {
+ File: {
+ _init: init
+ }
+ };
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/osfile_unix_front.jsm b/toolkit/components/osfile/modules/osfile_unix_front.jsm
new file mode 100644
index 000000000..19a27ae1a
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm
@@ -0,0 +1,1193 @@
+/* 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/. */
+
+/**
+ * Synchronous front-end for the JavaScript OS.File library.
+ * Unix implementation.
+ *
+ * This front-end is meant to be imported by a worker thread.
+ */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_unix_front.jsm to be used directly as a main thread
+ // module yet.
+
+ throw new Error("osfile_unix_front.jsm cannot be used from the main thread yet");
+ }
+ (function(exports) {
+ "use strict";
+
+ // exports.OS.Unix is created by osfile_unix_back.jsm
+ if (exports.OS && exports.OS.File) {
+ return; // Avoid double-initialization
+ }
+
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let Path = require("resource://gre/modules/osfile/ospath.jsm");
+ let SysAll = require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm");
+ exports.OS.Unix.File._init();
+ let LOG = SharedAll.LOG.bind(SharedAll, "Unix front-end");
+ let Const = SharedAll.Constants.libc;
+ let UnixFile = exports.OS.Unix.File;
+ let Type = UnixFile.Type;
+
+ /**
+ * Representation of a file.
+ *
+ * You generally do not need to call this constructor yourself. Rather,
+ * to open a file, use function |OS.File.open|.
+ *
+ * @param fd A OS-specific file descriptor.
+ * @param {string} path File path of the file handle, used for error-reporting.
+ * @constructor
+ */
+ let File = function File(fd, path) {
+ exports.OS.Shared.AbstractFile.call(this, fd, path);
+ this._closeResult = null;
+ };
+ File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype);
+
+ /**
+ * Close the file.
+ *
+ * This method has no effect if the file is already closed. However,
+ * if the first call to |close| has thrown an error, further calls
+ * will throw the same error.
+ *
+ * @throws File.Error If closing the file revealed an error that could
+ * not be reported earlier.
+ */
+ File.prototype.close = function close() {
+ if (this._fd) {
+ let fd = this._fd;
+ this._fd = null;
+ // Call |close(fd)|, detach finalizer if any
+ // (|fd| may not be a CDataFinalizer if it has been
+ // instantiated from a controller thread).
+ let result = UnixFile._close(fd);
+ if (typeof fd == "object" && "forget" in fd) {
+ fd.forget();
+ }
+ if (result == -1) {
+ this._closeResult = new File.Error("close", ctypes.errno, this._path);
+ }
+ }
+ if (this._closeResult) {
+ throw this._closeResult;
+ }
+ return;
+ };
+
+ /**
+ * Read some bytes from a file.
+ *
+ * @param {C pointer} buffer A buffer for holding the data
+ * once it is read.
+ * @param {number} nbytes The number of bytes to read. It must not
+ * exceed the size of |buffer| in bytes but it may exceed the number
+ * of bytes unread in the file.
+ * @param {*=} options Additional options for reading. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively read. If zero,
+ * the end of the file has been reached.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._read = function _read(buffer, nbytes, options = {}) {
+ // Populate the page cache with data from a file so the subsequent reads
+ // from that file will not block on disk I/O.
+ if (typeof(UnixFile.posix_fadvise) === 'function' &&
+ (options.sequential || !("sequential" in options))) {
+ UnixFile.posix_fadvise(this.fd, 0, nbytes,
+ OS.Constants.libc.POSIX_FADV_SEQUENTIAL);
+ }
+ return throw_on_negative("read",
+ UnixFile.read(this.fd, buffer, nbytes),
+ this._path
+ );
+ };
+
+ /**
+ * Write some bytes to a file.
+ *
+ * @param {Typed array} buffer A buffer holding the data that must be
+ * written.
+ * @param {number} nbytes The number of bytes to write. It must not
+ * exceed the size of |buffer| in bytes.
+ * @param {*=} options Additional options for writing. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively written.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._write = function _write(buffer, nbytes, options = {}) {
+ return throw_on_negative("write",
+ UnixFile.write(this.fd, buffer, nbytes),
+ this._path
+ );
+ };
+
+ /**
+ * Return the current position in the file.
+ */
+ File.prototype.getPosition = function getPosition(pos) {
+ return this.setPosition(0, File.POS_CURRENT);
+ };
+
+ /**
+ * Change the current position in the file.
+ *
+ * @param {number} pos The new position. Whether this position
+ * is considered from the current position, from the start of
+ * the file or from the end of the file is determined by
+ * argument |whence|. Note that |pos| may exceed the length of
+ * the file.
+ * @param {number=} whence The reference position. If omitted
+ * or |OS.File.POS_START|, |pos| is relative to the start of the
+ * file. If |OS.File.POS_CURRENT|, |pos| is relative to the
+ * current position in the file. If |OS.File.POS_END|, |pos| is
+ * relative to the end of the file.
+ *
+ * @return The new position in the file.
+ */
+ File.prototype.setPosition = function setPosition(pos, whence) {
+ if (whence === undefined) {
+ whence = Const.SEEK_SET;
+ }
+ return throw_on_negative("setPosition",
+ UnixFile.lseek(this.fd, pos, whence),
+ this._path
+ );
+ };
+
+ /**
+ * Fetch the information on the file.
+ *
+ * @return File.Info The information on |this| file.
+ */
+ File.prototype.stat = function stat() {
+ throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr),
+ this._path);
+ return new File.Info(gStatData, this._path);
+ };
+
+ /**
+ * Set the file's access permissions.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+ File.prototype.setPermissions = function setPermissions(options = {}) {
+ throw_on_negative("setPermissions",
+ UnixFile.fchmod(this.fd, unixMode(options)),
+ this._path);
+ };
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * WARNING: This method is not implemented on Android/B2G. On Android/B2G,
+ * you should use File.setDates instead.
+ *
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid parameters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ if (SharedAll.Constants.Sys.Name != "Android") {
+ File.prototype.setDates = function(accessDate, modificationDate) {
+ let {value, ptr} = datesToTimevals(accessDate, modificationDate);
+ throw_on_negative("setDates",
+ UnixFile.futimes(this.fd, ptr),
+ this._path);
+ };
+ }
+
+ /**
+ * Flushes the file's buffers and causes all buffered data
+ * to be written.
+ * Disk flushes are very expensive and therefore should be used carefully,
+ * sparingly and only in scenarios where it is vital that data survives
+ * system crashes. Even though the function will be executed off the
+ * main-thread, it might still affect the overall performance of any
+ * running application.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype.flush = function flush() {
+ throw_on_negative("flush", UnixFile.fsync(this.fd), this._path);
+ };
+
+ // The default unix mode for opening (0600)
+ const DEFAULT_UNIX_MODE = 384;
+
+ /**
+ * Open a file
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} mode The opening mode for the file, as
+ * an object that may contain the following fields:
+ *
+ * - {bool} truncate If |true|, the file will be opened
+ * for writing. If the file does not exist, it will be
+ * created. If the file exists, its contents will be
+ * erased. Cannot be specified with |create|.
+ * - {bool} create If |true|, the file will be opened
+ * for writing. If the file exists, this function fails.
+ * If the file does not exist, it will be created. Cannot
+ * be specified with |truncate| or |existing|.
+ * - {bool} existing. If the file does not exist, this function
+ * fails. Cannot be specified with |create|.
+ * - {bool} read If |true|, the file will be opened for
+ * reading. The file may also be opened for writing, depending
+ * on the other fields of |mode|.
+ * - {bool} write If |true|, the file will be opened for
+ * writing. The file may also be opened for reading, depending
+ * on the other fields of |mode|.
+ * - {bool} append If |true|, the file will be opened for appending,
+ * meaning the equivalent of |.setPosition(0, POS_END)| is executed
+ * before each write. The default is |true|, i.e. opening a file for
+ * appending. Specify |append: false| to open the file in regular mode.
+ *
+ * If neither |truncate|, |create| or |write| is specified, the file
+ * is opened for reading.
+ *
+ * Note that |false|, |null| or |undefined| flags are simply ignored.
+ *
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} unixFlags If specified, file opening flags, as
+ * per libc function |open|. Replaces |mode|.
+ * - {number} unixMode If specified, a file creation mode,
+ * as per libc function |open|. If unspecified, files are
+ * created with a default mode of 0600 (file is private to the
+ * user, the user can read and write).
+ *
+ * @return {File} A file object.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+ File.open = function Unix_open(path, mode, options = {}) {
+ // We don't need to filter for the umask because "open" does this for us.
+ let omode = options.unixMode !== undefined ?
+ options.unixMode : DEFAULT_UNIX_MODE;
+ let flags;
+ if (options.unixFlags !== undefined) {
+ flags = options.unixFlags;
+ } else {
+ mode = OS.Shared.AbstractFile.normalizeOpenMode(mode);
+ // Handle read/write
+ if (!mode.write) {
+ flags = Const.O_RDONLY;
+ } else if (mode.read) {
+ flags = Const.O_RDWR;
+ } else {
+ flags = Const.O_WRONLY;
+ }
+ // Finally, handle create/existing/trunc
+ if (mode.trunc) {
+ if (mode.existing) {
+ flags |= Const.O_TRUNC;
+ } else {
+ flags |= Const.O_CREAT | Const.O_TRUNC;
+ }
+ } else if (mode.create) {
+ flags |= Const.O_CREAT | Const.O_EXCL;
+ } else if (mode.read && !mode.write) {
+ // flags are sufficient
+ } else if (!mode.existing) {
+ flags |= Const.O_CREAT;
+ }
+ if (mode.append) {
+ flags |= Const.O_APPEND;
+ }
+ }
+ return error_or_file(UnixFile.open(path, flags, omode), path);
+ };
+
+ /**
+ * Checks if a file exists
+ *
+ * @param {string} path The path to the file.
+ *
+ * @return {bool} true if the file exists, false otherwise.
+ */
+ File.exists = function Unix_exists(path) {
+ if (UnixFile.access(path, Const.F_OK) == -1) {
+ return false;
+ } else {
+ return true;
+ }
+ };
+
+ /**
+ * Remove an existing file.
+ *
+ * @param {string} path The name of the file.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the file does
+ * not exist. |true| by default.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.remove = function remove(path, options = {}) {
+ let result = UnixFile.unlink(path);
+ if (result == -1) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.errno == Const.ENOENT) {
+ return;
+ }
+ throw new File.Error("remove", ctypes.errno, path);
+ }
+ };
+
+ /**
+ * Remove an empty directory.
+ *
+ * @param {string} path The name of the directory to remove.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory
+ * does not exist. |true| by default
+ */
+ File.removeEmptyDir = function removeEmptyDir(path, options = {}) {
+ let result = UnixFile.rmdir(path);
+ if (result == -1) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.errno == Const.ENOENT) {
+ return;
+ }
+ throw new File.Error("removeEmptyDir", ctypes.errno, path);
+ }
+ };
+
+ /**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} sourcePath Platform-specific path to a directory on
+ * the disk to query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+ File.getAvailableFreeSpace = function Unix_getAvailableFreeSpace(sourcePath) {
+ let fileSystemInfo = new Type.statvfs.implementation();
+ let fileSystemInfoPtr = fileSystemInfo.address();
+
+ throw_on_negative("statvfs", (UnixFile.statvfs || UnixFile.statfs)(sourcePath, fileSystemInfoPtr));
+
+ let bytes = new Type.uint64_t.implementation(
+ fileSystemInfo.f_bsize * fileSystemInfo.f_bavail);
+
+ return bytes.value;
+ };
+
+ /**
+ * Default mode for opening directories.
+ */
+ const DEFAULT_UNIX_MODE_DIR = Const.S_IRWXU;
+
+ /**
+ * Create a directory.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options. This
+ * implementation interprets the following fields:
+ *
+ * - {number} unixMode If specified, a file creation mode,
+ * as per libc function |mkdir|. If unspecified, dirs are
+ * created with a default mode of 0700 (dir is private to
+ * the user, the user can read, write and execute).
+ * - {bool} ignoreExisting If |false|, throw error if the directory
+ * already exists. |true| by default
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |from|
+ * and its existing descendants must be user-writeable and that |path|
+ * must be a descendant of |from|.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ */
+ File._makeDir = function makeDir(path, options = {}) {
+ let omode = options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE_DIR;
+ let result = UnixFile.mkdir(path, omode);
+ if (result == -1) {
+ if ((!("ignoreExisting" in options) || options.ignoreExisting) &&
+ (ctypes.errno == Const.EEXIST || ctypes.errno == Const.EISDIR)) {
+ return;
+ }
+ throw new File.Error("makeDir", ctypes.errno, path);
+ }
+ };
+
+ /**
+ * Copy a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be copied.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be copied with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.copy = null;
+
+ /**
+ * Move a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be moved.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ * @option {bool} noCopy - If set, this function will fail if the
+ * operation is more sophisticated than a simple renaming, i.e. if
+ * |sourcePath| and |destPath| are not situated on the same device.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be moved with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.move = null;
+
+ if (UnixFile.copyfile) {
+ // This implementation uses |copyfile(3)|, from the BSD library.
+ // Adding copying of hierarchies and/or attributes is just a flag
+ // away.
+ File.copy = function copyfile(sourcePath, destPath, options = {}) {
+ let flags = Const.COPYFILE_DATA;
+ if (options.noOverwrite) {
+ flags |= Const.COPYFILE_EXCL;
+ }
+ throw_on_negative("copy",
+ UnixFile.copyfile(sourcePath, destPath, null, flags),
+ sourcePath
+ );
+ };
+ } else {
+ // If the OS does not implement file copying for us, we need to
+ // implement it ourselves. For this purpose, we need to define
+ // a pumping function.
+
+ /**
+ * Copy bytes from one file to another one.
+ *
+ * @param {File} source The file containing the data to be copied. It
+ * should be opened for reading.
+ * @param {File} dest The file to which the data should be written. It
+ * should be opened for writing.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {number} nbytes The maximal number of bytes to
+ * copy. If unspecified, copy everything from the current
+ * position.
+ * @option {number} bufSize A hint regarding the size of the
+ * buffer to use for copying. The implementation may decide to
+ * ignore this hint.
+ * @option {bool} unixUserland Will force the copy operation to be
+ * caried out in user land, instead of using optimized syscalls such
+ * as splice(2).
+ *
+ * @throws {OS.File.Error} In case of error.
+ */
+ let pump;
+
+ // A buffer used by |pump_userland|
+ let pump_buffer = null;
+
+ // An implementation of |pump| using |read|/|write|
+ let pump_userland = function pump_userland(source, dest, options = {}) {
+ let bufSize = options.bufSize > 0 ? options.bufSize : 4096;
+ let nbytes = options.nbytes > 0 ? options.nbytes : Infinity;
+ if (!pump_buffer || pump_buffer.length < bufSize) {
+ pump_buffer = new (ctypes.ArrayType(ctypes.char))(bufSize);
+ }
+ let read = source._read.bind(source);
+ let write = dest._write.bind(dest);
+ // Perform actual copy
+ let total_read = 0;
+ while (true) {
+ let chunk_size = Math.min(nbytes, bufSize);
+ let bytes_just_read = read(pump_buffer, bufSize);
+ if (bytes_just_read == 0) {
+ return total_read;
+ }
+ total_read += bytes_just_read;
+ let bytes_written = 0;
+ do {
+ bytes_written += write(
+ pump_buffer.addressOfElement(bytes_written),
+ bytes_just_read - bytes_written
+ );
+ } while (bytes_written < bytes_just_read);
+ nbytes -= bytes_written;
+ if (nbytes <= 0) {
+ return total_read;
+ }
+ }
+ };
+
+ // Fortunately, under Linux, that pumping function can be optimized.
+ if (UnixFile.splice) {
+ const BUFSIZE = 1 << 17;
+
+ // An implementation of |pump| using |splice| (for Linux/Android)
+ pump = function pump_splice(source, dest, options = {}) {
+ let nbytes = options.nbytes > 0 ? options.nbytes : Infinity;
+ let pipe = [];
+ throw_on_negative("pump", UnixFile.pipe(pipe));
+ let pipe_read = pipe[0];
+ let pipe_write = pipe[1];
+ let source_fd = source.fd;
+ let dest_fd = dest.fd;
+ let total_read = 0;
+ let total_written = 0;
+ try {
+ while (true) {
+ let chunk_size = Math.min(nbytes, BUFSIZE);
+ let bytes_read = throw_on_negative("pump",
+ UnixFile.splice(source_fd, null,
+ pipe_write, null, chunk_size, 0)
+ );
+ if (!bytes_read) {
+ break;
+ }
+ total_read += bytes_read;
+ let bytes_written = throw_on_negative(
+ "pump",
+ UnixFile.splice(pipe_read, null,
+ dest_fd, null, bytes_read,
+ (bytes_read == chunk_size)?Const.SPLICE_F_MORE:0
+ ));
+ if (!bytes_written) {
+ // This should never happen
+ throw new Error("Internal error: pipe disconnected");
+ }
+ total_written += bytes_written;
+ nbytes -= bytes_read;
+ if (!nbytes) {
+ break;
+ }
+ }
+ return total_written;
+ } catch (x) {
+ if (x.unixErrno == Const.EINVAL) {
+ // We *might* be on a file system that does not support splice.
+ // Try again with a fallback pump.
+ if (total_read) {
+ source.setPosition(-total_read, File.POS_CURRENT);
+ }
+ if (total_written) {
+ dest.setPosition(-total_written, File.POS_CURRENT);
+ }
+ return pump_userland(source, dest, options);
+ }
+ throw x;
+ } finally {
+ pipe_read.dispose();
+ pipe_write.dispose();
+ }
+ };
+ } else {
+ // Fallback implementation of pump for other Unix platforms.
+ pump = pump_userland;
+ }
+
+ // Implement |copy| using |pump|.
+ // This implementation would require some work before being able to
+ // copy directories
+ File.copy = function copy(sourcePath, destPath, options = {}) {
+ let source, dest;
+ let result;
+ try {
+ source = File.open(sourcePath);
+ // Need to open the output file with |append:false|, or else |splice|
+ // won't work.
+ if (options.noOverwrite) {
+ dest = File.open(destPath, {create:true, append:false});
+ } else {
+ dest = File.open(destPath, {trunc:true, append:false});
+ }
+ if (options.unixUserland) {
+ result = pump_userland(source, dest, options);
+ } else {
+ result = pump(source, dest, options);
+ }
+ } catch (x) {
+ if (dest) {
+ dest.close();
+ }
+ if (source) {
+ source.close();
+ }
+ throw x;
+ }
+ };
+ } // End of definition of copy
+
+ // Implement |move| using |rename| (wherever possible) or |copy|
+ // (if files are on distinct devices).
+ File.move = function move(sourcePath, destPath, options = {}) {
+ // An implementation using |rename| whenever possible or
+ // |File.pump| when required, for other Unices.
+ // It can move directories on one file system, not
+ // across file systems
+
+ // If necessary, fail if the destination file exists
+ if (options.noOverwrite) {
+ let fd = UnixFile.open(destPath, Const.O_RDONLY, 0);
+ if (fd != -1) {
+ fd.dispose();
+ // The file exists and we have access
+ throw new File.Error("move", Const.EEXIST, sourcePath);
+ } else if (ctypes.errno == Const.EACCESS) {
+ // The file exists and we don't have access
+ throw new File.Error("move", Const.EEXIST, sourcePath);
+ }
+ }
+
+ // If we can, rename the file
+ let result = UnixFile.rename(sourcePath, destPath);
+ if (result != -1)
+ return;
+
+ // If the error is not EXDEV ("not on the same device"),
+ // or if the error is EXDEV and we have passed an option
+ // that prevents us from crossing devices, throw the
+ // error.
+ if (ctypes.errno != Const.EXDEV || options.noCopy) {
+ throw new File.Error("move", ctypes.errno, sourcePath);
+ }
+
+ // Otherwise, copy and remove.
+ File.copy(sourcePath, destPath, options);
+ // FIXME: Clean-up in case of copy error?
+ File.remove(sourcePath);
+ };
+
+ File.unixSymLink = function unixSymLink(sourcePath, destPath) {
+ throw_on_negative("symlink", UnixFile.symlink(sourcePath, destPath),
+ sourcePath);
+ };
+
+ /**
+ * Iterate on one directory.
+ *
+ * This iterator will not enter subdirectories.
+ *
+ * @param {string} path The directory upon which to iterate.
+ * @param {*=} options Ignored in this implementation.
+ *
+ * @throws {File.Error} If |path| does not represent a directory or
+ * if the directory cannot be iterated.
+ * @constructor
+ */
+ File.DirectoryIterator = function DirectoryIterator(path, options) {
+ exports.OS.Shared.AbstractFile.AbstractIterator.call(this);
+ this._path = path;
+ this._dir = UnixFile.opendir(this._path);
+ if (this._dir == null) {
+ let error = ctypes.errno;
+ if (error != Const.ENOENT) {
+ throw new File.Error("DirectoryIterator", error, path);
+ }
+ this._exists = false;
+ this._closed = true;
+ } else {
+ this._exists = true;
+ this._closed = false;
+ }
+ };
+ File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype);
+
+ /**
+ * Return the next entry in the directory, if any such entry is
+ * available.
+ *
+ * Skip special directories "." and "..".
+ *
+ * @return {File.Entry} The next entry in the directory.
+ * @throws {StopIteration} Once all files in the directory have been
+ * encountered.
+ */
+ File.DirectoryIterator.prototype.next = function next() {
+ if (!this._exists) {
+ throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
+ }
+ if (this._closed) {
+ throw StopIteration;
+ }
+ for (let entry = UnixFile.readdir(this._dir);
+ entry != null && !entry.isNull();
+ entry = UnixFile.readdir(this._dir)) {
+ let contents = entry.contents;
+ let name = contents.d_name.readString();
+ if (name == "." || name == "..") {
+ continue;
+ }
+
+ let isDir, isSymLink;
+ if (!("d_type" in contents)) {
+ // |dirent| doesn't have d_type on some platforms (e.g. Solaris).
+ let path = Path.join(this._path, name);
+ throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr), this._path);
+ isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR;
+ isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK;
+ } else {
+ isDir = contents.d_type == Const.DT_DIR;
+ isSymLink = contents.d_type == Const.DT_LNK;
+ }
+
+ return new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path);
+ }
+ this.close();
+ throw StopIteration;
+ };
+
+ /**
+ * Close the iterator and recover all resources.
+ * You should call this once you have finished iterating on a directory.
+ */
+ File.DirectoryIterator.prototype.close = function close() {
+ if (this._closed) return;
+ this._closed = true;
+ UnixFile.closedir(this._dir);
+ this._dir = null;
+ };
+
+ /**
+ * Determine whether the directory exists.
+ *
+ * @return {boolean}
+ */
+ File.DirectoryIterator.prototype.exists = function exists() {
+ return this._exists;
+ };
+
+ /**
+ * Return directory as |File|
+ */
+ File.DirectoryIterator.prototype.unixAsFile = function unixAsFile() {
+ if (!this._dir) throw File.Error.closed("unixAsFile", this._path);
+ return error_or_file(UnixFile.dirfd(this._dir), this._path);
+ };
+
+ /**
+ * An entry in a directory.
+ */
+ File.DirectoryIterator.Entry = function Entry(isDir, isSymLink, name, parent) {
+ // Copy the relevant part of |unix_entry| to ensure that
+ // our data is not overwritten prematurely.
+ this._parent = parent;
+ let path = Path.join(this._parent, name);
+
+ SysAll.AbstractEntry.call(this, isDir, isSymLink, name, path);
+ };
+ File.DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
+
+ /**
+ * Return a version of an instance of
+ * File.DirectoryIterator.Entry that can be sent from a worker
+ * thread to the main thread. Note that deserialization is
+ * asymmetric and returns an object with a different
+ * implementation.
+ */
+ File.DirectoryIterator.Entry.toMsg = function toMsg(value) {
+ if (!value instanceof File.DirectoryIterator.Entry) {
+ throw new TypeError("parameter of " +
+ "File.DirectoryIterator.Entry.toMsg must be a " +
+ "File.DirectoryIterator.Entry");
+ }
+ let serialized = {};
+ for (let key in File.DirectoryIterator.Entry.prototype) {
+ serialized[key] = value[key];
+ }
+ return serialized;
+ };
+
+ let gStatData = new Type.stat.implementation();
+ let gStatDataPtr = gStatData.address();
+
+ let MODE_MASK = 4095 /*= 07777*/;
+ File.Info = function Info(stat, path) {
+ let isDir = (stat.st_mode & Const.S_IFMT) == Const.S_IFDIR;
+ let isSymLink = (stat.st_mode & Const.S_IFMT) == Const.S_IFLNK;
+ let size = Type.off_t.importFromC(stat.st_size);
+
+ let lastAccessDate = new Date(stat.st_atime * 1000);
+ let lastModificationDate = new Date(stat.st_mtime * 1000);
+ let unixLastStatusChangeDate = new Date(stat.st_ctime * 1000);
+
+ let unixOwner = Type.uid_t.importFromC(stat.st_uid);
+ let unixGroup = Type.gid_t.importFromC(stat.st_gid);
+ let unixMode = Type.mode_t.importFromC(stat.st_mode & MODE_MASK);
+
+ SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size,
+ lastAccessDate, lastModificationDate, unixLastStatusChangeDate,
+ unixOwner, unixGroup, unixMode);
+
+ // Some platforms (e.g. MacOS X, some BSDs) store a file creation date
+ if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) {
+ let date = new Date(stat.st_birthtime * 1000);
+
+ /**
+ * The date of creation of this file.
+ *
+ * Note that the date returned by this method is not always
+ * reliable. Not all file systems are able to provide this
+ * information.
+ *
+ * @type {Date}
+ */
+ this.macBirthDate = date;
+ }
+ };
+ File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype);
+
+ // Deprecated, use macBirthDate/winBirthDate instead
+ Object.defineProperty(File.Info.prototype, "creationDate", {
+ get: function creationDate() {
+ // On the Macintosh, returns the birth date if available.
+ // On other Unix, as the birth date is not available,
+ // returns the epoch.
+ return this.macBirthDate || new Date(0);
+ }
+ });
+
+ /**
+ * Return a version of an instance of File.Info that can be sent
+ * from a worker thread to the main thread. Note that deserialization
+ * is asymmetric and returns an object with a different implementation.
+ */
+ File.Info.toMsg = function toMsg(stat) {
+ if (!stat instanceof File.Info) {
+ throw new TypeError("parameter of File.Info.toMsg must be a File.Info");
+ }
+ let serialized = {};
+ for (let key in File.Info.prototype) {
+ serialized[key] = stat[key];
+ }
+ return serialized;
+ };
+
+ /**
+ * Fetch the information on a file.
+ *
+ * @param {string} path The full name of the file to open.
+ * @param {*=} options Additional options. In this implementation:
+ *
+ * - {bool} unixNoFollowingLinks If set and |true|, if |path|
+ * represents a symbolic link, the call will return the information
+ * of the link itself, rather than that of the target file.
+ *
+ * @return {File.Information}
+ */
+ File.stat = function stat(path, options = {}) {
+ if (options.unixNoFollowingLinks) {
+ throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr), path);
+ } else {
+ throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr), path);
+ }
+ return new File.Info(gStatData, path);
+ };
+
+ /**
+ * Set the file's access permissions.
+ *
+ * This operation is likely to fail if applied to a file that was
+ * not created by the currently running program (more precisely,
+ * if it was created by a program running under a different OS-level
+ * user account). It may also fail, or silently do nothing, if the
+ * filesystem containing the file does not support access permissions.
+ *
+ * @param {string} path The name of the file to reset the permissions of.
+ * @param {*=} options Object specifying the requested permissions:
+ *
+ * - {number} unixMode The POSIX file mode to set on the file. If omitted,
+ * the POSIX file mode is reset to the default used by |OS.file.open|. If
+ * specified, the permissions will respect the process umask as if they
+ * had been specified as arguments of |OS.File.open|, unless the
+ * |unixHonorUmask| parameter tells otherwise.
+ * - {bool} unixHonorUmask If omitted or true, any |unixMode| value is
+ * modified by the process umask, as |OS.File.open| would have done. If
+ * false, the exact value of |unixMode| will be applied.
+ */
+ File.setPermissions = function setPermissions(path, options = {}) {
+ throw_on_negative("setPermissions",
+ UnixFile.chmod(path, unixMode(options)),
+ path);
+ };
+
+ /**
+ * Convert an access date and a modification date to an array
+ * of two |timeval|.
+ */
+ function datesToTimevals(accessDate, modificationDate) {
+ accessDate = normalizeDate("File.setDates", accessDate);
+ modificationDate = normalizeDate("File.setDates", modificationDate);
+
+ let timevals = new Type.timevals.implementation();
+ let timevalsPtr = timevals.address();
+
+ timevals[0].tv_sec = (accessDate / 1000) | 0;
+ timevals[0].tv_usec = 0;
+ timevals[1].tv_sec = (modificationDate / 1000) | 0;
+ timevals[1].tv_usec = 0;
+
+ return { value: timevals, ptr: timevalsPtr };
+ }
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * @param {string} path The full name of the file to set the dates for.
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid paramters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.setDates = function setDates(path, accessDate, modificationDate) {
+ let {value, ptr} = datesToTimevals(accessDate, modificationDate);
+ throw_on_negative("setDates",
+ UnixFile.utimes(path, ptr),
+ path);
+ };
+
+ File.read = exports.OS.Shared.AbstractFile.read;
+ File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
+ File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
+ File.makeDir = exports.OS.Shared.AbstractFile.makeDir;
+
+ /**
+ * Remove an existing directory and its contents.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't
+ * exist. |true| by default.
+ * - {boolean} ignorePermissions If |true|, remove the file even when lacking write
+ * permission.
+ *
+ * @throws {OS.File.Error} In case of I/O error, in particular if |path| is
+ * not a directory.
+ *
+ * Note: This function will remove a symlink even if it points a directory.
+ */
+ File.removeDir = function(path, options = {}) {
+ let isSymLink;
+ try {
+ let info = File.stat(path, {unixNoFollowingLinks: true});
+ isSymLink = info.isSymLink;
+ } catch (e) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.errno == Const.ENOENT) {
+ return;
+ }
+ throw e;
+ }
+ if (isSymLink) {
+ // A Unix symlink itself is not a directory even if it points
+ // a directory.
+ File.remove(path, options);
+ return;
+ }
+ exports.OS.Shared.AbstractFile.removeRecursive(path, options);
+ };
+
+ /**
+ * Get the current directory by getCurrentDirectory.
+ */
+ File.getCurrentDirectory = function getCurrentDirectory() {
+ let path, buf;
+ if (UnixFile.get_current_dir_name) {
+ path = UnixFile.get_current_dir_name();
+ } else if (UnixFile.getwd_auto) {
+ path = UnixFile.getwd_auto(null);
+ } else {
+ for (let length = Const.PATH_MAX; !path; length *= 2) {
+ buf = new (ctypes.char.array(length));
+ path = UnixFile.getcwd(buf, length);
+ };
+ }
+ throw_on_null("getCurrentDirectory", path);
+ return path.readString();
+ };
+
+ /**
+ * Set the current directory by setCurrentDirectory.
+ */
+ File.setCurrentDirectory = function setCurrentDirectory(path) {
+ throw_on_negative("setCurrentDirectory",
+ UnixFile.chdir(path),
+ path
+ );
+ };
+
+ /**
+ * Get/set the current directory.
+ */
+ Object.defineProperty(File, "curDir", {
+ set: function(path) {
+ this.setCurrentDirectory(path);
+ },
+ get: function() {
+ return this.getCurrentDirectory();
+ }
+ }
+ );
+
+ // Utility functions
+
+ /**
+ * Turn the result of |open| into an Error or a File
+ * @param {number} maybe The result of the |open| operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.errno, otherwise it returns the opened file.
+ * @param {string=} path The path of the file.
+ */
+ function error_or_file(maybe, path) {
+ if (maybe == -1) {
+ throw new File.Error("open", ctypes.errno, path);
+ }
+ return new File(maybe, path);
+ }
+
+ /**
+ * Utility function to sort errors represented as "-1" from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {number} result The result of the operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.errno, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_negative(operation, result, path) {
+ if (result < 0) {
+ throw new File.Error(operation, ctypes.errno, path);
+ }
+ return result;
+ }
+
+ /**
+ * Utility function to sort errors represented as |null| from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {pointer} result The result of the operation that may
+ * represent either an error or a success. If |null|, this function raises
+ * an error holding ctypes.errno, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_null(operation, result, path) {
+ if (result == null || (result.isNull && result.isNull())) {
+ throw new File.Error(operation, ctypes.errno, path);
+ }
+ return result;
+ }
+
+ /**
+ * Normalize and verify a Date or numeric date value.
+ *
+ * @param {string} fn Function name of the calling function.
+ * @param {Date,number} date The date to normalize. If omitted or null,
+ * then the current date will be used.
+ *
+ * @throws {TypeError} Invalid date provided.
+ *
+ * @return {number} Sanitized, numeric date in milliseconds since epoch.
+ */
+ function normalizeDate(fn, date) {
+ if (typeof date !== "number" && !date) {
+ // |date| was Omitted or null.
+ date = Date.now();
+ } else if (typeof date.getTime === "function") {
+ // Input might be a date or date-like object.
+ date = date.getTime();
+ }
+
+ if (typeof date !== "number" || Number.isNaN(date)) {
+ throw new TypeError("|date| parameter of " + fn + " must be a " +
+ "|Date| instance or number");
+ }
+ return date;
+ };
+
+ /**
+ * Helper used by both versions of setPermissions.
+ */
+ function unixMode(options) {
+ let mode = options.unixMode !== undefined ?
+ options.unixMode : DEFAULT_UNIX_MODE;
+ let unixHonorUmask = true;
+ if ("unixHonorUmask" in options) {
+ unixHonorUmask = options.unixHonorUmask;
+ }
+ if (unixHonorUmask) {
+ mode &= ~SharedAll.Constants.Sys.umask;
+ }
+ return mode;
+ }
+
+ File.Unix = exports.OS.Unix.File;
+ File.Error = SysAll.Error;
+ exports.OS.File = File;
+ exports.OS.Shared.Type = Type;
+
+ Object.defineProperty(File, "POS_START", { value: SysAll.POS_START });
+ Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT });
+ Object.defineProperty(File, "POS_END", { value: SysAll.POS_END });
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/osfile_win_allthreads.jsm b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
new file mode 100644
index 000000000..b059d4e12
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_win_allthreads.jsm
@@ -0,0 +1,425 @@
+/* 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/. */
+
+/**
+ * This module defines the thread-agnostic components of the Win version
+ * of OS.File. It depends on the thread-agnostic cross-platform components
+ * of OS.File.
+ *
+ * It serves the following purposes:
+ * - open kernel32;
+ * - define OS.Shared.Win.Error;
+ * - define a few constants and types that need to be defined on all platforms.
+ *
+ * This module can be:
+ * - opened from the main thread as a jsm module;
+ * - opened from a chrome worker through require().
+ */
+
+"use strict";
+
+var SharedAll;
+if (typeof Components != "undefined") {
+ let Cu = Components.utils;
+ // Module is opened as a jsm module
+ Cu.import("resource://gre/modules/ctypes.jsm", this);
+
+ SharedAll = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
+ this.exports = {};
+} else if (typeof module != "undefined" && typeof require != "undefined") {
+ // Module is loaded with require()
+ SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+} else {
+ throw new Error("Please open this module with Component.utils.import or with require()");
+}
+
+var LOG = SharedAll.LOG.bind(SharedAll, "Win", "allthreads");
+var Const = SharedAll.Constants.Win;
+
+// Open libc
+var libc = new SharedAll.Library("libc", "kernel32.dll");
+exports.libc = libc;
+
+// Define declareFFI
+var declareFFI = SharedAll.declareFFI.bind(null, libc);
+exports.declareFFI = declareFFI;
+
+var Scope = {};
+
+// Define Error
+libc.declareLazy(Scope, "FormatMessage",
+ "FormatMessageW", ctypes.winapi_abi,
+ /*return*/ ctypes.uint32_t,
+ /*flags*/ ctypes.uint32_t,
+ /*source*/ ctypes.voidptr_t,
+ /*msgid*/ ctypes.uint32_t,
+ /*langid*/ ctypes.uint32_t,
+ /*buf*/ ctypes.char16_t.ptr,
+ /*size*/ ctypes.uint32_t,
+ /*Arguments*/ctypes.voidptr_t);
+
+/**
+ * A File-related error.
+ *
+ * To obtain a human-readable error message, use method |toString|.
+ * To determine the cause of the error, use the various |becauseX|
+ * getters. To determine the operation that failed, use field
+ * |operation|.
+ *
+ * Additionally, this implementation offers a field
+ * |winLastError|, which holds the OS-specific error
+ * constant. If you need this level of detail, you may match the value
+ * of this field against the error constants of |OS.Constants.Win|.
+ *
+ * @param {string=} operation The operation that failed. If unspecified,
+ * the name of the calling function is taken to be the operation that
+ * failed.
+ * @param {number=} lastError The OS-specific constant detailing the
+ * reason of the error. If unspecified, this is fetched from the system
+ * status.
+ * @param {string=} path The file path that manipulated. If unspecified,
+ * assign the empty string.
+ *
+ * @constructor
+ * @extends {OS.Shared.Error}
+ */
+var OSError = function OSError(operation = "unknown operation",
+ lastError = ctypes.winLastError, path = "") {
+ operation = operation;
+ SharedAll.OSError.call(this, operation, path);
+ this.winLastError = lastError;
+};
+OSError.prototype = Object.create(SharedAll.OSError.prototype);
+OSError.prototype.toString = function toString() {
+ let buf = new (ctypes.ArrayType(ctypes.char16_t, 1024))();
+ let result = Scope.FormatMessage(
+ Const.FORMAT_MESSAGE_FROM_SYSTEM |
+ Const.FORMAT_MESSAGE_IGNORE_INSERTS,
+ null,
+ /* The error number */ this.winLastError,
+ /* Default language */ 0,
+ /* Output buffer*/ buf,
+ /* Minimum size of buffer */ 1024,
+ /* Format args*/ null
+ );
+ if (!result) {
+ buf = "additional error " +
+ ctypes.winLastError +
+ " while fetching system error message";
+ }
+ return "Win error " + this.winLastError + " during operation "
+ + this.operation + (this.path? " on file " + this.path : "") +
+ " (" + buf.readString() + ")";
+};
+OSError.prototype.toMsg = function toMsg() {
+ return OSError.toMsg(this);
+};
+
+/**
+ * |true| if the error was raised because a file or directory
+ * already exists, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseExists", {
+ get: function becauseExists() {
+ return this.winLastError == Const.ERROR_FILE_EXISTS ||
+ this.winLastError == Const.ERROR_ALREADY_EXISTS;
+ }
+});
+/**
+ * |true| if the error was raised because a file or directory
+ * does not exist, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseNoSuchFile", {
+ get: function becauseNoSuchFile() {
+ return this.winLastError == Const.ERROR_FILE_NOT_FOUND ||
+ this.winLastError == Const.ERROR_PATH_NOT_FOUND;
+ }
+});
+/**
+ * |true| if the error was raised because a directory is not empty
+ * does not exist, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseNotEmpty", {
+ get: function becauseNotEmpty() {
+ return this.winLastError == Const.ERROR_DIR_NOT_EMPTY;
+ }
+});
+/**
+ * |true| if the error was raised because a file or directory
+ * is closed, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseClosed", {
+ get: function becauseClosed() {
+ return this.winLastError == Const.ERROR_INVALID_HANDLE;
+ }
+});
+/**
+ * |true| if the error was raised because permission is denied to
+ * access a file or directory, |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseAccessDenied", {
+ get: function becauseAccessDenied() {
+ return this.winLastError == Const.ERROR_ACCESS_DENIED;
+ }
+});
+/**
+ * |true| if the error was raised because some invalid argument was passed,
+ * |false| otherwise.
+ */
+Object.defineProperty(OSError.prototype, "becauseInvalidArgument", {
+ get: function becauseInvalidArgument() {
+ return this.winLastError == Const.ERROR_NOT_SUPPORTED ||
+ this.winLastError == Const.ERROR_BAD_ARGUMENTS;
+ }
+});
+
+/**
+ * Serialize an instance of OSError to something that can be
+ * transmitted across threads (not necessarily a string).
+ */
+OSError.toMsg = function toMsg(error) {
+ return {
+ exn: "OS.File.Error",
+ fileName: error.moduleName,
+ lineNumber: error.lineNumber,
+ stack: error.moduleStack,
+ operation: error.operation,
+ winLastError: error.winLastError,
+ path: error.path
+ };
+};
+
+/**
+ * Deserialize a message back to an instance of OSError
+ */
+OSError.fromMsg = function fromMsg(msg) {
+ let error = new OSError(msg.operation, msg.winLastError, msg.path);
+ error.stack = msg.stack;
+ error.fileName = msg.fileName;
+ error.lineNumber = msg.lineNumber;
+ return error;
+};
+exports.Error = OSError;
+
+/**
+ * Code shared by implementation of File.Info on Windows
+ *
+ * @constructor
+ */
+var AbstractInfo = function AbstractInfo(path, isDir, isSymLink, size,
+ winBirthDate,
+ lastAccessDate, lastWriteDate,
+ winAttributes) {
+ this._path = path;
+ this._isDir = isDir;
+ this._isSymLink = isSymLink;
+ this._size = size;
+ this._winBirthDate = winBirthDate;
+ this._lastAccessDate = lastAccessDate;
+ this._lastModificationDate = lastWriteDate;
+ this._winAttributes = winAttributes;
+};
+
+AbstractInfo.prototype = {
+ /**
+ * The path of the file, used for error-reporting.
+ *
+ * @type {string}
+ */
+ get path() {
+ return this._path;
+ },
+ /**
+ * |true| if this file is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if this file is a symbolic link, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymLink;
+ },
+ /**
+ * The size of the file, in bytes.
+ *
+ * Note that the result may be |NaN| if the size of the file cannot be
+ * represented in JavaScript.
+ *
+ * @type {number}
+ */
+ get size() {
+ return this._size;
+ },
+ // Deprecated
+ get creationDate() {
+ return this._winBirthDate;
+ },
+ /**
+ * The date of creation of this file.
+ *
+ * @type {Date}
+ */
+ get winBirthDate() {
+ return this._winBirthDate;
+ },
+ /**
+ * The date of last access to this file.
+ *
+ * Note that the definition of last access may depend on the underlying
+ * operating system and file system.
+ *
+ * @type {Date}
+ */
+ get lastAccessDate() {
+ return this._lastAccessDate;
+ },
+ /**
+ * The date of last modification of this file.
+ *
+ * Note that the definition of last access may depend on the underlying
+ * operating system and file system.
+ *
+ * @type {Date}
+ */
+ get lastModificationDate() {
+ return this._lastModificationDate;
+ },
+ /**
+ * The Object with following boolean properties of this file.
+ * {readOnly, system, hidden}
+ *
+ * @type {object}
+ */
+ get winAttributes() {
+ return this._winAttributes;
+ }
+};
+exports.AbstractInfo = AbstractInfo;
+
+/**
+ * Code shared by implementation of File.DirectoryIterator.Entry on Windows
+ *
+ * @constructor
+ */
+var AbstractEntry = function AbstractEntry(isDir, isSymLink, name,
+ winCreationDate, winLastWriteDate,
+ winLastAccessDate, path) {
+ this._isDir = isDir;
+ this._isSymLink = isSymLink;
+ this._name = name;
+ this._winCreationDate = winCreationDate;
+ this._winLastWriteDate = winLastWriteDate;
+ this._winLastAccessDate = winLastAccessDate;
+ this._path = path;
+};
+
+AbstractEntry.prototype = {
+ /**
+ * |true| if the entry is a directory, |false| otherwise
+ */
+ get isDir() {
+ return this._isDir;
+ },
+ /**
+ * |true| if the entry is a symbolic link, |false| otherwise
+ */
+ get isSymLink() {
+ return this._isSymLink;
+ },
+ /**
+ * The name of the entry.
+ * @type {string}
+ */
+ get name() {
+ return this._name;
+ },
+ /**
+ * The creation time of this file.
+ * @type {Date}
+ */
+ get winCreationDate() {
+ return this._winCreationDate;
+ },
+ /**
+ * The last modification time of this file.
+ * @type {Date}
+ */
+ get winLastWriteDate() {
+ return this._winLastWriteDate;
+ },
+ /**
+ * The last access time of this file.
+ * @type {Date}
+ */
+ get winLastAccessDate() {
+ return this._winLastAccessDate;
+ },
+ /**
+ * The full path of the entry
+ * @type {string}
+ */
+ get path() {
+ return this._path;
+ }
+};
+exports.AbstractEntry = AbstractEntry;
+
+// Special constants that need to be defined on all platforms
+
+exports.POS_START = Const.FILE_BEGIN;
+exports.POS_CURRENT = Const.FILE_CURRENT;
+exports.POS_END = Const.FILE_END;
+
+// Special types that need to be defined for communication
+// between threads
+var Type = Object.create(SharedAll.Type);
+exports.Type = Type;
+
+/**
+ * Native paths
+ *
+ * Under Windows, expressed as wide strings
+ */
+Type.path = Type.wstring.withName("[in] path");
+Type.out_path = Type.out_wstring.withName("[out] path");
+
+// Special constructors that need to be defined on all threads
+OSError.closed = function closed(operation, path) {
+ return new OSError(operation, Const.ERROR_INVALID_HANDLE, path);
+};
+
+OSError.exists = function exists(operation, path) {
+ return new OSError(operation, Const.ERROR_FILE_EXISTS, path);
+};
+
+OSError.noSuchFile = function noSuchFile(operation, path) {
+ return new OSError(operation, Const.ERROR_FILE_NOT_FOUND, path);
+};
+
+OSError.invalidArgument = function invalidArgument(operation) {
+ return new OSError(operation, Const.ERROR_NOT_SUPPORTED);
+};
+
+var EXPORTED_SYMBOLS = [
+ "declareFFI",
+ "libc",
+ "Error",
+ "AbstractInfo",
+ "AbstractEntry",
+ "Type",
+ "POS_START",
+ "POS_CURRENT",
+ "POS_END"
+];
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/osfile_win_back.jsm b/toolkit/components/osfile/modules/osfile_win_back.jsm
new file mode 100644
index 000000000..21172b6bf
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_win_back.jsm
@@ -0,0 +1,437 @@
+/* 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/. */
+
+/**
+ * This file can be used in the following contexts:
+ *
+ * 1. included from a non-osfile worker thread using importScript
+ * (it serves to define a synchronous API for that worker thread)
+ * (bug 707681)
+ *
+ * 2. included from the main thread using Components.utils.import
+ * (it serves to define the asynchronous API, whose implementation
+ * resides in the worker thread)
+ * (bug 729057)
+ *
+ * 3. included from the osfile worker thread using importScript
+ * (it serves to define the implementation of the asynchronous API)
+ * (bug 729057)
+ */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_win.jsm to be used directly as a main thread
+ // module yet. When time comes, it will be loaded by a combination of
+ // a main thread front-end/worker thread implementation that makes sure
+ // that we are not executing synchronous IO code in the main thread.
+
+ throw new Error("osfile_win.jsm cannot be used from the main thread yet");
+ }
+
+ (function(exports) {
+ "use strict";
+ if (exports.OS && exports.OS.Win && exports.OS.Win.File) {
+ return; // Avoid double initialization
+ }
+
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let SysAll = require("resource://gre/modules/osfile/osfile_win_allthreads.jsm");
+ let LOG = SharedAll.LOG.bind(SharedAll, "Unix", "back");
+ let libc = SysAll.libc;
+ let advapi32 = new SharedAll.Library("advapi32", "advapi32.dll");
+ let Const = SharedAll.Constants.Win;
+
+ /**
+ * Initialize the Windows module.
+ *
+ * @param {function=} declareFFI
+ */
+ // FIXME: Both |init| and |aDeclareFFI| are deprecated, we should remove them
+ let init = function init(aDeclareFFI) {
+ let declareFFI;
+ if (aDeclareFFI) {
+ declareFFI = aDeclareFFI.bind(null, libc);
+ } else {
+ declareFFI = SysAll.declareFFI;
+ }
+ let declareLazyFFI = SharedAll.declareLazyFFI;
+
+ // Initialize types that require additional OS-specific
+ // support - either finalization or matching against
+ // OS-specific constants.
+ let Type = Object.create(SysAll.Type);
+ let SysFile = exports.OS.Win.File = { Type: Type };
+
+ // Initialize types
+
+ /**
+ * A C integer holding INVALID_HANDLE_VALUE in case of error or
+ * a file descriptor in case of success.
+ */
+ Type.HANDLE =
+ Type.voidptr_t.withName("HANDLE");
+ Type.HANDLE.importFromC = function importFromC(maybe) {
+ if (Type.int.cast(maybe).value == INVALID_HANDLE) {
+ // Ensure that API clients can effectively compare against
+ // Const.INVALID_HANDLE_VALUE. Without this cast,
+ // == would always return |false|.
+ return INVALID_HANDLE;
+ }
+ return ctypes.CDataFinalizer(maybe, this.finalizeHANDLE);
+ };
+ Type.HANDLE.finalizeHANDLE = function placeholder() {
+ throw new Error("finalizeHANDLE should be implemented");
+ };
+ let INVALID_HANDLE = Const.INVALID_HANDLE_VALUE;
+
+ Type.file_HANDLE = Type.HANDLE.withName("file HANDLE");
+ SharedAll.defineLazyGetter(Type.file_HANDLE,
+ "finalizeHANDLE",
+ function() {
+ return SysFile._CloseHandle;
+ });
+
+ Type.find_HANDLE = Type.HANDLE.withName("find HANDLE");
+ SharedAll.defineLazyGetter(Type.find_HANDLE,
+ "finalizeHANDLE",
+ function() {
+ return SysFile._FindClose;
+ });
+
+ Type.DWORD = Type.uint32_t.withName("DWORD");
+
+ /* A special type used to represent flags passed as DWORDs to a function.
+ * In JavaScript, bitwise manipulation of numbers, such as or-ing flags,
+ * can produce negative numbers. Since DWORD is unsigned, these negative
+ * numbers simply cannot be converted to DWORD. For this reason, whenever
+ * bit manipulation is called for, you should rather use DWORD_FLAGS,
+ * which is represented as a signed integer, hence has the correct
+ * semantics.
+ */
+ Type.DWORD_FLAGS = Type.int32_t.withName("DWORD_FLAGS");
+
+ /**
+ * A C integer holding 0 in case of error or a positive integer
+ * in case of success.
+ */
+ Type.zero_or_DWORD =
+ Type.DWORD.withName("zero_or_DWORD");
+
+ /**
+ * A C integer holding 0 in case of error, any other value in
+ * case of success.
+ */
+ Type.zero_or_nothing =
+ Type.int.withName("zero_or_nothing");
+
+ /**
+ * A C integer holding flags related to NTFS security.
+ */
+ Type.SECURITY_ATTRIBUTES =
+ Type.void_t.withName("SECURITY_ATTRIBUTES");
+
+ /**
+ * A C integer holding pointers related to NTFS security.
+ */
+ Type.PSID =
+ Type.voidptr_t.withName("PSID");
+
+ Type.PACL =
+ Type.voidptr_t.withName("PACL");
+
+ Type.PSECURITY_DESCRIPTOR =
+ Type.voidptr_t.withName("PSECURITY_DESCRIPTOR");
+
+ /**
+ * A C integer holding Win32 local memory handle.
+ */
+ Type.HLOCAL =
+ Type.voidptr_t.withName("HLOCAL");
+
+ Type.FILETIME =
+ new SharedAll.Type("FILETIME",
+ ctypes.StructType("FILETIME", [
+ { lo: Type.DWORD.implementation },
+ { hi: Type.DWORD.implementation }]));
+
+ Type.FindData =
+ new SharedAll.Type("FIND_DATA",
+ ctypes.StructType("FIND_DATA", [
+ { dwFileAttributes: ctypes.uint32_t },
+ { ftCreationTime: Type.FILETIME.implementation },
+ { ftLastAccessTime: Type.FILETIME.implementation },
+ { ftLastWriteTime: Type.FILETIME.implementation },
+ { nFileSizeHigh: Type.DWORD.implementation },
+ { nFileSizeLow: Type.DWORD.implementation },
+ { dwReserved0: Type.DWORD.implementation },
+ { dwReserved1: Type.DWORD.implementation },
+ { cFileName: ctypes.ArrayType(ctypes.char16_t, Const.MAX_PATH) },
+ { cAlternateFileName: ctypes.ArrayType(ctypes.char16_t, 14) }
+ ]));
+
+ Type.FILE_INFORMATION =
+ new SharedAll.Type("FILE_INFORMATION",
+ ctypes.StructType("FILE_INFORMATION", [
+ { dwFileAttributes: ctypes.uint32_t },
+ { ftCreationTime: Type.FILETIME.implementation },
+ { ftLastAccessTime: Type.FILETIME.implementation },
+ { ftLastWriteTime: Type.FILETIME.implementation },
+ { dwVolumeSerialNumber: ctypes.uint32_t },
+ { nFileSizeHigh: Type.DWORD.implementation },
+ { nFileSizeLow: Type.DWORD.implementation },
+ { nNumberOfLinks: ctypes.uint32_t },
+ { nFileIndex: ctypes.uint64_t }
+ ]));
+
+ Type.SystemTime =
+ new SharedAll.Type("SystemTime",
+ ctypes.StructType("SystemTime", [
+ { wYear: ctypes.int16_t },
+ { wMonth: ctypes.int16_t },
+ { wDayOfWeek: ctypes.int16_t },
+ { wDay: ctypes.int16_t },
+ { wHour: ctypes.int16_t },
+ { wMinute: ctypes.int16_t },
+ { wSecond: ctypes.int16_t },
+ { wMilliSeconds: ctypes.int16_t }
+ ]));
+
+ // Special case: these functions are used by the
+ // finalizer
+ libc.declareLazy(SysFile, "_CloseHandle",
+ "CloseHandle", ctypes.winapi_abi,
+ /*return */ctypes.bool,
+ /*handle*/ ctypes.voidptr_t);
+
+ SysFile.CloseHandle = function(fd) {
+ if (fd == INVALID_HANDLE) {
+ return true;
+ } else {
+ return fd.dispose(); // Returns the value of |CloseHandle|.
+ }
+ };
+
+ libc.declareLazy(SysFile, "_FindClose",
+ "FindClose", ctypes.winapi_abi,
+ /*return */ctypes.bool,
+ /*handle*/ ctypes.voidptr_t);
+
+ SysFile.FindClose = function(handle) {
+ if (handle == INVALID_HANDLE) {
+ return true;
+ } else {
+ return handle.dispose(); // Returns the value of |FindClose|.
+ }
+ };
+
+ // Declare libc functions as functions of |OS.Win.File|
+
+ libc.declareLazyFFI(SysFile, "CopyFile",
+ "CopyFileW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*sourcePath*/ Type.path,
+ /*destPath*/ Type.path,
+ /*bailIfExist*/Type.bool);
+
+ libc.declareLazyFFI(SysFile, "CreateDirectory",
+ "CreateDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*name*/ Type.char16_t.in_ptr,
+ /*security*/Type.SECURITY_ATTRIBUTES.in_ptr);
+
+ libc.declareLazyFFI(SysFile, "CreateFile",
+ "CreateFileW", ctypes.winapi_abi,
+ /*return*/ Type.file_HANDLE,
+ /*name*/ Type.path,
+ /*access*/ Type.DWORD_FLAGS,
+ /*share*/ Type.DWORD_FLAGS,
+ /*security*/Type.SECURITY_ATTRIBUTES.in_ptr,
+ /*creation*/Type.DWORD_FLAGS,
+ /*flags*/ Type.DWORD_FLAGS,
+ /*template*/Type.HANDLE);
+
+ libc.declareLazyFFI(SysFile, "DeleteFile",
+ "DeleteFileW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "FileTimeToSystemTime",
+ "FileTimeToSystemTime", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*filetime*/Type.FILETIME.in_ptr,
+ /*systime*/ Type.SystemTime.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "SystemTimeToFileTime",
+ "SystemTimeToFileTime", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*systime*/ Type.SystemTime.in_ptr,
+ /*filetime*/ Type.FILETIME.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "FindFirstFile",
+ "FindFirstFileW", ctypes.winapi_abi,
+ /*return*/ Type.find_HANDLE,
+ /*pattern*/Type.path,
+ /*data*/ Type.FindData.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "FindNextFile",
+ "FindNextFileW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*prev*/ Type.find_HANDLE,
+ /*data*/ Type.FindData.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "FormatMessage",
+ "FormatMessageW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*flags*/ Type.DWORD_FLAGS,
+ /*source*/ Type.void_t.in_ptr,
+ /*msgid*/ Type.DWORD_FLAGS,
+ /*langid*/ Type.DWORD_FLAGS,
+ /*buf*/ Type.out_wstring,
+ /*size*/ Type.DWORD,
+ /*Arguments*/Type.void_t.in_ptr
+ );
+
+ libc.declareLazyFFI(SysFile, "GetCurrentDirectory",
+ "GetCurrentDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_DWORD,
+ /*length*/ Type.DWORD,
+ /*buf*/ Type.out_path
+ );
+
+ libc.declareLazyFFI(SysFile, "GetFullPathName",
+ "GetFullPathNameW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_DWORD,
+ /*fileName*/ Type.path,
+ /*length*/ Type.DWORD,
+ /*buf*/ Type.out_path,
+ /*filePart*/ Type.DWORD
+ );
+
+ libc.declareLazyFFI(SysFile, "GetDiskFreeSpaceEx",
+ "GetDiskFreeSpaceExW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*directoryName*/ Type.path,
+ /*freeBytesForUser*/ Type.uint64_t.out_ptr,
+ /*totalBytesForUser*/ Type.uint64_t.out_ptr,
+ /*freeTotalBytesOnDrive*/ Type.uint64_t.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "GetFileInformationByHandle",
+ "GetFileInformationByHandle", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*handle*/ Type.HANDLE,
+ /*info*/ Type.FILE_INFORMATION.out_ptr);
+
+ libc.declareLazyFFI(SysFile, "MoveFileEx",
+ "MoveFileExW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*sourcePath*/ Type.path,
+ /*destPath*/ Type.path,
+ /*flags*/ Type.DWORD
+ );
+
+ libc.declareLazyFFI(SysFile, "ReadFile",
+ "ReadFile", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE,
+ /*buffer*/ Type.voidptr_t,
+ /*nbytes*/ Type.DWORD,
+ /*nbytes_read*/Type.DWORD.out_ptr,
+ /*overlapped*/Type.void_t.inout_ptr // FIXME: Implement?
+ );
+
+ libc.declareLazyFFI(SysFile, "RemoveDirectory",
+ "RemoveDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*path*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "SetCurrentDirectory",
+ "SetCurrentDirectoryW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*path*/ Type.path
+ );
+
+ libc.declareLazyFFI(SysFile, "SetEndOfFile",
+ "SetEndOfFile", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE);
+
+ libc.declareLazyFFI(SysFile, "SetFilePointer",
+ "SetFilePointer", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*file*/ Type.HANDLE,
+ /*distlow*/Type.long,
+ /*disthi*/ Type.long.in_ptr,
+ /*method*/ Type.DWORD);
+
+ libc.declareLazyFFI(SysFile, "SetFileTime",
+ "SetFileTime", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE,
+ /*creation*/ Type.FILETIME.in_ptr,
+ /*access*/ Type.FILETIME.in_ptr,
+ /*write*/ Type.FILETIME.in_ptr);
+
+
+ libc.declareLazyFFI(SysFile, "WriteFile",
+ "WriteFile", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE,
+ /*buffer*/ Type.voidptr_t,
+ /*nbytes*/ Type.DWORD,
+ /*nbytes_wr*/Type.DWORD.out_ptr,
+ /*overlapped*/Type.void_t.inout_ptr // FIXME: Implement?
+ );
+
+ libc.declareLazyFFI(SysFile, "FlushFileBuffers",
+ "FlushFileBuffers", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*file*/ Type.HANDLE);
+
+ libc.declareLazyFFI(SysFile, "GetFileAttributes",
+ "GetFileAttributesW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD_FLAGS,
+ /*fileName*/ Type.path);
+
+ libc.declareLazyFFI(SysFile, "SetFileAttributes",
+ "SetFileAttributesW", ctypes.winapi_abi,
+ /*return*/ Type.zero_or_nothing,
+ /*fileName*/ Type.path,
+ /*fileAttributes*/ Type.DWORD_FLAGS);
+
+ advapi32.declareLazyFFI(SysFile, "GetNamedSecurityInfo",
+ "GetNamedSecurityInfoW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*objectName*/ Type.path,
+ /*objectType*/ Type.DWORD,
+ /*securityInfo*/ Type.DWORD,
+ /*sidOwner*/ Type.PSID.out_ptr,
+ /*sidGroup*/ Type.PSID.out_ptr,
+ /*dacl*/ Type.PACL.out_ptr,
+ /*sacl*/ Type.PACL.out_ptr,
+ /*securityDesc*/ Type.PSECURITY_DESCRIPTOR.out_ptr);
+
+ advapi32.declareLazyFFI(SysFile, "SetNamedSecurityInfo",
+ "SetNamedSecurityInfoW", ctypes.winapi_abi,
+ /*return*/ Type.DWORD,
+ /*objectName*/ Type.path,
+ /*objectType*/ Type.DWORD,
+ /*securityInfo*/ Type.DWORD,
+ /*sidOwner*/ Type.PSID,
+ /*sidGroup*/ Type.PSID,
+ /*dacl*/ Type.PACL,
+ /*sacl*/ Type.PACL);
+
+ libc.declareLazyFFI(SysFile, "LocalFree",
+ "LocalFree", ctypes.winapi_abi,
+ /*return*/ Type.HLOCAL,
+ /*mem*/ Type.HLOCAL);
+ };
+
+ exports.OS.Win = {
+ File: {
+ _init: init
+ }
+ };
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/osfile_win_front.jsm b/toolkit/components/osfile/modules/osfile_win_front.jsm
new file mode 100644
index 000000000..387dd08b5
--- /dev/null
+++ b/toolkit/components/osfile/modules/osfile_win_front.jsm
@@ -0,0 +1,1266 @@
+/* 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/. */
+
+/**
+ * Synchronous front-end for the JavaScript OS.File library.
+ * Windows implementation.
+ *
+ * This front-end is meant to be imported by a worker thread.
+ */
+
+{
+ if (typeof Components != "undefined") {
+ // We do not wish osfile_win_front.jsm to be used directly as a main thread
+ // module yet.
+ throw new Error("osfile_win_front.jsm cannot be used from the main thread yet");
+ }
+
+ (function(exports) {
+ "use strict";
+
+
+ // exports.OS.Win is created by osfile_win_back.jsm
+ if (exports.OS && exports.OS.File) {
+ return; // Avoid double-initialization
+ }
+
+ let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+ let Path = require("resource://gre/modules/osfile/ospath.jsm");
+ let SysAll = require("resource://gre/modules/osfile/osfile_win_allthreads.jsm");
+ exports.OS.Win.File._init();
+ let Const = exports.OS.Constants.Win;
+ let WinFile = exports.OS.Win.File;
+ let Type = WinFile.Type;
+
+ // Mutable thread-global data
+ // In the Windows implementation, methods |read| and |write|
+ // require passing a pointer to an uint32 to determine how many
+ // bytes have been read/written. In C, this is a benigne operation,
+ // but in js-ctypes, this has a cost. Rather than re-allocating a
+ // C uint32 and a C uint32* for each |read|/|write|, we take advantage
+ // of the fact that the state is thread-private -- hence that two
+ // |read|/|write| operations cannot take place at the same time --
+ // and we use the following global mutable values:
+ let gBytesRead = new ctypes.uint32_t(0);
+ let gBytesReadPtr = gBytesRead.address();
+ let gBytesWritten = new ctypes.uint32_t(0);
+ let gBytesWrittenPtr = gBytesWritten.address();
+
+ // Same story for GetFileInformationByHandle
+ let gFileInfo = new Type.FILE_INFORMATION.implementation();
+ let gFileInfoPtr = gFileInfo.address();
+
+ /**
+ * Representation of a file.
+ *
+ * You generally do not need to call this constructor yourself. Rather,
+ * to open a file, use function |OS.File.open|.
+ *
+ * @param fd A OS-specific file descriptor.
+ * @param {string} path File path of the file handle, used for error-reporting.
+ * @constructor
+ */
+ let File = function File(fd, path) {
+ exports.OS.Shared.AbstractFile.call(this, fd, path);
+ this._closeResult = null;
+ };
+ File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype);
+
+ /**
+ * Close the file.
+ *
+ * This method has no effect if the file is already closed. However,
+ * if the first call to |close| has thrown an error, further calls
+ * will throw the same error.
+ *
+ * @throws File.Error If closing the file revealed an error that could
+ * not be reported earlier.
+ */
+ File.prototype.close = function close() {
+ if (this._fd) {
+ let fd = this._fd;
+ this._fd = null;
+ // Call |close(fd)|, detach finalizer if any
+ // (|fd| may not be a CDataFinalizer if it has been
+ // instantiated from a controller thread).
+ let result = WinFile._CloseHandle(fd);
+ if (typeof fd == "object" && "forget" in fd) {
+ fd.forget();
+ }
+ if (result == -1) {
+ this._closeResult = new File.Error("close", ctypes.winLastError, this._path);
+ }
+ }
+ if (this._closeResult) {
+ throw this._closeResult;
+ }
+ return;
+ };
+
+ /**
+ * Read some bytes from a file.
+ *
+ * @param {C pointer} buffer A buffer for holding the data
+ * once it is read.
+ * @param {number} nbytes The number of bytes to read. It must not
+ * exceed the size of |buffer| in bytes but it may exceed the number
+ * of bytes unread in the file.
+ * @param {*=} options Additional options for reading. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively read. If zero,
+ * the end of the file has been reached.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._read = function _read(buffer, nbytes, options) {
+ // |gBytesReadPtr| is a pointer to |gBytesRead|.
+ throw_on_zero("read",
+ WinFile.ReadFile(this.fd, buffer, nbytes, gBytesReadPtr, null),
+ this._path
+ );
+ return gBytesRead.value;
+ };
+
+ /**
+ * Write some bytes to a file.
+ *
+ * @param {Typed array} buffer A buffer holding the data that must be
+ * written.
+ * @param {number} nbytes The number of bytes to write. It must not
+ * exceed the size of |buffer| in bytes.
+ * @param {*=} options Additional options for writing. Ignored in
+ * this implementation.
+ *
+ * @return {number} The number of bytes effectively written.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype._write = function _write(buffer, nbytes, options) {
+ if (this._appendMode) {
+ // Need to manually seek on Windows, as O_APPEND is not supported.
+ // This is, of course, a race, but there is no real way around this.
+ this.setPosition(0, File.POS_END);
+ }
+ // |gBytesWrittenPtr| is a pointer to |gBytesWritten|.
+ throw_on_zero("write",
+ WinFile.WriteFile(this.fd, buffer, nbytes, gBytesWrittenPtr, null),
+ this._path
+ );
+ return gBytesWritten.value;
+ };
+
+ /**
+ * Return the current position in the file.
+ */
+ File.prototype.getPosition = function getPosition(pos) {
+ return this.setPosition(0, File.POS_CURRENT);
+ };
+
+ /**
+ * Change the current position in the file.
+ *
+ * @param {number} pos The new position. Whether this position
+ * is considered from the current position, from the start of
+ * the file or from the end of the file is determined by
+ * argument |whence|. Note that |pos| may exceed the length of
+ * the file.
+ * @param {number=} whence The reference position. If omitted
+ * or |OS.File.POS_START|, |pos| is relative to the start of the
+ * file. If |OS.File.POS_CURRENT|, |pos| is relative to the
+ * current position in the file. If |OS.File.POS_END|, |pos| is
+ * relative to the end of the file.
+ *
+ * @return The new position in the file.
+ */
+ File.prototype.setPosition = function setPosition(pos, whence) {
+ if (whence === undefined) {
+ whence = Const.FILE_BEGIN;
+ }
+ let pos64 = ctypes.Int64(pos);
+ // Per MSDN, while |lDistanceToMove| (low) is declared as int32_t, when
+ // providing |lDistanceToMoveHigh| as well, it should countain the
+ // bottom 32 bits of the 64-bit integer. Hence the following |posLo|
+ // cast is OK.
+ let posLo = new ctypes.uint32_t(ctypes.Int64.lo(pos64));
+ posLo = ctypes.cast(posLo, ctypes.int32_t);
+ let posHi = new ctypes.int32_t(ctypes.Int64.hi(pos64));
+ let result = WinFile.SetFilePointer(
+ this.fd, posLo.value, posHi.address(), whence);
+ // INVALID_SET_FILE_POINTER might be still a valid result, as it
+ // represents the lower 32 bit of the int64 result. MSDN says to check
+ // both, INVALID_SET_FILE_POINTER and a non-zero winLastError.
+ if (result == Const.INVALID_SET_FILE_POINTER && ctypes.winLastError) {
+ throw new File.Error("setPosition", ctypes.winLastError, this._path);
+ }
+ pos64 = ctypes.Int64.join(posHi.value, result);
+ return Type.int64_t.project(pos64);
+ };
+
+ /**
+ * Fetch the information on the file.
+ *
+ * @return File.Info The information on |this| file.
+ */
+ File.prototype.stat = function stat() {
+ throw_on_zero("stat",
+ WinFile.GetFileInformationByHandle(this.fd, gFileInfoPtr),
+ this._path);
+ return new File.Info(gFileInfo, this._path);
+ };
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid parameters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype.setDates = function setDates(accessDate, modificationDate) {
+ accessDate = Date_to_FILETIME("File.prototype.setDates", accessDate, this._path);
+ modificationDate = Date_to_FILETIME("File.prototype.setDates",
+ modificationDate,
+ this._path);
+ throw_on_zero("setDates",
+ WinFile.SetFileTime(this.fd, null, accessDate.address(),
+ modificationDate.address()),
+ this._path);
+ };
+
+ /**
+ * Set the file's access permission bits.
+ */
+ File.prototype.setPermissions = function setPermissions(options = {}) {
+ if (!("winAttributes" in options)) {
+ return;
+ }
+ let oldAttributes = WinFile.GetFileAttributes(this._path);
+ if (oldAttributes == Const.INVALID_FILE_ATTRIBUTES) {
+ throw new File.Error("setPermissions", ctypes.winLastError, this._path);
+ }
+ let newAttributes = toFileAttributes(options.winAttributes, oldAttributes);
+ throw_on_zero("setPermissions",
+ WinFile.SetFileAttributes(this._path, newAttributes),
+ this._path);
+ };
+
+ /**
+ * Flushes the file's buffers and causes all buffered data
+ * to be written.
+ * Disk flushes are very expensive and therefore should be used carefully,
+ * sparingly and only in scenarios where it is vital that data survives
+ * system crashes. Even though the function will be executed off the
+ * main-thread, it might still affect the overall performance of any
+ * running application.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.prototype.flush = function flush() {
+ throw_on_zero("flush", WinFile.FlushFileBuffers(this.fd), this._path);
+ };
+
+ // The default sharing mode for opening files: files are not
+ // locked against being reopened for reading/writing or against
+ // being deleted by the same process or another process.
+ // This is consistent with the default Unix policy.
+ const DEFAULT_SHARE = Const.FILE_SHARE_READ |
+ Const.FILE_SHARE_WRITE | Const.FILE_SHARE_DELETE;
+
+ // The default flags for opening files.
+ const DEFAULT_FLAGS = Const.FILE_ATTRIBUTE_NORMAL;
+
+ /**
+ * Open a file
+ *
+ * @param {string} path The path to the file.
+ * @param {*=} mode The opening mode for the file, as
+ * an object that may contain the following fields:
+ *
+ * - {bool} truncate If |true|, the file will be opened
+ * for writing. If the file does not exist, it will be
+ * created. If the file exists, its contents will be
+ * erased. Cannot be specified with |create|.
+ * - {bool} create If |true|, the file will be opened
+ * for writing. If the file exists, this function fails.
+ * If the file does not exist, it will be created. Cannot
+ * be specified with |truncate| or |existing|.
+ * - {bool} existing. If the file does not exist, this function
+ * fails. Cannot be specified with |create|.
+ * - {bool} read If |true|, the file will be opened for
+ * reading. The file may also be opened for writing, depending
+ * on the other fields of |mode|.
+ * - {bool} write If |true|, the file will be opened for
+ * writing. The file may also be opened for reading, depending
+ * on the other fields of |mode|.
+ * - {bool} append If |true|, the file will be opened for appending,
+ * meaning the equivalent of |.setPosition(0, POS_END)| is executed
+ * before each write. The default is |true|, i.e. opening a file for
+ * appending. Specify |append: false| to open the file in regular mode.
+ *
+ * If neither |truncate|, |create| or |write| is specified, the file
+ * is opened for reading.
+ *
+ * Note that |false|, |null| or |undefined| flags are simply ignored.
+ *
+ * @param {*=} options Additional options for file opening. This
+ * implementation interprets the following fields:
+ *
+ * - {number} winShare If specified, a share mode, as per
+ * Windows function |CreateFile|. You can build it from
+ * constants |OS.Constants.Win.FILE_SHARE_*|. If unspecified,
+ * the file uses the default sharing policy: it can be opened
+ * for reading and/or writing and it can be removed by other
+ * processes and by the same process.
+ * - {number} winSecurity If specified, Windows security
+ * attributes, as per Windows function |CreateFile|. If unspecified,
+ * no security attributes.
+ * - {number} winAccess If specified, Windows access mode, as
+ * per Windows function |CreateFile|. This also requires option
+ * |winDisposition| and this replaces argument |mode|. If unspecified,
+ * uses the string |mode|.
+ * - {number} winDisposition If specified, Windows disposition mode,
+ * as per Windows function |CreateFile|. This also requires option
+ * |winAccess| and this replaces argument |mode|. If unspecified,
+ * uses the string |mode|.
+ *
+ * @return {File} A file object.
+ * @throws {OS.File.Error} If the file could not be opened.
+ */
+ File.open = function Win_open(path, mode = {}, options = {}) {
+ let share = options.winShare !== undefined ? options.winShare : DEFAULT_SHARE;
+ let security = options.winSecurity || null;
+ let flags = options.winFlags !== undefined ? options.winFlags : DEFAULT_FLAGS;
+ let template = options.winTemplate ? options.winTemplate._fd : null;
+ let access;
+ let disposition;
+
+ mode = OS.Shared.AbstractFile.normalizeOpenMode(mode);
+
+ // The following option isn't a generic implementation of access to paths
+ // of arbitrary lengths. It allows for the specific case of writing to an
+ // Alternate Data Stream on a file whose path length is already close to
+ // MAX_PATH. This implementation is safe with a full path as input, if
+ // the first part of the path comes from local configuration and the
+ // file without the ADS was successfully opened before, so we know the
+ // path is valid.
+ if (options.winAllowLengthBeyondMaxPathWithCaveats) {
+ // Use the \\?\ syntax to allow lengths beyond MAX_PATH. This limited
+ // implementation only supports a DOS local path or UNC path as input.
+ let isUNC = path.length >= 2 && (path[0] == "\\" || path[0] == "/") &&
+ (path[1] == "\\" || path[1] == "/");
+ let pathToUse = "\\\\?\\" + (isUNC ? "UNC\\" + path.slice(2) : path);
+ // Use GetFullPathName to normalize slashes into backslashes. This is
+ // required because CreateFile won't do this for the \\?\ syntax.
+ let buffer_size = 512;
+ let array = new (ctypes.ArrayType(ctypes.char16_t, buffer_size))();
+ let expected_size = throw_on_zero("open",
+ WinFile.GetFullPathName(pathToUse, buffer_size, array, 0)
+ );
+ if (expected_size > buffer_size) {
+ // We don't need to allow an arbitrary path length for now.
+ throw new File.Error("open", ctypes.winLastError, path);
+ }
+ path = array.readString();
+ }
+
+ if ("winAccess" in options && "winDisposition" in options) {
+ access = options.winAccess;
+ disposition = options.winDisposition;
+ } else if (("winAccess" in options && !("winDisposition" in options))
+ ||(!("winAccess" in options) && "winDisposition" in options)) {
+ throw new TypeError("OS.File.open requires either both options " +
+ "winAccess and winDisposition or neither");
+ } else {
+ if (mode.read) {
+ access |= Const.GENERIC_READ;
+ }
+ if (mode.write) {
+ access |= Const.GENERIC_WRITE;
+ }
+ // Finally, handle create/existing/trunc
+ if (mode.trunc) {
+ if (mode.existing) {
+ // It seems that Const.TRUNCATE_EXISTING is broken
+ // in presence of links (source, anyone?). We need
+ // to open normally, then perform truncation manually.
+ disposition = Const.OPEN_EXISTING;
+ } else {
+ disposition = Const.CREATE_ALWAYS;
+ }
+ } else if (mode.create) {
+ disposition = Const.CREATE_NEW;
+ } else if (mode.read && !mode.write) {
+ disposition = Const.OPEN_EXISTING;
+ } else if (mode.existing) {
+ disposition = Const.OPEN_EXISTING;
+ } else {
+ disposition = Const.OPEN_ALWAYS;
+ }
+ }
+
+ let file = error_or_file(WinFile.CreateFile(path,
+ access, share, security, disposition, flags, template), path);
+
+ file._appendMode = !!mode.append;
+
+ if (!(mode.trunc && mode.existing)) {
+ return file;
+ }
+ // Now, perform manual truncation
+ file.setPosition(0, File.POS_START);
+ throw_on_zero("open",
+ WinFile.SetEndOfFile(file.fd),
+ path);
+ return file;
+ };
+
+ /**
+ * Checks if a file or directory exists
+ *
+ * @param {string} path The path to the file.
+ *
+ * @return {bool} true if the file exists, false otherwise.
+ */
+ File.exists = function Win_exists(path) {
+ try {
+ let file = File.open(path, FILE_STAT_MODE, FILE_STAT_OPTIONS);
+ file.close();
+ return true;
+ } catch (x) {
+ return false;
+ }
+ };
+
+ /**
+ * Remove an existing file.
+ *
+ * @param {string} path The name of the file.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the file does
+ * not exist. |true| by default.
+ *
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.remove = function remove(path, options = {}) {
+ if (WinFile.DeleteFile(path)) {
+ return;
+ }
+
+ if (ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND ||
+ ctypes.winLastError == Const.ERROR_PATH_NOT_FOUND) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent)) {
+ return;
+ }
+ } else if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED) {
+ // Save winLastError before another ctypes call.
+ let lastError = ctypes.winLastError;
+ let attributes = WinFile.GetFileAttributes(path);
+ if (attributes != Const.INVALID_FILE_ATTRIBUTES) {
+ if (!(attributes & Const.FILE_ATTRIBUTE_READONLY)) {
+ throw new File.Error("remove", lastError, path);
+ }
+ let newAttributes = attributes & ~Const.FILE_ATTRIBUTE_READONLY;
+ if (WinFile.SetFileAttributes(path, newAttributes) &&
+ WinFile.DeleteFile(path)) {
+ return;
+ }
+ }
+ }
+
+ throw new File.Error("remove", ctypes.winLastError, path);
+ };
+
+ /**
+ * Remove an empty directory.
+ *
+ * @param {string} path The name of the directory to remove.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory
+ * does not exist. |true| by default
+ */
+ File.removeEmptyDir = function removeEmptyDir(path, options = {}) {
+ let result = WinFile.RemoveDirectory(path);
+ if (!result) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ throw new File.Error("removeEmptyDir", ctypes.winLastError, path);
+ }
+ };
+
+ /**
+ * Create a directory and, optionally, its parent directories.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options. This
+ * implementation interprets the following fields:
+ *
+ * - {C pointer} winSecurity If specified, security attributes
+ * as per winapi function |CreateDirectory|. If unspecified,
+ * use the default security descriptor, inherited from the
+ * parent directory.
+ * - {bool} ignoreExisting If |false|, throw an error if the directory
+ * already exists. |true| by default
+ * - {string} from If specified, the call to |makeDir| creates all the
+ * ancestors of |path| that are descendants of |from|. Note that |from|
+ * and its existing descendants must be user-writeable and that |path|
+ * must be a descendant of |from|.
+ * Example:
+ * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
+ * creates directories profileDir/foo, profileDir/foo/bar
+ */
+ File._makeDir = function makeDir(path, options = {}) {
+ let security = options.winSecurity || null;
+ let result = WinFile.CreateDirectory(path, security);
+
+ if (result) {
+ return;
+ }
+
+ if (("ignoreExisting" in options) && !options.ignoreExisting) {
+ throw new File.Error("makeDir", ctypes.winLastError, path);
+ }
+
+ if (ctypes.winLastError == Const.ERROR_ALREADY_EXISTS) {
+ return;
+ }
+
+ // If the user has no access, but it's a root directory, no error should be thrown
+ let splitPath = OS.Path.split(path);
+ // Removing last component if it's empty
+ // An empty last component is caused by trailing slashes in path
+ // This is always the case with root directories
+ if( splitPath.components[splitPath.components.length - 1].length === 0 ) {
+ splitPath.components.pop();
+ }
+ // One component consisting of a drive letter implies a directory root.
+ if (ctypes.winLastError == Const.ERROR_ACCESS_DENIED &&
+ splitPath.winDrive &&
+ splitPath.components.length === 1 ) {
+ return;
+ }
+
+ throw new File.Error("makeDir", ctypes.winLastError, path);
+ };
+
+ /**
+ * Copy a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be copied.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If true, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be copied with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.copy = function copy(sourcePath, destPath, options = {}) {
+ throw_on_zero("copy",
+ WinFile.CopyFile(sourcePath, destPath, options.noOverwrite || false),
+ sourcePath
+ );
+ };
+
+ /**
+ * Move a file to a destination.
+ *
+ * @param {string} sourcePath The platform-specific path at which
+ * the file may currently be found.
+ * @param {string} destPath The platform-specific path at which the
+ * file should be moved.
+ * @param {*=} options An object which may contain the following fields:
+ *
+ * @option {bool} noOverwrite - If set, this function will fail if
+ * a file already exists at |destPath|. Otherwise, if this file exists,
+ * it will be erased silently.
+ * @option {bool} noCopy - If set, this function will fail if the
+ * operation is more sophisticated than a simple renaming, i.e. if
+ * |sourcePath| and |destPath| are not situated on the same drive.
+ *
+ * @throws {OS.File.Error} In case of any error.
+ *
+ * General note: The behavior of this function is defined only when
+ * it is called on a single file. If it is called on a directory, the
+ * behavior is undefined and may not be the same across all platforms.
+ *
+ * General note: The behavior of this function with respect to metadata
+ * is unspecified. Metadata may or may not be moved with the file. The
+ * behavior may not be the same across all platforms.
+ */
+ File.move = function move(sourcePath, destPath, options = {}) {
+ let flags = 0;
+ if (!options.noCopy) {
+ flags = Const.MOVEFILE_COPY_ALLOWED;
+ }
+ if (!options.noOverwrite) {
+ flags = flags | Const.MOVEFILE_REPLACE_EXISTING;
+ }
+ throw_on_zero("move",
+ WinFile.MoveFileEx(sourcePath, destPath, flags),
+ sourcePath
+ );
+
+ // Inherit NTFS permissions from the destination directory
+ // if possible.
+ if (Path.dirname(sourcePath) === Path.dirname(destPath)) {
+ // Skip if the move operation was the simple rename,
+ return;
+ }
+ // The function may fail for various reasons (e.g. not all
+ // filesystems support NTFS permissions or the user may not
+ // have the enough rights to read/write permissions).
+ // However we can safely ignore errors. The file was already
+ // moved. Setting permissions is not mandatory.
+ let dacl = new ctypes.voidptr_t();
+ let sd = new ctypes.voidptr_t();
+ WinFile.GetNamedSecurityInfo(destPath, Const.SE_FILE_OBJECT,
+ Const.DACL_SECURITY_INFORMATION,
+ null /*sidOwner*/, null /*sidGroup*/,
+ dacl.address(), null /*sacl*/,
+ sd.address());
+ // dacl will be set only if the function succeeds.
+ if (!dacl.isNull()) {
+ WinFile.SetNamedSecurityInfo(destPath, Const.SE_FILE_OBJECT,
+ Const.DACL_SECURITY_INFORMATION |
+ Const.UNPROTECTED_DACL_SECURITY_INFORMATION,
+ null /*sidOwner*/, null /*sidGroup*/,
+ dacl, null /*sacl*/);
+ }
+ // sd will be set only if the function succeeds.
+ if (!sd.isNull()) {
+ WinFile.LocalFree(Type.HLOCAL.cast(sd));
+ }
+ };
+
+ /**
+ * Gets the number of bytes available on disk to the current user.
+ *
+ * @param {string} sourcePath Platform-specific path to a directory on
+ * the disk to query for free available bytes.
+ *
+ * @return {number} The number of bytes available for the current user.
+ * @throws {OS.File.Error} In case of any error.
+ */
+ File.getAvailableFreeSpace = function Win_getAvailableFreeSpace(sourcePath) {
+ let freeBytesAvailableToUser = new Type.uint64_t.implementation(0);
+ let freeBytesAvailableToUserPtr = freeBytesAvailableToUser.address();
+
+ throw_on_zero("getAvailableFreeSpace",
+ WinFile.GetDiskFreeSpaceEx(sourcePath, freeBytesAvailableToUserPtr, null, null)
+ );
+
+ return freeBytesAvailableToUser.value;
+ };
+
+ /**
+ * A global value used to receive data during time conversions.
+ */
+ let gSystemTime = new Type.SystemTime.implementation();
+ let gSystemTimePtr = gSystemTime.address();
+
+ /**
+ * Utility function: convert a FILETIME to a JavaScript Date.
+ */
+ let FILETIME_to_Date = function FILETIME_to_Date(fileTime, path) {
+ if (fileTime == null) {
+ throw new TypeError("Expecting a non-null filetime");
+ }
+ throw_on_zero("FILETIME_to_Date",
+ WinFile.FileTimeToSystemTime(fileTime.address(),
+ gSystemTimePtr),
+ path);
+ // Windows counts hours, minutes, seconds from UTC,
+ // JS counts from local time, so we need to go through UTC.
+ let utc = Date.UTC(gSystemTime.wYear,
+ gSystemTime.wMonth - 1
+ /*Windows counts months from 1, JS from 0*/,
+ gSystemTime.wDay, gSystemTime.wHour,
+ gSystemTime.wMinute, gSystemTime.wSecond,
+ gSystemTime.wMilliSeconds);
+ return new Date(utc);
+ };
+
+ /**
+ * Utility function: convert Javascript Date to FileTime.
+ *
+ * @param {string} fn Name of the calling function.
+ * @param {Date,number} date The date to be converted. If omitted or null,
+ * then the current date will be used. If numeric, assumed to be the date
+ * in milliseconds since epoch.
+ */
+ let Date_to_FILETIME = function Date_to_FILETIME(fn, date, path) {
+ if (typeof date === "number") {
+ date = new Date(date);
+ } else if (!date) {
+ date = new Date();
+ } else if (typeof date.getUTCFullYear !== "function") {
+ throw new TypeError("|date| parameter of " + fn + " must be a " +
+ "|Date| instance or number");
+ }
+ gSystemTime.wYear = date.getUTCFullYear();
+ // Windows counts months from 1, JS from 0.
+ gSystemTime.wMonth = date.getUTCMonth() + 1;
+ gSystemTime.wDay = date.getUTCDate();
+ gSystemTime.wHour = date.getUTCHours();
+ gSystemTime.wMinute = date.getUTCMinutes();
+ gSystemTime.wSecond = date.getUTCSeconds();
+ gSystemTime.wMilliseconds = date.getUTCMilliseconds();
+ let result = new OS.Shared.Type.FILETIME.implementation();
+ throw_on_zero("Date_to_FILETIME",
+ WinFile.SystemTimeToFileTime(gSystemTimePtr,
+ result.address()),
+ path);
+ return result;
+ };
+
+ /**
+ * Iterate on one directory.
+ *
+ * This iterator will not enter subdirectories.
+ *
+ * @param {string} path The directory upon which to iterate.
+ * @param {*=} options An object that may contain the following field:
+ * @option {string} winPattern Windows file name pattern; if set,
+ * only files matching this pattern are returned.
+ *
+ * @throws {File.Error} If |path| does not represent a directory or
+ * if the directory cannot be iterated.
+ * @constructor
+ */
+ File.DirectoryIterator = function DirectoryIterator(path, options) {
+ exports.OS.Shared.AbstractFile.AbstractIterator.call(this);
+ if (options && options.winPattern) {
+ this._pattern = path + "\\" + options.winPattern;
+ } else {
+ this._pattern = path + "\\*";
+ }
+ this._path = path;
+
+ // Pre-open the first item.
+ this._first = true;
+ this._findData = new Type.FindData.implementation();
+ this._findDataPtr = this._findData.address();
+ this._handle = WinFile.FindFirstFile(this._pattern, this._findDataPtr);
+ if (this._handle == Const.INVALID_HANDLE_VALUE) {
+ let error = ctypes.winLastError;
+ this._findData = null;
+ this._findDataPtr = null;
+ if (error == Const.ERROR_FILE_NOT_FOUND) {
+ // Directory is empty, let's behave as if it were closed
+ SharedAll.LOG("Directory is empty");
+ this._closed = true;
+ this._exists = true;
+ } else if (error == Const.ERROR_PATH_NOT_FOUND) {
+ // Directory does not exist, let's throw if we attempt to walk it
+ SharedAll.LOG("Directory does not exist");
+ this._closed = true;
+ this._exists = false;
+ } else {
+ throw new File.Error("DirectoryIterator", error, this._path);
+ }
+ } else {
+ this._closed = false;
+ this._exists = true;
+ }
+ };
+
+ File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype);
+
+
+ /**
+ * Fetch the next entry in the directory.
+ *
+ * @return null If we have reached the end of the directory.
+ */
+ File.DirectoryIterator.prototype._next = function _next() {
+ // Bailout if the directory does not exist
+ if (!this._exists) {
+ throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path);
+ }
+ // Bailout if the iterator is closed.
+ if (this._closed) {
+ return null;
+ }
+ // If this is the first entry, we have obtained it already
+ // during construction.
+ if (this._first) {
+ this._first = false;
+ return this._findData;
+ }
+
+ if (WinFile.FindNextFile(this._handle, this._findDataPtr)) {
+ return this._findData;
+ } else {
+ let error = ctypes.winLastError;
+ this.close();
+ if (error == Const.ERROR_NO_MORE_FILES) {
+ return null;
+ } else {
+ throw new File.Error("iter (FindNextFile)", error, this._path);
+ }
+ }
+ },
+
+ /**
+ * Return the next entry in the directory, if any such entry is
+ * available.
+ *
+ * Skip special directories "." and "..".
+ *
+ * @return {File.Entry} The next entry in the directory.
+ * @throws {StopIteration} Once all files in the directory have been
+ * encountered.
+ */
+ File.DirectoryIterator.prototype.next = function next() {
+ // FIXME: If we start supporting "\\?\"-prefixed paths, do not forget
+ // that "." and ".." are absolutely normal file names if _path starts
+ // with such prefix
+ for (let entry = this._next(); entry != null; entry = this._next()) {
+ let name = entry.cFileName.readString();
+ if (name == "." || name == "..") {
+ continue;
+ }
+ return new File.DirectoryIterator.Entry(entry, this._path);
+ }
+ throw StopIteration;
+ };
+
+ File.DirectoryIterator.prototype.close = function close() {
+ if (this._closed) {
+ return;
+ }
+ this._closed = true;
+ if (this._handle) {
+ // We might not have a handle if the iterator is closed
+ // before being used.
+ throw_on_zero("FindClose",
+ WinFile.FindClose(this._handle),
+ this._path);
+ this._handle = null;
+ }
+ };
+
+ /**
+ * Determine whether the directory exists.
+ *
+ * @return {boolean}
+ */
+ File.DirectoryIterator.prototype.exists = function exists() {
+ return this._exists;
+ };
+
+ File.DirectoryIterator.Entry = function Entry(win_entry, parent) {
+ if (!win_entry.dwFileAttributes || !win_entry.ftCreationTime ||
+ !win_entry.ftLastAccessTime || !win_entry.ftLastWriteTime)
+ throw new TypeError();
+
+ // Copy the relevant part of |win_entry| to ensure that
+ // our data is not overwritten prematurely.
+ let isDir = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY);
+ let isSymLink = !!(win_entry.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT);
+
+ let winCreationDate = FILETIME_to_Date(win_entry.ftCreationTime, this._path);
+ let winLastWriteDate = FILETIME_to_Date(win_entry.ftLastWriteTime, this._path);
+ let winLastAccessDate = FILETIME_to_Date(win_entry.ftLastAccessTime, this._path);
+
+ let name = win_entry.cFileName.readString();
+ if (!name) {
+ throw new TypeError("Empty name");
+ }
+
+ if (!parent) {
+ throw new TypeError("Empty parent");
+ }
+ this._parent = parent;
+
+ let path = Path.join(this._parent, name);
+
+ SysAll.AbstractEntry.call(this, isDir, isSymLink, name,
+ winCreationDate, winLastWriteDate,
+ winLastAccessDate, path);
+ };
+ File.DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype);
+
+ /**
+ * Return a version of an instance of
+ * File.DirectoryIterator.Entry that can be sent from a worker
+ * thread to the main thread. Note that deserialization is
+ * asymmetric and returns an object with a different
+ * implementation.
+ */
+ File.DirectoryIterator.Entry.toMsg = function toMsg(value) {
+ if (!value instanceof File.DirectoryIterator.Entry) {
+ throw new TypeError("parameter of " +
+ "File.DirectoryIterator.Entry.toMsg must be a " +
+ "File.DirectoryIterator.Entry");
+ }
+ let serialized = {};
+ for (let key in File.DirectoryIterator.Entry.prototype) {
+ serialized[key] = value[key];
+ }
+ return serialized;
+ };
+
+
+ /**
+ * Information on a file.
+ *
+ * To obtain the latest information on a file, use |File.stat|
+ * (for an unopened file) or |File.prototype.stat| (for an
+ * already opened file).
+ *
+ * @constructor
+ */
+ File.Info = function Info(stat, path) {
+ let isDir = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_DIRECTORY);
+ let isSymLink = !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_REPARSE_POINT);
+
+ let winBirthDate = FILETIME_to_Date(stat.ftCreationTime, this._path);
+ let lastAccessDate = FILETIME_to_Date(stat.ftLastAccessTime, this._path);
+ let lastWriteDate = FILETIME_to_Date(stat.ftLastWriteTime, this._path);
+
+ let value = ctypes.UInt64.join(stat.nFileSizeHigh, stat.nFileSizeLow);
+ let size = Type.uint64_t.importFromC(value);
+ let winAttributes = {
+ readOnly: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_READONLY),
+ system: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_SYSTEM),
+ hidden: !!(stat.dwFileAttributes & Const.FILE_ATTRIBUTE_HIDDEN),
+ };
+
+ SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size,
+ winBirthDate, lastAccessDate, lastWriteDate, winAttributes);
+ };
+ File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype);
+
+ /**
+ * Return a version of an instance of File.Info that can be sent
+ * from a worker thread to the main thread. Note that deserialization
+ * is asymmetric and returns an object with a different implementation.
+ */
+ File.Info.toMsg = function toMsg(stat) {
+ if (!stat instanceof File.Info) {
+ throw new TypeError("parameter of File.Info.toMsg must be a File.Info");
+ }
+ let serialized = {};
+ for (let key in File.Info.prototype) {
+ serialized[key] = stat[key];
+ }
+ return serialized;
+ };
+
+
+ /**
+ * Fetch the information on a file.
+ *
+ * Performance note: if you have opened the file already,
+ * method |File.prototype.stat| is generally much faster
+ * than method |File.stat|.
+ *
+ * Platform-specific note: under Windows, if the file is
+ * already opened without sharing of the read capability,
+ * this function will fail.
+ *
+ * @return {File.Information}
+ */
+ File.stat = function stat(path) {
+ let file = File.open(path, FILE_STAT_MODE, FILE_STAT_OPTIONS);
+ try {
+ return file.stat();
+ } finally {
+ file.close();
+ }
+ };
+ // All of the following is required to ensure that File.stat
+ // also works on directories.
+ const FILE_STAT_MODE = {
+ read: true
+ };
+ const FILE_STAT_OPTIONS = {
+ // Directories can be opened neither for reading(!) nor for writing
+ winAccess: 0,
+ // Directories can only be opened with backup semantics(!)
+ winFlags: Const.FILE_FLAG_BACKUP_SEMANTICS,
+ winDisposition: Const.OPEN_EXISTING
+ };
+
+ /**
+ * Set the file's access permission bits.
+ */
+ File.setPermissions = function setPermissions(path, options = {}) {
+ if (!("winAttributes" in options)) {
+ return;
+ }
+ let oldAttributes = WinFile.GetFileAttributes(path);
+ if (oldAttributes == Const.INVALID_FILE_ATTRIBUTES) {
+ throw new File.Error("setPermissions", ctypes.winLastError, path);
+ }
+ let newAttributes = toFileAttributes(options.winAttributes, oldAttributes);
+ throw_on_zero("setPermissions",
+ WinFile.SetFileAttributes(path, newAttributes),
+ path);
+ };
+
+ /**
+ * Set the last access and modification date of the file.
+ * The time stamp resolution is 1 second at best, but might be worse
+ * depending on the platform.
+ *
+ * Performance note: if you have opened the file already in write mode,
+ * method |File.prototype.stat| is generally much faster
+ * than method |File.stat|.
+ *
+ * Platform-specific note: under Windows, if the file is
+ * already opened without sharing of the write capability,
+ * this function will fail.
+ *
+ * @param {string} path The full name of the file to set the dates for.
+ * @param {Date,number=} accessDate The last access date. If numeric,
+ * milliseconds since epoch. If omitted or null, then the current date
+ * will be used.
+ * @param {Date,number=} modificationDate The last modification date. If
+ * numeric, milliseconds since epoch. If omitted or null, then the current
+ * date will be used.
+ *
+ * @throws {TypeError} In case of invalid paramters.
+ * @throws {OS.File.Error} In case of I/O error.
+ */
+ File.setDates = function setDates(path, accessDate, modificationDate) {
+ let file = File.open(path, FILE_SETDATES_MODE, FILE_SETDATES_OPTIONS);
+ try {
+ return file.setDates(accessDate, modificationDate);
+ } finally {
+ file.close();
+ }
+ };
+ // All of the following is required to ensure that File.setDates
+ // also works on directories.
+ const FILE_SETDATES_MODE = {
+ write: true
+ };
+ const FILE_SETDATES_OPTIONS = {
+ winAccess: Const.GENERIC_WRITE,
+ // Directories can only be opened with backup semantics(!)
+ winFlags: Const.FILE_FLAG_BACKUP_SEMANTICS,
+ winDisposition: Const.OPEN_EXISTING
+ };
+
+ File.read = exports.OS.Shared.AbstractFile.read;
+ File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
+ File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
+ File.makeDir = exports.OS.Shared.AbstractFile.makeDir;
+
+ /**
+ * Remove an existing directory and its contents.
+ *
+ * @param {string} path The name of the directory.
+ * @param {*=} options Additional options.
+ * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't
+ * exist. |true| by default.
+ * - {boolean} ignorePermissions If |true|, remove the file even when lacking write
+ * permission.
+ *
+ * @throws {OS.File.Error} In case of I/O error, in particular if |path| is
+ * not a directory.
+ */
+ File.removeDir = function(path, options = {}) {
+ // We can't use File.stat here because it will follow the symlink.
+ let attributes = WinFile.GetFileAttributes(path);
+ if (attributes == Const.INVALID_FILE_ATTRIBUTES) {
+ if ((!("ignoreAbsent" in options) || options.ignoreAbsent) &&
+ ctypes.winLastError == Const.ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ throw new File.Error("removeEmptyDir", ctypes.winLastError, path);
+ }
+ if (attributes & Const.FILE_ATTRIBUTE_REPARSE_POINT) {
+ // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
+ // directories are directories themselves. OS.File.remove()
+ // will not work for them.
+ OS.File.removeEmptyDir(path, options);
+ return;
+ }
+ exports.OS.Shared.AbstractFile.removeRecursive(path, options);
+ };
+
+ /**
+ * Get the current directory by getCurrentDirectory.
+ */
+ File.getCurrentDirectory = function getCurrentDirectory() {
+ // This function is more complicated than one could hope.
+ //
+ // This is due to two facts:
+ // - the maximal length of a path under Windows is not completely
+ // specified (there is a constant MAX_PATH, but it is quite possible
+ // to create paths that are much larger, see bug 744413);
+ // - if we attempt to call |GetCurrentDirectory| with a buffer that
+ // is too short, it returns the length of the current directory, but
+ // this length might be insufficient by the time we can call again
+ // the function with a larger buffer, in the (unlikely but possible)
+ // case in which the process changes directory to a directory with
+ // a longer name between both calls.
+ //
+ let buffer_size = 4096;
+ while (true) {
+ let array = new (ctypes.ArrayType(ctypes.char16_t, buffer_size))();
+ let expected_size = throw_on_zero("getCurrentDirectory",
+ WinFile.GetCurrentDirectory(buffer_size, array)
+ );
+ if (expected_size <= buffer_size) {
+ return array.readString();
+ }
+ // At this point, we are in a case in which our buffer was not
+ // large enough to hold the name of the current directory.
+ // Consequently, we need to increase the size of the buffer.
+ // Note that, even in crazy scenarios, the loop will eventually
+ // converge, as the length of the paths cannot increase infinitely.
+ buffer_size = expected_size + 1 /* to store \0 */;
+ }
+ };
+
+ /**
+ * Set the current directory by setCurrentDirectory.
+ */
+ File.setCurrentDirectory = function setCurrentDirectory(path) {
+ throw_on_zero("setCurrentDirectory",
+ WinFile.SetCurrentDirectory(path),
+ path);
+ };
+
+ /**
+ * Get/set the current directory by |curDir|.
+ */
+ Object.defineProperty(File, "curDir", {
+ set: function(path) {
+ this.setCurrentDirectory(path);
+ },
+ get: function() {
+ return this.getCurrentDirectory();
+ }
+ }
+ );
+
+ // Utility functions, used for error-handling
+
+ /**
+ * Turn the result of |open| into an Error or a File
+ * @param {number} maybe The result of the |open| operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns the opened file.
+ * @param {string=} path The path of the file.
+ */
+ function error_or_file(maybe, path) {
+ if (maybe == Const.INVALID_HANDLE_VALUE) {
+ throw new File.Error("open", ctypes.winLastError, path);
+ }
+ return new File(maybe, path);
+ }
+
+ /**
+ * Utility function to sort errors represented as "0" from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {number} result The result of the operation that may
+ * represent either an error or a success. If 0, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_zero(operation, result, path) {
+ if (result == 0) {
+ throw new File.Error(operation, ctypes.winLastError, path);
+ }
+ return result;
+ }
+
+ /**
+ * Utility function to sort errors represented as "-1" from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {number} result The result of the operation that may
+ * represent either an error or a success. If -1, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_negative(operation, result, path) {
+ if (result < 0) {
+ throw new File.Error(operation, ctypes.winLastError, path);
+ }
+ return result;
+ }
+
+ /**
+ * Utility function to sort errors represented as |null| from successes.
+ *
+ * @param {string=} operation The name of the operation. If unspecified,
+ * the name of the caller function.
+ * @param {pointer} result The result of the operation that may
+ * represent either an error or a success. If |null|, this function raises
+ * an error holding ctypes.winLastError, otherwise it returns |result|.
+ * @param {string=} path The path of the file.
+ */
+ function throw_on_null(operation, result, path) {
+ if (result == null || (result.isNull && result.isNull())) {
+ throw new File.Error(operation, ctypes.winLastError, path);
+ }
+ return result;
+ }
+
+ /**
+ * Helper used by both versions of setPermissions
+ */
+ function toFileAttributes(winAttributes, oldDwAttrs) {
+ if ("readOnly" in winAttributes) {
+ if (winAttributes.readOnly) {
+ oldDwAttrs |= Const.FILE_ATTRIBUTE_READONLY;
+ } else {
+ oldDwAttrs &= ~Const.FILE_ATTRIBUTE_READONLY;
+ }
+ }
+ if ("system" in winAttributes) {
+ if (winAttributes.system) {
+ oldDwAttrs |= Const.FILE_ATTRIBUTE_SYSTEM;
+ } else {
+ oldDwAttrs &= ~Const.FILE_ATTRIBUTE_SYSTEM;
+ }
+ }
+ if ("hidden" in winAttributes) {
+ if (winAttributes.hidden) {
+ oldDwAttrs |= Const.FILE_ATTRIBUTE_HIDDEN;
+ } else {
+ oldDwAttrs &= ~Const.FILE_ATTRIBUTE_HIDDEN;
+ }
+ }
+ return oldDwAttrs;
+ }
+
+ File.Win = exports.OS.Win.File;
+ File.Error = SysAll.Error;
+ exports.OS.File = File;
+ exports.OS.Shared.Type = Type;
+
+ Object.defineProperty(File, "POS_START", { value: SysAll.POS_START });
+ Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT });
+ Object.defineProperty(File, "POS_END", { value: SysAll.POS_END });
+ })(this);
+}
diff --git a/toolkit/components/osfile/modules/ospath.jsm b/toolkit/components/osfile/modules/ospath.jsm
new file mode 100644
index 000000000..68bbe4345
--- /dev/null
+++ b/toolkit/components/osfile/modules/ospath.jsm
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/**
+ * Handling native paths.
+ *
+ * This module contains a number of functions destined to simplify
+ * working with native paths through a cross-platform API. Functions
+ * of this module will only work with the following assumptions:
+ *
+ * - paths are valid;
+ * - paths are defined with one of the grammars that this module can
+ * parse (see later);
+ * - all path concatenations go through function |join|.
+ */
+
+"use strict";
+
+if (typeof Components == "undefined") {
+ let Path;
+ if (OS.Constants.Win) {
+ Path = require("resource://gre/modules/osfile/ospath_win.jsm");
+ } else {
+ Path = require("resource://gre/modules/osfile/ospath_unix.jsm");
+ }
+ module.exports = Path;
+} else {
+ let Cu = Components.utils;
+ let Scope = {};
+ Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", Scope);
+
+ let Path = {};
+ if (Scope.OS.Constants.Win) {
+ Cu.import("resource://gre/modules/osfile/ospath_win.jsm", Path);
+ } else {
+ Cu.import("resource://gre/modules/osfile/ospath_unix.jsm", Path);
+ }
+
+ this.EXPORTED_SYMBOLS = [];
+ for (let k in Path) {
+ this.EXPORTED_SYMBOLS.push(k);
+ this[k] = Path[k];
+ }
+}
diff --git a/toolkit/components/osfile/modules/ospath_unix.jsm b/toolkit/components/osfile/modules/ospath_unix.jsm
new file mode 100644
index 000000000..1d574baed
--- /dev/null
+++ b/toolkit/components/osfile/modules/ospath_unix.jsm
@@ -0,0 +1,202 @@
+/* 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/. */
+
+/**
+ * Handling native paths.
+ *
+ * This module contains a number of functions destined to simplify
+ * working with native paths through a cross-platform API. Functions
+ * of this module will only work with the following assumptions:
+ *
+ * - paths are valid;
+ * - paths are defined with one of the grammars that this module can
+ * parse (see later);
+ * - all path concatenations go through function |join|.
+ */
+
+"use strict";
+
+// Boilerplate used to be able to import this module both from the main
+// thread and from worker threads.
+if (typeof Components != "undefined") {
+ Components.utils.importGlobalProperties(["URL"]);
+ // Global definition of |exports|, to keep everybody happy.
+ // In non-main thread, |exports| is provided by the module
+ // loader.
+ this.exports = {};
+} else if (typeof module == "undefined" || typeof exports == "undefined") {
+ throw new Error("Please load this module using require()");
+}
+
+var EXPORTED_SYMBOLS = [
+ "basename",
+ "dirname",
+ "join",
+ "normalize",
+ "split",
+ "toFileURI",
+ "fromFileURI",
+];
+
+/**
+ * Return the final part of the path.
+ * The final part of the path is everything after the last "/".
+ */
+var basename = function(path) {
+ return path.slice(path.lastIndexOf("/") + 1);
+};
+exports.basename = basename;
+
+/**
+ * Return the directory part of the path.
+ * The directory part of the path is everything before the last
+ * "/". If the last few characters of this part are also "/",
+ * they are ignored.
+ *
+ * If the path contains no directory, return ".".
+ */
+var dirname = function(path) {
+ let index = path.lastIndexOf("/");
+ if (index == -1) {
+ return ".";
+ }
+ while (index >= 0 && path[index] == "/") {
+ --index;
+ }
+ return path.slice(0, index + 1);
+};
+exports.dirname = dirname;
+
+/**
+ * Join path components.
+ * This is the recommended manner of getting the path of a file/subdirectory
+ * in a directory.
+ *
+ * Example: Obtaining $TMP/foo/bar in an OS-independent manner
+ * var tmpDir = OS.Constants.Path.tmpDir;
+ * var path = OS.Path.join(tmpDir, "foo", "bar");
+ *
+ * Under Unix, this will return "/tmp/foo/bar".
+ *
+ * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the
+ * same as `OS.Path.join("foo", "bar")`.
+ */
+var join = function(...path) {
+ // If there is a path that starts with a "/", eliminate everything before
+ let paths = [];
+ for (let subpath of path) {
+ if (subpath == null) {
+ throw new TypeError("invalid path component");
+ }
+ if (subpath.length == 0) {
+ continue;
+ } else if (subpath[0] == "/") {
+ paths = [subpath];
+ } else {
+ paths.push(subpath);
+ }
+ }
+ return paths.join("/");
+};
+exports.join = join;
+
+/**
+ * Normalize a path by removing any unneeded ".", "..", "//".
+ */
+var normalize = function(path) {
+ let stack = [];
+ let absolute;
+ if (path.length >= 0 && path[0] == "/") {
+ absolute = true;
+ } else {
+ absolute = false;
+ }
+ path.split("/").forEach(function(v) {
+ switch (v) {
+ case "": case ".":// fallthrough
+ break;
+ case "..":
+ if (stack.length == 0) {
+ if (absolute) {
+ throw new Error("Path is ill-formed: attempting to go past root");
+ } else {
+ stack.push("..");
+ }
+ } else {
+ if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ }
+ break;
+ default:
+ stack.push(v);
+ }
+ });
+ let string = stack.join("/");
+ return absolute ? "/" + string : string;
+};
+exports.normalize = normalize;
+
+/**
+ * Return the components of a path.
+ * You should generally apply this function to a normalized path.
+ *
+ * @return {{
+ * {bool} absolute |true| if the path is absolute, |false| otherwise
+ * {array} components the string components of the path
+ * }}
+ *
+ * Other implementations may add additional OS-specific informations.
+ */
+var split = function(path) {
+ return {
+ absolute: path.length && path[0] == "/",
+ components: path.split("/")
+ };
+};
+exports.split = split;
+
+/**
+ * Returns the file:// URI file path of the given local file path.
+ */
+// The case of %3b is designed to match Services.io, but fundamentally doesn't matter.
+var toFileURIExtraEncodings = {';': '%3b', '?': '%3F', '#': '%23'};
+var toFileURI = function toFileURI(path) {
+ // Per https://url.spec.whatwg.org we should not encode [] in the path
+ let dontNeedEscaping = {'%5B': '[', '%5D': ']'};
+ let uri = encodeURI(this.normalize(path)).replace(/%(5B|5D)/gi,
+ match => dontNeedEscaping[match]);
+
+ // add a prefix, and encodeURI doesn't escape a few characters that we do
+ // want to escape, so fix that up
+ let prefix = "file://";
+ uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]);
+
+ return uri;
+};
+exports.toFileURI = toFileURI;
+
+/**
+ * Returns the local file path from a given file URI.
+ */
+var fromFileURI = function fromFileURI(uri) {
+ let url = new URL(uri);
+ if (url.protocol != 'file:') {
+ throw new Error("fromFileURI expects a file URI");
+ }
+ let path = this.normalize(decodeURIComponent(url.pathname));
+ return path;
+};
+exports.fromFileURI = fromFileURI;
+
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/modules/ospath_win.jsm b/toolkit/components/osfile/modules/ospath_win.jsm
new file mode 100644
index 000000000..31a87b115
--- /dev/null
+++ b/toolkit/components/osfile/modules/ospath_win.jsm
@@ -0,0 +1,373 @@
+/**
+ * Handling native paths.
+ *
+ * This module contains a number of functions destined to simplify
+ * working with native paths through a cross-platform API. Functions
+ * of this module will only work with the following assumptions:
+ *
+ * - paths are valid;
+ * - paths are defined with one of the grammars that this module can
+ * parse (see later);
+ * - all path concatenations go through function |join|.
+ *
+ * Limitations of this implementation.
+ *
+ * Windows supports 6 distinct grammars for paths. For the moment, this
+ * implementation supports the following subset:
+ *
+ * - drivename:backslash-separated components
+ * - backslash-separated components
+ * - \\drivename\ followed by backslash-separated components
+ *
+ * Additionally, |normalize| can convert a path containing slash-
+ * separated components to a path containing backslash-separated
+ * components.
+ */
+
+"use strict";
+
+// Boilerplate used to be able to import this module both from the main
+// thread and from worker threads.
+if (typeof Components != "undefined") {
+ Components.utils.importGlobalProperties(["URL"]);
+ // Global definition of |exports|, to keep everybody happy.
+ // In non-main thread, |exports| is provided by the module
+ // loader.
+ this.exports = {};
+} else if (typeof module == "undefined" || typeof exports == "undefined") {
+ throw new Error("Please load this module using require()");
+}
+
+var EXPORTED_SYMBOLS = [
+ "basename",
+ "dirname",
+ "join",
+ "normalize",
+ "split",
+ "winGetDrive",
+ "winIsAbsolute",
+ "toFileURI",
+ "fromFileURI",
+];
+
+/**
+ * Return the final part of the path.
+ * The final part of the path is everything after the last "\\".
+ */
+var basename = function(path) {
+ if (path.startsWith("\\\\")) {
+ // UNC-style path
+ let index = path.lastIndexOf("\\");
+ if (index != 1) {
+ return path.slice(index + 1);
+ }
+ return ""; // Degenerate case
+ }
+ return path.slice(Math.max(path.lastIndexOf("\\"),
+ path.lastIndexOf(":")) + 1);
+};
+exports.basename = basename;
+
+/**
+ * Return the directory part of the path.
+ *
+ * If the path contains no directory, return the drive letter,
+ * or "." if the path contains no drive letter or if option
+ * |winNoDrive| is set.
+ *
+ * Otherwise, return everything before the last backslash,
+ * including the drive/server name.
+ *
+ *
+ * @param {string} path The path.
+ * @param {*=} options Platform-specific options controlling the behavior
+ * of this function. This implementation supports the following options:
+ * - |winNoDrive| If |true|, also remove the letter from the path name.
+ */
+var dirname = function(path, options) {
+ let noDrive = (options && options.winNoDrive);
+
+ // Find the last occurrence of "\\"
+ let index = path.lastIndexOf("\\");
+ if (index == -1) {
+ // If there is no directory component...
+ if (!noDrive) {
+ // Return the drive path if possible, falling back to "."
+ return this.winGetDrive(path) || ".";
+ } else {
+ // Or just "."
+ return ".";
+ }
+ }
+
+ if (index == 1 && path.charAt(0) == "\\") {
+ // The path is reduced to a UNC drive
+ if (noDrive) {
+ return ".";
+ } else {
+ return path;
+ }
+ }
+
+ // Ignore any occurrence of "\\: immediately before that one
+ while (index >= 0 && path[index] == "\\") {
+ --index;
+ }
+
+ // Compute what is left, removing the drive name if necessary
+ let start;
+ if (noDrive) {
+ start = (this.winGetDrive(path) || "").length;
+ } else {
+ start = 0;
+ }
+ return path.slice(start, index + 1);
+};
+exports.dirname = dirname;
+
+/**
+ * Join path components.
+ * This is the recommended manner of getting the path of a file/subdirectory
+ * in a directory.
+ *
+ * Example: Obtaining $TMP/foo/bar in an OS-independent manner
+ * var tmpDir = OS.Constants.Path.tmpDir;
+ * var path = OS.Path.join(tmpDir, "foo", "bar");
+ *
+ * Under Windows, this will return "$TMP\foo\bar".
+ *
+ * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the
+ * same as `OS.Path.join("foo", "bar")`.
+ */
+var join = function(...path) {
+ let paths = [];
+ let root;
+ let absolute = false;
+ for (let subpath of path) {
+ if (subpath == null) {
+ throw new TypeError("invalid path component");
+ }
+ if (subpath == "") {
+ continue;
+ }
+ let drive = this.winGetDrive(subpath);
+ if (drive) {
+ root = drive;
+ let component = trimBackslashes(subpath.slice(drive.length));
+ if (component) {
+ paths = [component];
+ } else {
+ paths = [];
+ }
+ absolute = true;
+ } else if (this.winIsAbsolute(subpath)) {
+ paths = [trimBackslashes(subpath)];
+ absolute = true;
+ } else {
+ paths.push(trimBackslashes(subpath));
+ }
+ }
+ let result = "";
+ if (root) {
+ result += root;
+ }
+ if (absolute) {
+ result += "\\";
+ }
+ result += paths.join("\\");
+ return result;
+};
+exports.join = join;
+
+/**
+ * Return the drive name of a path, or |null| if the path does
+ * not contain a drive name.
+ *
+ * Drive name appear either as "DriveName:..." (the return drive
+ * name includes the ":") or "\\\\DriveName..." (the returned drive name
+ * includes "\\\\").
+ */
+var winGetDrive = function(path) {
+ if (path == null) {
+ throw new TypeError("path is invalid");
+ }
+
+ if (path.startsWith("\\\\")) {
+ // UNC path
+ if (path.length == 2) {
+ return null;
+ }
+ let index = path.indexOf("\\", 2);
+ if (index == -1) {
+ return path;
+ }
+ return path.slice(0, index);
+ }
+ // Non-UNC path
+ let index = path.indexOf(":");
+ if (index <= 0) return null;
+ return path.slice(0, index + 1);
+};
+exports.winGetDrive = winGetDrive;
+
+/**
+ * Return |true| if the path is absolute, |false| otherwise.
+ *
+ * We consider that a path is absolute if it starts with "\\"
+ * or "driveletter:\\".
+ */
+var winIsAbsolute = function(path) {
+ let index = path.indexOf(":");
+ return path.length > index + 1 && path[index + 1] == "\\";
+};
+exports.winIsAbsolute = winIsAbsolute;
+
+/**
+ * Normalize a path by removing any unneeded ".", "..", "\\".
+ * Also convert any "/" to a "\\".
+ */
+var normalize = function(path) {
+ let stack = [];
+
+ if (!path.startsWith("\\\\")) {
+ // Normalize "/" to "\\"
+ path = path.replace(/\//g, "\\");
+ }
+
+ // Remove the drive (we will put it back at the end)
+ let root = this.winGetDrive(path);
+ if (root) {
+ path = path.slice(root.length);
+ }
+
+ // Remember whether we need to restore a leading "\\" or drive name.
+ let absolute = this.winIsAbsolute(path);
+
+ // And now, fill |stack| from the components,
+ // popping whenever there is a ".."
+ path.split("\\").forEach(function loop(v) {
+ switch (v) {
+ case "": case ".": // Ignore
+ break;
+ case "..":
+ if (stack.length == 0) {
+ if (absolute) {
+ throw new Error("Path is ill-formed: attempting to go past root");
+ } else {
+ stack.push("..");
+ }
+ } else {
+ if (stack[stack.length - 1] == "..") {
+ stack.push("..");
+ } else {
+ stack.pop();
+ }
+ }
+ break;
+ default:
+ stack.push(v);
+ }
+ });
+
+ // Put everything back together
+ let result = stack.join("\\");
+ if (absolute || root) {
+ result = "\\" + result;
+ }
+ if (root) {
+ result = root + result;
+ }
+ return result;
+};
+exports.normalize = normalize;
+
+/**
+ * Return the components of a path.
+ * You should generally apply this function to a normalized path.
+ *
+ * @return {{
+ * {bool} absolute |true| if the path is absolute, |false| otherwise
+ * {array} components the string components of the path
+ * {string?} winDrive the drive or server for this path
+ * }}
+ *
+ * Other implementations may add additional OS-specific informations.
+ */
+var split = function(path) {
+ return {
+ absolute: this.winIsAbsolute(path),
+ winDrive: this.winGetDrive(path),
+ components: path.split("\\")
+ };
+};
+exports.split = split;
+
+/**
+ * Return the file:// URI file path of the given local file path.
+ */
+// The case of %3b is designed to match Services.io, but fundamentally doesn't matter.
+var toFileURIExtraEncodings = {';': '%3b', '?': '%3F', '#': '%23'};
+var toFileURI = function toFileURI(path) {
+ // URI-escape forward slashes and convert backward slashes to forward
+ path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F');
+ // Per https://url.spec.whatwg.org we should not encode [] in the path
+ let dontNeedEscaping = {'%5B': '[', '%5D': ']'};
+ let uri = encodeURI(path).replace(/%(5B|5D)/gi,
+ match => dontNeedEscaping[match]);
+
+ // add a prefix, and encodeURI doesn't escape a few characters that we do
+ // want to escape, so fix that up
+ let prefix = "file:///";
+ uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]);
+
+ // turn e.g., file:///C: into file:///C:/
+ if (uri.charAt(uri.length - 1) === ':') {
+ uri += "/"
+ }
+
+ return uri;
+};
+exports.toFileURI = toFileURI;
+
+/**
+ * Returns the local file path from a given file URI.
+ */
+var fromFileURI = function fromFileURI(uri) {
+ let url = new URL(uri);
+ if (url.protocol != 'file:') {
+ throw new Error("fromFileURI expects a file URI");
+ }
+
+ // strip leading slash, since Windows paths don't start with one
+ uri = url.pathname.substr(1);
+
+ let path = decodeURI(uri);
+ // decode a few characters where URL's parsing is overzealous
+ path = path.replace(/%(3b|3f|23)/gi,
+ match => decodeURIComponent(match));
+ path = this.normalize(path);
+
+ // this.normalize() does not remove the trailing slash if the path
+ // component is a drive letter. eg. 'C:\'' will not get normalized.
+ if (path.endsWith(":\\")) {
+ path = path.substr(0, path.length - 1);
+ }
+ return this.normalize(path);
+};
+exports.fromFileURI = fromFileURI;
+
+/**
+* Utility function: Remove any leading/trailing backslashes
+* from a string.
+*/
+var trimBackslashes = function trimBackslashes(string) {
+ return string.replace(/^\\+|\\+$/g,'');
+};
+
+//////////// Boilerplate
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
+ for (let symbol of EXPORTED_SYMBOLS) {
+ this[symbol] = exports[symbol];
+ }
+}
diff --git a/toolkit/components/osfile/moz.build b/toolkit/components/osfile/moz.build
new file mode 100644
index 000000000..d17da2d60
--- /dev/null
+++ b/toolkit/components/osfile/moz.build
@@ -0,0 +1,35 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DIRS += [
+ 'modules',
+]
+
+MOCHITEST_CHROME_MANIFESTS += ['tests/mochi/chrome.ini']
+XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
+
+SOURCES += [
+ 'NativeOSFileInternals.cpp',
+]
+
+XPIDL_MODULE = 'toolkit_osfile'
+
+XPIDL_SOURCES += [
+ 'nsINativeOSFileInternals.idl',
+]
+
+EXPORTS.mozilla += [
+ 'NativeOSFileInternals.h',
+]
+
+EXTRA_PP_JS_MODULES += [
+ 'osfile.jsm',
+]
+
+FINAL_LIBRARY = 'xul'
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'OS.File')
diff --git a/toolkit/components/osfile/nsINativeOSFileInternals.idl b/toolkit/components/osfile/nsINativeOSFileInternals.idl
new file mode 100644
index 000000000..c1bf8be14
--- /dev/null
+++ b/toolkit/components/osfile/nsINativeOSFileInternals.idl
@@ -0,0 +1,93 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=40: */
+/* 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * The result of a successful asynchronous operation.
+ */
+[scriptable, builtinclass, uuid(08B4CF29-3D65-4E79-B522-A694C322ED07)]
+interface nsINativeOSFileResult: nsISupports
+{
+ /**
+ * The actual value produced by the operation.
+ *
+ * Actual type of this value depends on the options passed to the
+ * operation.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval result;
+
+ /**
+ * Delay between when the operation was requested on the main thread and
+ * when the operation was started off main thread.
+ */
+ readonly attribute double dispatchDurationMS;
+
+ /**
+ * Duration of the off main thread execution.
+ */
+ readonly attribute double executionDurationMS;
+};
+
+/**
+ * A callback invoked in case of success.
+ */
+[scriptable, function, uuid(2C1922CA-CA1B-4099-8B61-EC23CFF49412)]
+interface nsINativeOSFileSuccessCallback: nsISupports
+{
+ void complete(in nsINativeOSFileResult result);
+};
+
+/**
+ * A callback invoked in case of error.
+ */
+[scriptable, function, uuid(F612E0FC-6736-4D24-AA50-FD661B3B40B6)]
+interface nsINativeOSFileErrorCallback: nsISupports
+{
+ /**
+ * @param operation The name of the failed operation. Provided to aid
+ * debugging only, may change without notice.
+ * @param OSstatus The OS status of the operation (errno under Unix,
+ * GetLastError under Windows).
+ */
+ void complete(in ACString operation, in long OSstatus);
+};
+
+/**
+ * A service providing native implementations of some of the features
+ * of OS.File.
+ */
+[scriptable, builtinclass, uuid(913362AD-1526-4623-9E6B-A2EB08AFBBB9)]
+interface nsINativeOSFileInternalsService: nsISupports
+{
+ /**
+ * Implementation of OS.File.read
+ *
+ * @param path The absolute path to the file to read.
+ * @param options An object that may contain some of the following fields
+ * - {number} bytes The maximal number of bytes to read.
+ * - {string} encoding If provided, return the result as a string, decoded
+ * using this encoding. Otherwise, pass the result as an ArrayBuffer.
+ * Invalid encodings cause onError to be called with the platform-specific
+ * "invalid argument" constant.
+ * - {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);
+};
+
+
+%{ 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"
+
+%}
diff --git a/toolkit/components/osfile/osfile.jsm b/toolkit/components/osfile/osfile.jsm
new file mode 100644
index 000000000..20f21591a
--- /dev/null
+++ b/toolkit/components/osfile/osfile.jsm
@@ -0,0 +1,32 @@
+/* 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/. */
+
+/**
+ * Common front for various implementations of OS.File
+ */
+
+if (typeof Components != "undefined") {
+ this.EXPORTED_SYMBOLS = ["OS"];
+ Components.utils.import("resource://gre/modules/osfile/osfile_async_front.jsm", this);
+} else {
+ // At this stage, we need to import all sources at once to avoid
+ // a unique failure on tbpl + talos that seems caused by a
+ // what looks like a nested event loop bug (see bug 794091).
+#ifdef XP_WIN
+ importScripts(
+ "resource://gre/modules/workers/require.js",
+ "resource://gre/modules/osfile/osfile_win_back.jsm",
+ "resource://gre/modules/osfile/osfile_shared_front.jsm",
+ "resource://gre/modules/osfile/osfile_win_front.jsm"
+ );
+#else
+ importScripts(
+ "resource://gre/modules/workers/require.js",
+ "resource://gre/modules/osfile/osfile_unix_back.jsm",
+ "resource://gre/modules/osfile/osfile_shared_front.jsm",
+ "resource://gre/modules/osfile/osfile_unix_front.jsm"
+ );
+#endif
+ OS.Path = require("resource://gre/modules/osfile/ospath.jsm");
+}
diff --git a/toolkit/components/osfile/tests/mochi/.eslintrc.js b/toolkit/components/osfile/tests/mochi/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/osfile/tests/mochi/chrome.ini b/toolkit/components/osfile/tests/mochi/chrome.ini
new file mode 100644
index 000000000..1da463316
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/chrome.ini
@@ -0,0 +1,15 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ main_test_osfile_async.js
+ worker_handler.js
+ worker_test_osfile_comms.js
+ worker_test_osfile_front.js
+ worker_test_osfile_shared.js
+ worker_test_osfile_unix.js
+ worker_test_osfile_win.js
+
+[test_osfile_async.xul]
+[test_osfile_back.xul]
+[test_osfile_comms.xul]
+[test_osfile_front.xul]
diff --git a/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
new file mode 100644
index 000000000..b940a032a
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/main_test_osfile_async.js
@@ -0,0 +1,443 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/AsyncShutdown.jsm");
+
+// The following are used to compare against a well-tested reference
+// implementation of file I/O.
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var myok = ok;
+var myis = is;
+var myinfo = info;
+var myisnot = isnot;
+
+var isPromise = function ispromise(value) {
+ return value != null && typeof value == "object" && "then" in value;
+};
+
+var maketest = function(prefix, test) {
+ let utils = {
+ ok: function ok(t, m) {
+ myok(t, prefix + ": " + m);
+ },
+ is: function is(l, r, m) {
+ myis(l, r, prefix + ": " + m);
+ },
+ isnot: function isnot(l, r, m) {
+ myisnot(l, r, prefix + ": " + m);
+ },
+ info: function info(m) {
+ myinfo(prefix + ": " + m);
+ },
+ fail: function fail(m) {
+ utils.ok(false, m);
+ },
+ okpromise: function okpromise(t, m) {
+ return t.then(
+ function onSuccess() {
+ util.ok(true, m);
+ },
+ function onFailure() {
+ util.ok(false, m);
+ }
+ );
+ }
+ };
+ return function runtest() {
+ utils.info("Entering");
+ try {
+ let result = test.call(this, utils);
+ if (!isPromise(result)) {
+ throw new TypeError("The test did not return a promise");
+ }
+ utils.info("This was a promise");
+ // The test returns a promise
+ result = result.then(function test_complete() {
+ utils.info("Complete");
+ }, function catch_uncaught_errors(err) {
+ utils.fail("Uncaught error " + err);
+ if (err && typeof err == "object" && "message" in err) {
+ utils.fail("(" + err.message + ")");
+ }
+ if (err && typeof err == "object" && "stack" in err) {
+ utils.fail("at " + err.stack);
+ }
+ });
+ return result;
+ } catch (x) {
+ utils.fail("Error " + x + " at " + x.stack);
+ return null;
+ }
+ };
+};
+
+/**
+ * Fetch asynchronously the contents of a file using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} path The _absolute_ path to the file.
+ * @return {promise}
+ * @resolves {string} The contents of the file.
+ */
+var reference_fetch_file = function reference_fetch_file(path, test) {
+ test.info("Fetching file " + path);
+ let promise = Promise.defer();
+ let file = new FileUtils.File(path);
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function(stream, status) {
+ if (!Components.isSuccessCode(status)) {
+ promise.reject(status);
+ return;
+ }
+ let result, reject;
+ try {
+ result = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (x) {
+ reject = x;
+ }
+ stream.close();
+ if (reject) {
+ promise.reject(reject);
+ } else {
+ promise.resolve(result);
+ }
+ });
+
+ return promise.promise;
+};
+
+/**
+ * Compare asynchronously the contents two files using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} a The _absolute_ path to the first file.
+ * @param {string} b The _absolute_ path to the second file.
+ *
+ * @resolves {null}
+ */
+var reference_compare_files = function reference_compare_files(a, b, test) {
+ test.info("Comparing files " + a + " and " + b);
+ let a_contents = yield reference_fetch_file(a, test);
+ let b_contents = yield reference_fetch_file(b, test);
+ is(a_contents, b_contents, "Contents of files " + a + " and " + b + " match");
+};
+
+var reference_dir_contents = function reference_dir_contents(path) {
+ let result = [];
+ let entries = new FileUtils.File(path).directoryEntries;
+ while (entries.hasMoreElements()) {
+ let entry = entries.getNext().QueryInterface(Components.interfaces.nsILocalFile);
+ result.push(entry.path);
+ }
+ return result;
+};
+
+// Set/Unset OS.Shared.DEBUG, OS.Shared.TEST and a console listener.
+function toggleDebugTest (pref, consoleListener) {
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref);
+ Services.console[pref ? "registerListener" : "unregisterListener"](
+ consoleListener);
+}
+
+var test = maketest("Main", function main(test) {
+ return Task.spawn(function() {
+ SimpleTest.waitForExplicitFinish();
+ yield test_stat();
+ yield test_debug();
+ yield test_info_features_detect();
+ yield test_position();
+ yield test_iter();
+ yield test_exists();
+ yield test_debug_test();
+ info("Test is over");
+ SimpleTest.finish();
+ });
+});
+
+/**
+ * A file that we know exists and that can be used for reading.
+ */
+var EXISTING_FILE = OS.Path.join("chrome", "toolkit", "components",
+ "osfile", "tests", "mochi", "main_test_osfile_async.js");
+
+/**
+ * Test OS.File.stat and OS.File.prototype.stat
+ */
+var test_stat = maketest("stat", function stat(test) {
+ return Task.spawn(function() {
+ // Open a file and stat it
+ let file = yield OS.File.open(EXISTING_FILE);
+ let stat1;
+
+ try {
+ test.info("Stating file");
+ stat1 = yield file.stat();
+ test.ok(true, "stat has worked " + stat1);
+ test.ok(stat1, "stat is not empty");
+ } finally {
+ yield file.close();
+ }
+
+ // Stat the same file without opening it
+ test.info("Stating a file without opening it");
+ let stat2 = yield OS.File.stat(EXISTING_FILE);
+ test.ok(true, "stat 2 has worked " + stat2);
+ test.ok(stat2, "stat 2 is not empty");
+ for (let key in stat2) {
+ test.is("" + stat1[key], "" + stat2[key], "Stat field " + key + "is the same");
+ }
+ });
+});
+
+/**
+ * Test feature detection using OS.File.Info.prototype on main thread
+ */
+var test_info_features_detect = maketest("features_detect", function features_detect(test) {
+ return Task.spawn(function() {
+ if (OS.Constants.Win) {
+ // see if winBirthDate is defined
+ if ("winBirthDate" in OS.File.Info.prototype) {
+ test.ok(true, "winBirthDate is defined");
+ } else {
+ test.fail("winBirthDate not defined though we are under Windows");
+ }
+ } else if (OS.Constants.libc) {
+ // see if unixGroup is defined
+ if ("unixGroup" in OS.File.Info.prototype) {
+ test.ok(true, "unixGroup is defined");
+ } else {
+ test.fail("unixGroup is not defined though we are under Unix");
+ }
+ }
+ });
+});
+
+/**
+ * Test file.{getPosition, setPosition}
+ */
+var test_position = maketest("position", function position(test) {
+ return Task.spawn(function() {
+ let file = yield OS.File.open(EXISTING_FILE);
+
+ try {
+ let view = yield file.read();
+ test.info("First batch of content read");
+ let CHUNK_SIZE = 178;// An arbitrary number of bytes to read from the file
+ let pos = yield file.getPosition();
+ test.info("Obtained position");
+ test.is(pos, view.byteLength, "getPosition returned the end of the file");
+ pos = yield file.setPosition(-CHUNK_SIZE, OS.File.POS_END);
+ test.info("Changed position");
+ test.is(pos, view.byteLength - CHUNK_SIZE, "setPosition returned the correct position");
+
+ let view2 = yield file.read();
+ test.info("Read the end of the file");
+ for (let i = 0; i < CHUNK_SIZE; ++i) {
+ if (view2[i] != view[i + view.byteLength - CHUNK_SIZE]) {
+ test.is(view2[i], view[i], "setPosition put us in the right position");
+ }
+ }
+ } finally {
+ yield file.close();
+ }
+ });
+});
+
+/**
+ * Test OS.File.prototype.{DirectoryIterator}
+ */
+var test_iter = maketest("iter", function iter(test) {
+ return Task.spawn(function() {
+ let currentDir = yield OS.File.getCurrentDirectory();
+
+ // Trivial walks through the directory
+ test.info("Preparing iteration");
+ let iterator = new OS.File.DirectoryIterator(currentDir);
+ let temporary_file_name = OS.Path.join(currentDir, "empty-temporary-file.tmp");
+ try {
+ yield OS.File.remove(temporary_file_name);
+ } catch (err) {
+ // Ignore errors removing file
+ }
+ let allFiles1 = yield iterator.nextBatch();
+ test.info("Obtained all files through nextBatch");
+ test.isnot(allFiles1.length, 0, "There is at least one file");
+ test.isnot(allFiles1[0].path, null, "Files have a path");
+
+ // Ensure that we have the same entries with |reference_dir_contents|
+ let referenceEntries = new Set();
+ for (let entry of reference_dir_contents(currentDir)) {
+ referenceEntries.add(entry);
+ }
+ test.is(referenceEntries.size, allFiles1.length, "All the entries in the directory have been listed");
+ for (let entry of allFiles1) {
+ test.ok(referenceEntries.has(entry.path), "File " + entry.path + " effectively exists");
+ // Ensure that we have correct isDir and isSymLink
+ // Current directory is {objdir}/_tests/testing/mochitest/, assume it has some dirs and symlinks.
+ var f = new FileUtils.File(entry.path);
+ test.is(entry.isDir, f.isDirectory(), "Get file " + entry.path + " isDir correctly");
+ test.is(entry.isSymLink, f.isSymlink(), "Get file " + entry.path + " isSymLink correctly");
+ }
+
+ yield iterator.close();
+ test.info("Closed iterator");
+
+ test.info("Double closing DirectoryIterator");
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ yield iterator.close();
+ yield iterator.close(); //double closing |DirectoryIterator|
+ test.ok(true, "|DirectoryIterator| was closed twice successfully");
+
+ let allFiles2 = [];
+ let i = 0;
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ yield iterator.forEach(function(entry, index) {
+ test.is(i++, index, "Getting the correct index");
+ allFiles2.push(entry);
+ });
+ test.info("Obtained all files through forEach");
+ is(allFiles1.length, allFiles2.length, "Both runs returned the same number of files");
+ for (let i = 0; i < allFiles1.length; ++i) {
+ if (allFiles1[i].path != allFiles2[i].path) {
+ test.is(allFiles1[i].path, allFiles2[i].path, "Both runs return the same files");
+ break;
+ }
+ }
+
+ // Testing batch iteration + whether an iteration can be stopped early
+ let BATCH_LENGTH = 10;
+ test.info("Getting some files through nextBatch");
+ yield iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ let someFiles1 = yield iterator.nextBatch(BATCH_LENGTH);
+ let someFiles2 = yield iterator.nextBatch(BATCH_LENGTH);
+ yield iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ yield iterator.forEach(function cb(entry, index, iterator) {
+ if (index < BATCH_LENGTH) {
+ test.is(entry.path, someFiles1[index].path, "Both runs return the same files (part 1)");
+ } else if (index < 2*BATCH_LENGTH) {
+ test.is(entry.path, someFiles2[index - BATCH_LENGTH].path, "Both runs return the same files (part 2)");
+ } else if (index == 2 * BATCH_LENGTH) {
+ test.info("Attempting to stop asynchronous forEach");
+ return iterator.close();
+ } else {
+ test.fail("Can we stop an asynchronous forEach? " + index);
+ }
+ return null;
+ });
+ yield iterator.close();
+
+ // Ensuring that we find new files if they appear
+ let file = yield OS.File.open(temporary_file_name, { write: true } );
+ file.close();
+ iterator = new OS.File.DirectoryIterator(currentDir);
+ try {
+ let files = yield iterator.nextBatch();
+ is(files.length, allFiles1.length + 1, "The directory iterator has noticed the new file");
+ let exists = yield iterator.exists();
+ test.ok(exists, "After nextBatch, iterator detects that the directory exists");
+ } finally {
+ yield iterator.close();
+ }
+
+ // Ensuring that opening a non-existing directory fails consistently
+ // once iteration starts.
+ try {
+ iterator = null;
+ iterator = new OS.File.DirectoryIterator("/I do not exist");
+ let exists = yield iterator.exists();
+ test.ok(!exists, "Before any iteration, iterator detects that the directory doesn't exist");
+ let exn = null;
+ try {
+ yield iterator.next();
+ } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+ exn = ex;
+ let exists = yield iterator.exists();
+ test.ok(!exists, "After one iteration, iterator detects that the directory doesn't exist");
+ }
+ test.ok(exn, "Iterating through a directory that does not exist has failed with becauseNoSuchFile");
+ } finally {
+ if (iterator) {
+ iterator.close();
+ }
+ }
+ test.ok(!!iterator, "The directory iterator for a non-existing directory was correctly created");
+ });
+});
+
+/**
+ * Test OS.File.prototype.{exists}
+ */
+var test_exists = maketest("exists", function exists(test) {
+ return Task.spawn(function() {
+ let fileExists = yield OS.File.exists(EXISTING_FILE);
+ test.ok(fileExists, "file exists");
+ fileExists = yield OS.File.exists(EXISTING_FILE + ".tmp");
+ test.ok(!fileExists, "file does not exists");
+ });
+});
+
+/**
+ * Test changes to OS.Shared.DEBUG flag.
+ */
+var test_debug = maketest("debug", function debug(test) {
+ return Task.spawn(function() {
+ function testSetDebugPref (pref) {
+ try {
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ } catch (x) {
+ test.fail("Setting OS.Shared.DEBUG to " + pref +
+ " should not cause error.");
+ } finally {
+ test.is(OS.Shared.DEBUG, pref, "OS.Shared.DEBUG is set correctly.");
+ }
+ }
+ testSetDebugPref(true);
+ let workerDEBUG = yield OS.File.GET_DEBUG();
+ test.is(workerDEBUG, true, "Worker's DEBUG is set.");
+ testSetDebugPref(false);
+ workerDEBUG = yield OS.File.GET_DEBUG();
+ test.is(workerDEBUG, false, "Worker's DEBUG is unset.");
+ });
+});
+
+/**
+ * Test logging in the main thread with set OS.Shared.DEBUG and
+ * OS.Shared.TEST flags.
+ */
+var test_debug_test = maketest("debug_test", function debug_test(test) {
+ return Task.spawn(function () {
+ // Create a console listener.
+ let consoleListener = {
+ observe: function (aMessage) {
+ // Ignore unexpected messages.
+ if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
+ return;
+ }
+ if (aMessage.message.indexOf("TEST OS") < 0) {
+ return;
+ }
+ test.ok(true, "DEBUG TEST messages are logged correctly.");
+ }
+ };
+ toggleDebugTest(true, consoleListener);
+ // Execution of OS.File.exist method will trigger OS.File.LOG several times.
+ let fileExists = yield OS.File.exists(EXISTING_FILE);
+ toggleDebugTest(false, consoleListener);
+ });
+});
+
+
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_async.xul b/toolkit/components/osfile/tests/mochi/test_osfile_async.xul
new file mode 100644
index 000000000..1ef103f02
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_async.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing async I/O with OS.File"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="main_test_osfile_async.js"/>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_back.xul b/toolkit/components/osfile/tests/mochi/test_osfile_back.xul
new file mode 100644
index 000000000..b6af6303f
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_back.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="worker_handler.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+
+function test() {
+ ok(true, "test_osfile.xul: Starting test");
+ if (navigator.platform.indexOf("Win") != -1) {
+ ok(true, "test_osfile.xul: Using Windows test suite");
+ worker = new ChromeWorker("worker_test_osfile_win.js");
+ } else {
+ ok(true, "test_osfile.xul: Using Unix test suite");
+ worker = new ChromeWorker("worker_test_osfile_unix.js");
+ }
+ SimpleTest.waitForExplicitFinish();
+ ok(true, "test_osfile.xul: Chrome worker created");
+ dump("MAIN: go\n");
+ worker_handler(worker);
+ worker.postMessage(0);
+ ok(true, "test_osfile.xul: Test in progress");
+};
+]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_comms.xul b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xul
new file mode 100644
index 000000000..88e474ce2
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_comms.xul
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+"use strict";
+
+let worker;
+
+let test = function test() {
+ SimpleTest.info("test_osfile_comms.xul: Starting test");
+ Components.utils.import("resource://gre/modules/ctypes.jsm");
+ Components.utils.import("resource://gre/modules/osfile.jsm");
+ worker = new ChromeWorker("worker_test_osfile_comms.js");
+ SimpleTest.waitForExplicitFinish();
+ try {
+ worker.onerror = function onerror(error) {
+ SimpleTest.ok(false, "received error "+error);
+ }
+ worker.onmessage = function onmessage(msg) {
+ Components.utils.forceShrinkingGC();
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ " ("+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ " ("+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ case "value":
+ SimpleTest.ok(true, "test_osfile_comms.xul: Received value " + JSON.stringify(msg.data.value));
+ let type = eval(msg.data.typename);
+ let check = eval(msg.data.check);
+ let value = msg.data.value;
+ let deserialized = type.fromMsg(value);
+ check(deserialized, "Main thread test: ");
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile_comms.xul: wrong message "+JSON.stringify(msg.data));
+ return;
+ }
+ };
+ worker.postMessage(0)
+ ok(true, "Worker launched");
+ } catch(x) {
+ // As we have set |waitForExplicitFinish|, we add this fallback
+ // in case of uncaught error, to ensure that the test does not
+ // just freeze.
+ ok(false, "Caught exception " + x + " at " + x.stack);
+ SimpleTest.finish();
+ }
+};
+
+]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/test_osfile_front.xul b/toolkit/components/osfile/tests/mochi/test_osfile_front.xul
new file mode 100644
index 000000000..10d3fbd01
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/test_osfile_front.xul
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<window title="Testing OS.File on a chrome worker thread"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="test();">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/>
+ <script type="application/javascript"
+ src="worker_handler.js"/>
+ <script type="application/javascript">
+ <![CDATA[
+
+let worker;
+let main = this;
+
+function test() {
+ info("test_osfile_front.xul: Starting test");
+
+ // Test the OS.File worker
+
+ worker = new ChromeWorker("worker_test_osfile_front.js");
+ SimpleTest.waitForExplicitFinish();
+ info("test_osfile_front.xul: Chrome worker created");
+ dump("MAIN: go\n");
+ worker_handler(worker);
+ worker.postMessage(0);
+ ok(true, "test_osfile_front.xul: Test in progress");
+};
+]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+ <label id="test-result"/>
+</window>
diff --git a/toolkit/components/osfile/tests/mochi/worker_handler.js b/toolkit/components/osfile/tests/mochi/worker_handler.js
new file mode 100644
index 000000000..6c513e6ba
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_handler.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function worker_handler(worker) {
+ worker.onerror = function(error) {
+ error.preventDefault();
+ ok(false, "Worker error " + error.message);
+ }
+ worker.onmessage = function(msg) {
+ ok(true, "MAIN: onmessage " + JSON.stringify(msg.data));
+ switch (msg.data.kind) {
+ case "is":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " ==? " + msg.data.b + ")" );
+ return;
+ case "isnot":
+ SimpleTest.ok(msg.data.outcome, msg.data.description +
+ "( "+ msg.data.a + " !=? " + msg.data.b + ")" );
+ return;
+ case "ok":
+ SimpleTest.ok(msg.data.condition, msg.data.description);
+ return;
+ case "info":
+ SimpleTest.info(msg.data.description);
+ return;
+ case "finish":
+ SimpleTest.finish();
+ return;
+ default:
+ SimpleTest.ok(false, "test_osfile.xul: wrong message " + JSON.stringify(msg.data));
+ return;
+ }
+ };
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js
new file mode 100644
index 000000000..5e8bdd9ca
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_comms.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+importScripts('worker_test_osfile_shared.js');
+
+// The set of samples for communications test. Declare as a global
+// variable to prevent this from being garbage-collected too early.
+var samples;
+
+self.onmessage = function(msg) {
+ info("Initializing");
+ self.onmessage = function on_unexpected_message(msg) {
+ throw new Error("Unexpected message " + JSON.stringify(msg.data));
+ };
+ importScripts("resource://gre/modules/osfile.jsm");
+ info("Initialization complete");
+
+ samples = [
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "String",
+ value: "This is a test",
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_string(candidate, prefix) {
+ is(candidate, "This is a test", prefix);
+ }},
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "Typed array",
+ value: (function() {
+ let view = new Uint8Array(15);
+ for (let i = 0; i < 15; ++i) {
+ view[i] = i;
+ }
+ return view;
+ })(),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_ArrayBuffer(candidate, prefix) {
+ for (let i = 0; i < 15; ++i) {
+ is(candidate[i], i % 256, prefix + "Checking that the contents of the ArrayBuffer were preserved");
+ }
+ }},
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "Pointer",
+ value: new OS.Shared.Type.char.in_ptr.implementation(1),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_ptr(candidate, prefix) {
+ let address = ctypes.cast(candidate, ctypes.uintptr_t).value.toString();
+ is(address, "1", prefix + "Checking that the pointer address was preserved");
+ }},
+ { typename: "OS.Shared.Type.char.in_ptr",
+ valuedescr: "C array",
+ value: (function() {
+ let buf = new (ctypes.ArrayType(ctypes.uint8_t, 15))();
+ for (let i = 0; i < 15; ++i) {
+ buf[i] = i % 256;
+ }
+ return buf;
+ })(),
+ type: OS.Shared.Type.char.in_ptr,
+ check: function check_array(candidate, prefix) {
+ let cast = ctypes.cast(candidate, ctypes.uint8_t.ptr);
+ for (let i = 0; i < 15; ++i) {
+ is(cast.contents, i % 256, prefix + "Checking that the contents of the C array were preserved, index " + i);
+ cast = cast.increment();
+ }
+ }
+ },
+ { typename: "OS.File.Error",
+ valuedescr: "OS Error",
+ type: OS.File.Error,
+ value: new OS.File.Error("foo", 1),
+ check: function check_error(candidate, prefix) {
+ ok(candidate instanceof OS.File.Error,
+ prefix + "Error is an OS.File.Error");
+ ok(candidate.unixErrno == 1 || candidate.winLastError == 1,
+ prefix + "Error code is correct");
+ try {
+ let string = candidate.toString();
+ info(prefix + ".toString() works " + string);
+ } catch (x) {
+ ok(false, prefix + ".toString() fails " + x);
+ }
+ }
+ }
+ ];
+ samples.forEach(function test(sample) {
+ let type = sample.type;
+ let value = sample.value;
+ let check = sample.check;
+ info("Testing handling of type " + sample.typename + " communicating " + sample.valuedescr);
+
+ // 1. Test serialization
+ let serialized;
+ let exn;
+ try {
+ serialized = type.toMsg(value);
+ } catch (ex) {
+ exn = ex;
+ }
+ is(exn, null, "Can I serialize the following value? " + value +
+ " aka " + JSON.stringify(value));
+ if (exn) {
+ return;
+ }
+
+ if ("data" in serialized) {
+ // Unwrap from `Meta`
+ serialized = serialized.data;
+ }
+
+ // 2. Test deserialization
+ let deserialized;
+ try {
+ deserialized = type.fromMsg(serialized);
+ } catch (ex) {
+ exn = ex;
+ }
+ is(exn, null, "Can I deserialize the following message? " + serialized
+ + " aka " + JSON.stringify(serialized));
+ if (exn) {
+ return;
+ }
+
+ // 3. Local test deserialized value
+ info("Running test on deserialized value " + deserialized +
+ " aka " + JSON.stringify(deserialized));
+ check(deserialized, "Local test: ");
+
+ // 4. Test sending serialized
+ info("Attempting to send message");
+ try {
+ self.postMessage({kind:"value",
+ typename: sample.typename,
+ value: serialized,
+ check: check.toSource()});
+ } catch (ex) {
+ exn = ex;
+ }
+ is(exn, null, "Can I send the following message? " + serialized
+ + " aka " + JSON.stringify(serialized));
+ });
+
+ finish();
+ };
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js
new file mode 100644
index 000000000..29e613510
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_front.js
@@ -0,0 +1,566 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts('worker_test_osfile_shared.js');
+importScripts("resource://gre/modules/workers/require.js");
+
+var SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
+SharedAll.Config.DEBUG = true;
+
+function should_throw(f) {
+ try {
+ f();
+ } catch (x) {
+ return x;
+ }
+ return null;
+}
+
+self.onmessage = function onmessage_start(msg) {
+ self.onmessage = function onmessage_ignored(msg) {
+ log("ignored message " + JSON.stringify(msg.data));
+ };
+ try {
+ test_init();
+ test_open_existing_file();
+ test_open_non_existing_file();
+ test_flush_open_file();
+ test_copy_existing_file();
+ test_position();
+ test_move_file();
+ test_iter_dir();
+ test_info();
+ test_path();
+ test_exists_file();
+ test_remove_file();
+ } catch (x) {
+ log("Catching error: " + x);
+ log("Stack: " + x.stack);
+ log("Source: " + x.toSource());
+ ok(false, x.toString() + "\n" + x.stack);
+ }
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+/**
+ * Test that we can open an existing file.
+ */
+function test_open_existing_file()
+{
+ info("Starting test_open_existing");
+ let file = OS.File.open("chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js");
+ file.close();
+}
+
+/**
+ * Test that opening a file that does not exist fails with the right error.
+ */
+function test_open_non_existing_file()
+{
+ info("Starting test_open_non_existing");
+ let exn;
+ try {
+ let file = OS.File.open("/I do not exist");
+ } catch (x) {
+ exn = x;
+ info("test_open_non_existing_file: Exception detail " + exn);
+ }
+ ok(!!exn, "test_open_non_existing_file: Exception was raised ");
+ ok(exn instanceof OS.File.Error, "test_open_non_existing_file: Exception was a OS.File.Error");
+ ok(exn.becauseNoSuchFile, "test_open_non_existing_file: Exception confirms that the file does not exist");
+}
+
+/**
+ * Test that to ensure that |foo.flush()| does not
+ * cause an error, where |foo| is an open file.
+ */
+function test_flush_open_file()
+{
+ info("Starting test_flush_open_file");
+ let tmp = "test_flush.tmp";
+ let file = OS.File.open(tmp, {create: true, write: true});
+ file.flush();
+ file.close();
+ OS.File.remove(tmp);
+}
+
+/**
+ * Utility function for comparing two files (or a prefix of two files).
+ *
+ * This function returns nothing but fails of both files (or prefixes)
+ * are not identical.
+ *
+ * @param {string} test The name of the test (used for logging).
+ * @param {string} sourcePath The name of the first file.
+ * @param {string} destPath The name of the second file.
+ * @param {number=} prefix If specified, only compare the |prefix|
+ * first bytes of |sourcePath| and |destPath|.
+ */
+function compare_files(test, sourcePath, destPath, prefix)
+{
+ info(test + ": Comparing " + sourcePath + " and " + destPath);
+ let source = OS.File.open(sourcePath);
+ let dest = OS.File.open(destPath);
+ info("Files are open");
+ let sourceResult, destResult;
+ try {
+ if (prefix != undefined) {
+ sourceResult = source.read(prefix);
+ destResult = dest.read(prefix);
+ } else {
+ sourceResult = source.read();
+ destResult = dest.read();
+ }
+ is(sourceResult.length, destResult.length, test + ": Both files have the same size");
+ for (let i = 0; i < sourceResult.length; ++i) {
+ if (sourceResult[i] != destResult[i]) {
+ is(sourceResult[i] != destResult[i], test + ": Comparing char " + i);
+ break;
+ }
+ }
+ } finally {
+ source.close();
+ dest.close();
+ }
+ info(test + ": Comparison complete");
+}
+
+/**
+ * Test that copying a file using |copy| works.
+ */
+function test_copy_existing_file()
+{
+ let src_file_name =
+ OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi",
+ "worker_test_osfile_front.js");
+ let tmp_file_name = "test_osfile_front.tmp";
+ info("Starting test_copy_existing");
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ info("test_copy_existing: Copy complete");
+ compare_files("test_copy_existing", src_file_name, tmp_file_name);
+
+ // Create a bogus file with arbitrary content, then attempt to overwrite
+ // it with |copy|.
+ let dest = OS.File.open(tmp_file_name, {trunc: true});
+ let buf = new Uint8Array(50);
+ dest.write(buf);
+ dest.close();
+
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ compare_files("test_copy_existing 2", src_file_name, tmp_file_name);
+
+ // Attempt to overwrite with noOverwrite
+ let exn;
+ try {
+ OS.File.copy(src_file_name, tmp_file_name, {noOverwrite: true});
+ } catch(x) {
+ exn = x;
+ }
+ ok(!!exn, "test_copy_existing: noOverwrite prevents overwriting existing files");
+
+ info("test_copy_existing: Cleaning up");
+ OS.File.remove(tmp_file_name);
+}
+
+/**
+ * Test that moving a file works.
+ */
+function test_move_file()
+{
+ info("test_move_file: Starting");
+ // 1. Copy file into a temporary file
+ let src_file_name =
+ OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi",
+ "worker_test_osfile_front.js");
+ let tmp_file_name = "test_osfile_front.tmp";
+ let tmp2_file_name = "test_osfile_front.tmp2";
+ OS.File.copy(src_file_name, tmp_file_name);
+
+ info("test_move_file: Copy complete");
+
+ // 2. Move
+ OS.File.move(tmp_file_name, tmp2_file_name);
+
+ info("test_move_file: Move complete");
+
+ // 3. Check that destination exists
+ compare_files("test_move_file", src_file_name, tmp2_file_name);
+
+ // 4. Check that original file does not exist anymore
+ let exn;
+ try {
+ OS.File.open(tmp_file_name);
+ } catch (x) {
+ exn = x;
+ }
+ ok(!!exn, "test_move_file: Original file has been removed");
+
+ info("test_move_file: Cleaning up");
+ OS.File.remove(tmp2_file_name);
+}
+
+function test_iter_dir()
+{
+ info("test_iter_dir: Starting");
+
+ // Create a file, to be sure that it exists
+ let tmp_file_name = "test_osfile_front.tmp";
+ let tmp_file = OS.File.open(tmp_file_name, {write: true, trunc:true});
+ tmp_file.close();
+
+ let parent = OS.File.getCurrentDirectory();
+ info("test_iter_dir: directory " + parent);
+ let iterator = new OS.File.DirectoryIterator(parent);
+ info("test_iter_dir: iterator created");
+ let encountered_tmp_file = false;
+ for (let entry in iterator) {
+ // Checking that |name| can be decoded properly
+ info("test_iter_dir: encountering entry " + entry.name);
+
+ if (entry.name == tmp_file_name) {
+ encountered_tmp_file = true;
+ isnot(entry.isDir, "test_iter_dir: The temporary file is not a directory");
+ isnot(entry.isSymLink, "test_iter_dir: The temporary file is not a link");
+ }
+
+ let file;
+ let success = true;
+ try {
+ file = OS.File.open(entry.path);
+ } catch (x) {
+ if (x.becauseNoSuchFile) {
+ success = false;
+ }
+ }
+ if (file) {
+ file.close();
+ }
+ ok(success, "test_iter_dir: Entry " + entry.path + " exists");
+
+ if (OS.Win) {
+ // We assume that the files are at least as recent as 2009.
+ // Since this test was written in 2011 and some of our packaging
+ // sets dates arbitrarily to 2010, this should be safe.
+ let year = new Date().getFullYear();
+ let creation = entry.winCreationDate;
+ ok(creation, "test_iter_dir: Windows creation date exists: " + creation);
+ ok(creation.getFullYear() >= 2009 && creation.getFullYear() <= year, "test_iter_dir: consistent creation date");
+
+ let lastWrite = entry.winLastWriteDate;
+ ok(lastWrite, "test_iter_dir: Windows lastWrite date exists: " + lastWrite);
+ ok(lastWrite.getFullYear() >= 2009 && lastWrite.getFullYear() <= year, "test_iter_dir: consistent lastWrite date");
+
+ let lastAccess = entry.winLastAccessDate;
+ ok(lastAccess, "test_iter_dir: Windows lastAccess date exists: " + lastAccess);
+ ok(lastAccess.getFullYear() >= 2009 && lastAccess.getFullYear() <= year, "test_iter_dir: consistent lastAccess date");
+ }
+
+ }
+ ok(encountered_tmp_file, "test_iter_dir: We have found the temporary file");
+
+ info("test_iter_dir: Cleaning up");
+ iterator.close();
+
+ // Testing nextBatch()
+ iterator = new OS.File.DirectoryIterator(parent);
+ let allentries = [];
+ for (let x in iterator) {
+ allentries.push(x);
+ }
+ iterator.close();
+
+ ok(allentries.length >= 14, "test_iter_dir: Meta-check: the test directory should contain at least 14 items");
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ let firstten = iterator.nextBatch(10);
+ is(firstten.length, 10, "test_iter_dir: nextBatch(10) returns 10 items");
+ for (let i = 0; i < firstten.length; ++i) {
+ is(allentries[i].path, firstten[i].path, "test_iter_dir: Checking that batch returns the correct entries");
+ }
+ let nextthree = iterator.nextBatch(3);
+ is(nextthree.length, 3, "test_iter_dir: nextBatch(3) returns 3 items");
+ for (let i = 0; i < nextthree.length; ++i) {
+ is(allentries[i + firstten.length].path, nextthree[i].path, "test_iter_dir: Checking that batch 2 returns the correct entries");
+ }
+ let everythingelse = iterator.nextBatch();
+ ok(everythingelse.length >= 1, "test_iter_dir: nextBatch() returns at least one item");
+ for (let i = 0; i < everythingelse.length; ++i) {
+ is(allentries[i + firstten.length + nextthree.length].path, everythingelse[i].path, "test_iter_dir: Checking that batch 3 returns the correct entries");
+ }
+ is(iterator.nextBatch().length, 0, "test_iter_dir: Once there is nothing left, nextBatch returns an empty array");
+ iterator.close();
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ iterator.close();
+ is(iterator.nextBatch().length, 0, "test_iter_dir: nextBatch on closed iterator returns an empty array");
+
+ iterator = new OS.File.DirectoryIterator(parent);
+ let allentries2 = iterator.nextBatch();
+ is(allentries.length, allentries2.length, "test_iter_dir: Checking that getBatch(null) returns the right number of entries");
+ for (let i = 0; i < allentries.length; ++i) {
+ is(allentries[i].path, allentries2[i].path, "test_iter_dir: Checking that getBatch(null) returns everything in the right order");
+ }
+ iterator.close();
+
+ // Test forEach
+ iterator = new OS.File.DirectoryIterator(parent);
+ let index = 0;
+ iterator.forEach(
+ function cb(entry, aIndex, aIterator) {
+ is(index, aIndex, "test_iter_dir: Checking that forEach index is correct");
+ ok(iterator == aIterator, "test_iter_dir: Checking that right iterator is passed");
+ if (index < 10) {
+ is(allentries[index].path, entry.path, "test_iter_dir: Checking that forEach entry is correct");
+ } else if (index == 10) {
+ iterator.close();
+ } else {
+ ok(false, "test_iter_dir: Checking that forEach can be stopped early");
+ }
+ ++index;
+ });
+ iterator.close();
+
+ //test for prototype |OS.File.DirectoryIterator.unixAsFile|
+ if ("unixAsFile" in OS.File.DirectoryIterator.prototype) {
+ info("testing property unixAsFile");
+ let path = OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi");
+ iterator = new OS.File.DirectoryIterator(path);
+
+ let dir_file = iterator.unixAsFile();// return |File|
+ let stat0 = dir_file.stat();
+ let stat1 = OS.File.stat(path);
+
+ let unix_info_to_string = function unix_info_to_string(info) {
+ return "| " + info.unixMode + " | " + info.unixOwner + " | " + info.unixGroup + " | " + info.creationDate + " | " + info.lastModificationDate + " | " + info.lastAccessDate + " | " + info.size + " |";
+ };
+
+ let s0_string = unix_info_to_string(stat0);
+ let s1_string = unix_info_to_string(stat1);
+
+ ok(stat0.isDir, "unixAsFile returned a directory");
+ is(s0_string, s1_string, "unixAsFile returned the correct file");
+ dir_file.close();
+ iterator.close();
+ }
+ info("test_iter_dir: Complete");
+}
+
+function test_position() {
+ info("test_position: Starting");
+
+ ok("POS_START" in OS.File, "test_position: POS_START exists");
+ ok("POS_CURRENT" in OS.File, "test_position: POS_CURRENT exists");
+ ok("POS_END" in OS.File, "test_position: POS_END exists");
+
+ let ARBITRARY_POSITION = 321;
+ let src_file_name =
+ OS.Path.join("chrome", "toolkit", "components", "osfile", "tests", "mochi",
+ "worker_test_osfile_front.js");
+
+ let file = OS.File.open(src_file_name);
+ is(file.getPosition(), 0, "test_position: Initial position is 0");
+
+ let size = 0 + file.stat().size; // Hack: We can remove this 0 + once 776259 has landed
+
+ file.setPosition(ARBITRARY_POSITION, OS.File.POS_START);
+ is(file.getPosition(), ARBITRARY_POSITION, "test_position: Setting position from start");
+
+ file.setPosition(0, OS.File.POS_START);
+ is(file.getPosition(), 0, "test_position: Setting position from start back to 0");
+
+ file.setPosition(ARBITRARY_POSITION);
+ is(file.getPosition(), ARBITRARY_POSITION, "test_position: Setting position without argument");
+
+ file.setPosition(-ARBITRARY_POSITION, OS.File.POS_END);
+ is(file.getPosition(), size - ARBITRARY_POSITION, "test_position: Setting position from end");
+
+ file.setPosition(ARBITRARY_POSITION, OS.File.POS_CURRENT);
+ is(file.getPosition(), size, "test_position: Setting position from current");
+
+ file.close();
+ info("test_position: Complete");
+}
+
+function test_info() {
+ info("test_info: Starting");
+
+ let filename = "test_info.tmp";
+ let size = 261;// An arbitrary file length
+ let start = new Date();
+
+ // Cleanup any leftover from previous tests
+ try {
+ OS.File.remove(filename);
+ info("test_info: Cleaned up previous garbage");
+ } catch (x) {
+ if (!x.becauseNoSuchFile) {
+ throw x;
+ }
+ info("test_info: No previous garbage");
+ }
+
+ let file = OS.File.open(filename, {trunc: true});
+ let buf = new ArrayBuffer(size);
+ file._write(buf, size);
+ file.close();
+
+ // Test OS.File.stat on new file
+ let stat = OS.File.stat(filename);
+ ok(!!stat, "test_info: info acquired");
+ ok(!stat.isDir, "test_info: file is not a directory");
+ is(stat.isSymLink, false, "test_info: file is not a link");
+ is(stat.size.toString(), size, "test_info: correct size");
+
+ let stop = new Date();
+
+ // We round down/up by 1s as file system precision is lower than
+ // Date precision (no clear specifications about that, but it seems
+ // that this can be a little over 1 second under ext3 and 2 seconds
+ // under FAT).
+ let SLOPPY_FILE_SYSTEM_ADJUSTMENT = 3000;
+ let startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ let stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ info("Testing stat with bounds [ " + startMs + ", " + stopMs +" ]");
+
+ (function() {
+ let birth;
+ if ("winBirthDate" in stat) {
+ birth = stat.winBirthDate;
+ } else if ("macBirthDate" in stat) {
+ birth = stat.macBirthDate;
+ } else {
+ ok(true, "Skipping birthdate test");
+ return;
+ }
+ ok(birth.getTime() <= stopMs,
+ "test_info: platformBirthDate is consistent");
+ // Note: Previous versions of this test checked whether the file
+ // has been created after the start of the test. Unfortunately,
+ // this sometimes failed under Windows, in specific circumstances:
+ // if the file has been removed at the start of the test and
+ // recreated immediately, the Windows file system detects this and
+ // decides that the file was actually truncated rather than
+ // recreated, hence that it should keep its previous creation
+ // date. Debugging hilarity ensues.
+ });
+
+ let change = stat.lastModificationDate;
+ info("Testing lastModificationDate: " + change);
+ ok(change.getTime() >= startMs && change.getTime() <= stopMs,
+ "test_info: lastModificationDate is consistent");
+
+ // Test OS.File.prototype.stat on new file
+ file = OS.File.open(filename);
+ try {
+ stat = file.stat();
+ } finally {
+ file.close();
+ }
+
+ ok(!!stat, "test_info: info acquired 2");
+ ok(!stat.isDir, "test_info: file is not a directory 2");
+ ok(!stat.isSymLink, "test_info: file is not a link 2");
+ is(stat.size.toString(), size, "test_info: correct size 2");
+
+ stop = new Date();
+
+ // Round up/down as above
+ startMs = start.getTime() - SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ stopMs = stop.getTime() + SLOPPY_FILE_SYSTEM_ADJUSTMENT;
+ info("Testing stat 2 with bounds [ " + startMs + ", " + stopMs +" ]");
+
+ let access = stat.lastAccessDate;
+ info("Testing lastAccessDate: " + access);
+ ok(access.getTime() >= startMs && access.getTime() <= stopMs,
+ "test_info: lastAccessDate is consistent");
+
+ change = stat.lastModificationDate;
+ info("Testing lastModificationDate 2: " + change);
+ ok(change.getTime() >= startMs && change.getTime() <= stopMs,
+ "test_info: lastModificationDate 2 is consistent");
+
+ // Test OS.File.stat on directory
+ stat = OS.File.stat(OS.File.getCurrentDirectory());
+ ok(!!stat, "test_info: info on directory acquired");
+ ok(stat.isDir, "test_info: directory is a directory");
+
+ info("test_info: Complete");
+}
+
+// Note that most of the features of path are tested in
+// worker_test_osfile_{unix, win}.js
+function test_path()
+{
+ info("test_path: starting");
+ let abcd = OS.Path.join("a", "b", "c", "d");
+ is(OS.Path.basename(abcd), "d", "basename of a/b/c/d");
+
+ let abc = OS.Path.join("a", "b", "c");
+ is(OS.Path.dirname(abcd), abc, "dirname of a/b/c/d");
+
+ let abdotsc = OS.Path.join("a", "b", "..", "c");
+ is(OS.Path.normalize(abdotsc), OS.Path.join("a", "c"), "normalize a/b/../c");
+
+ let adotsdotsdots = OS.Path.join("a", "..", "..", "..");
+ is(OS.Path.normalize(adotsdotsdots), OS.Path.join("..", ".."), "normalize a/../../..");
+
+ info("test_path: Complete");
+}
+
+/**
+ * Test the file |exists| method.
+ */
+function test_exists_file()
+{
+ let file_name = OS.Path.join("chrome", "toolkit", "components" ,"osfile",
+ "tests", "mochi", "test_osfile_front.xul");
+ info("test_exists_file: starting");
+ ok(OS.File.exists(file_name), "test_exists_file: file exists (OS.File.exists)");
+ ok(!OS.File.exists(file_name + ".tmp"), "test_exists_file: file does not exists (OS.File.exists)");
+
+ let dir_name = OS.Path.join("chrome", "toolkit", "components" ,"osfile",
+ "tests", "mochi");
+ ok(OS.File.exists(dir_name), "test_exists_file: directory exists");
+ ok(!OS.File.exists(dir_name) + ".tmp", "test_exists_file: directory does not exist");
+
+ info("test_exists_file: complete");
+}
+
+/**
+ * Test the file |remove| method.
+ */
+function test_remove_file()
+{
+ let absent_file_name = "test_osfile_front_absent.tmp";
+
+ // Check that removing absent files is handled correctly
+ let exn = should_throw(function() {
+ OS.File.remove(absent_file_name, {ignoreAbsent: false});
+ });
+ ok(!!exn, "test_remove_file: throws if there is no such file");
+
+ exn = should_throw(function() {
+ OS.File.remove(absent_file_name, {ignoreAbsent: true});
+ OS.File.remove(absent_file_name);
+ });
+ ok(!exn, "test_remove_file: ignoreAbsent works");
+
+ if (OS.Win) {
+ let file_name = "test_osfile_front_file_to_remove.tmp";
+ let file = OS.File.open(file_name, {write: true});
+ file.close();
+ ok(OS.File.exists(file_name), "test_remove_file: test file exists");
+ OS.Win.File.SetFileAttributes(file_name,
+ OS.Constants.Win.FILE_ATTRIBUTE_READONLY);
+ OS.File.remove(file_name);
+ ok(!OS.File.exists(file_name),
+ "test_remove_file: test file has been removed");
+ }
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js
new file mode 100644
index 000000000..da82d4b0a
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_shared.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function log(text) {
+ dump("WORKER " + text + "\n");
+}
+
+function send(message) {
+ self.postMessage(message);
+}
+
+function finish() {
+ send({kind: "finish"});
+}
+
+function ok(condition, description) {
+ send({kind: "ok", condition: !!condition, description: "" + description});
+}
+
+function is(a, b, description) {
+ let outcome = a == b; // Need to decide outcome here, as not everything can be serialized
+ send({kind: "is", outcome: outcome, description: "" + description, a: "" + a, b: "" + b});
+}
+
+function isnot(a, b, description) {
+ let outcome = a != b; // Need to decide outcome here, as not everything can be serialized
+ send({kind: "isnot", outcome: outcome, description: "" + description, a: "" + a, b: "" + b});
+}
+
+function info(description) {
+ send({kind: "info", description: "" + description});
+}
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js
new file mode 100644
index 000000000..9fe2d0b4e
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts('worker_test_osfile_shared.js');
+
+self.onmessage = function(msg) {
+ log("received message "+JSON.stringify(msg.data));
+ self.onmessage = function(msg) {
+ log("ignored message "+JSON.stringify(msg.data));
+ };
+ test_init();
+ test_getcwd();
+ test_open_close();
+ test_create_file();
+ test_access();
+ test_read_write();
+ test_passing_undefined();
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+function test_open_close() {
+ info("Starting test_open_close");
+ is(typeof OS.Unix.File.open, "function", "OS.Unix.File.open is a function");
+ let file = OS.Unix.File.open("chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js", OS.Constants.libc.O_RDONLY, 0);
+ isnot(file, -1, "test_open_close: opening succeeded");
+ info("Close: "+OS.Unix.File.close.toSource());
+ let result = OS.Unix.File.close(file);
+ is(result, 0, "test_open_close: close succeeded");
+
+ file = OS.Unix.File.open("/i do not exist", OS.Constants.libc.O_RDONLY, 0);
+ is(file, -1, "test_open_close: opening of non-existing file failed");
+ is(ctypes.errno, OS.Constants.libc.ENOENT, "test_open_close: error is ENOENT");
+}
+
+function test_create_file()
+{
+ info("Starting test_create_file");
+ let file = OS.Unix.File.open("test.tmp", OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ isnot(file, -1, "test_create_file: file created");
+ OS.Unix.File.close(file);
+}
+
+function test_access()
+{
+ info("Starting test_access");
+ let file = OS.Unix.File.open("test1.tmp", OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ let result = OS.Unix.File.access("test1.tmp", OS.Constants.libc.R_OK | OS.Constants.libc.W_OK | OS.Constants.libc.X_OK | OS.Constants.libc.F_OK);
+ is(result, 0, "first call to access() succeeded");
+ OS.Unix.File.close(file);
+
+ file = OS.Unix.File.open("test1.tmp", OS.Constants.libc.O_WRONLY
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IWUSR);
+
+ info("test_access: preparing second call to access()");
+ result = OS.Unix.File.access("test2.tmp", OS.Constants.libc.R_OK
+ | OS.Constants.libc.W_OK
+ | OS.Constants.libc.X_OK
+ | OS.Constants.libc.F_OK);
+ is(result, -1, "test_access: second call to access() failed as expected");
+ is(ctypes.errno, OS.Constants.libc.ENOENT, "This is the correct error");
+ OS.Unix.File.close(file);
+}
+
+function test_getcwd()
+{
+ let array = new (ctypes.ArrayType(ctypes.char, 32768))();
+ let path = OS.Unix.File.getcwd(array, array.length);
+ if (ctypes.char.ptr(path).isNull()) {
+ ok(false, "test_get_cwd: getcwd returned null, errno: " + ctypes.errno);
+ }
+ let path2;
+ if (OS.Unix.File.get_current_dir_name) {
+ path2 = OS.Unix.File.get_current_dir_name();
+ } else {
+ path2 = OS.Unix.File.getwd_auto(null);
+ }
+ if (ctypes.char.ptr(path2).isNull()) {
+ ok(false, "test_get_cwd: getwd_auto/get_current_dir_name returned null, errno: " + ctypes.errno);
+ }
+ is(path.readString(), path2.readString(), "test_get_cwd: getcwd and getwd return the same path");
+}
+
+function test_read_write()
+{
+ let output_name = "osfile_copy.tmp";
+ // Copy file
+ let input = OS.Unix.File.open(
+ "chrome/toolkit/components/osfile/tests/mochi/worker_test_osfile_unix.js",
+ OS.Constants.libc.O_RDONLY, 0);
+ isnot(input, -1, "test_read_write: input file opened");
+ let output = OS.Unix.File.open("osfile_copy.tmp", OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ isnot(output, -1, "test_read_write: output file opened");
+
+ let array = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes = -1;
+ let total = 0;
+ while (true) {
+ bytes = OS.Unix.File.read(input, array, 4096);
+ ok(bytes != undefined, "test_read_write: bytes is defined");
+ isnot(bytes, -1, "test_read_write: no read error");
+ let write_from = 0;
+ if (bytes == 0) {
+ break;
+ }
+ while (bytes > 0) {
+ let ptr = array.addressOfElement(write_from);
+ // Note: |write| launches an exception in case of error
+ let written = OS.Unix.File.write(output, array, bytes);
+ isnot(written, -1, "test_read_write: no write error");
+ write_from += written;
+ bytes -= written;
+ }
+ total += write_from;
+ }
+ info("test_read_write: copy complete " + total);
+
+ // Compare files
+ let result;
+ info("SEEK_SET: " + OS.Constants.libc.SEEK_SET);
+ info("Input: " + input + "(" + input.toSource() + ")");
+ info("Output: " + output + "(" + output.toSource() + ")");
+ result = OS.Unix.File.lseek(input, 0, OS.Constants.libc.SEEK_SET);
+ info("Result of lseek: " + result);
+ isnot(result, -1, "test_read_write: input seek succeeded " + ctypes.errno);
+ result = OS.Unix.File.lseek(output, 0, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: output seek succeeded " + ctypes.errno);
+
+ let array2 = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes2 = -1;
+ let pos = 0;
+ while (true) {
+ bytes = OS.Unix.File.read(input, array, 4096);
+ isnot(bytes, -1, "test_read_write: input read succeeded");
+ bytes2 = OS.Unix.File.read(output, array2, 4096);
+ isnot(bytes, -1, "test_read_write: output read succeeded");
+ is(bytes > 0, bytes2 > 0, "Both files contain data or neither does "+bytes+", "+bytes2);
+ if (bytes == 0) {
+ break;
+ }
+ if (bytes != bytes2) {
+ // This would be surprising, but theoretically possible with a
+ // remote file system, I believe.
+ bytes = Math.min(bytes, bytes2);
+ pos += bytes;
+ result = OS.Unix.File.lseek(input, pos, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: input seek succeeded");
+ result = OS.Unix.File.lseek(output, pos, OS.Constants.libc.SEEK_SET);
+ isnot(result, -1, "test_read_write: output seek succeeded");
+ } else {
+ pos += bytes;
+ }
+ for (let i = 0; i < bytes; ++i) {
+ if (array[i] != array2[i]) {
+ ok(false, "Files do not match at position " + i
+ + " ("+array[i] + "/"+array2[i] + ")");
+ }
+ }
+ }
+ info("test_read_write test complete");
+ result = OS.Unix.File.close(input);
+ isnot(result, -1, "test_read_write: input close succeeded");
+ result = OS.Unix.File.close(output);
+ isnot(result, -1, "test_read_write: output close succeeded");
+ result = OS.Unix.File.unlink(output_name);
+ isnot(result, -1, "test_read_write: input remove succeeded");
+ info("test_read_write cleanup complete");
+}
+
+function test_passing_undefined()
+{
+ info("Testing that an exception gets thrown when an FFI function is passed undefined");
+ let exceptionRaised = false;
+
+ try {
+ let file = OS.Unix.File.open(undefined, OS.Constants.libc.O_RDWR
+ | OS.Constants.libc.O_CREAT
+ | OS.Constants.libc.O_TRUNC,
+ OS.Constants.libc.S_IRWXU);
+ } catch(e if e instanceof TypeError && e.message.indexOf("open") > -1) {
+ exceptionRaised = true;
+ }
+
+ ok(exceptionRaised, "test_passing_undefined: exception gets thrown")
+}
+
diff --git a/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js
new file mode 100644
index 000000000..f41fdecfe
--- /dev/null
+++ b/toolkit/components/osfile/tests/mochi/worker_test_osfile_win.js
@@ -0,0 +1,211 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+importScripts('worker_test_osfile_shared.js');
+
+self.onmessage = function(msg) {
+ self.onmessage = function(msg) {
+ log("ignored message "+JSON.stringify(msg.data));
+ };
+
+ test_init();
+ test_GetCurrentDirectory();
+ test_OpenClose();
+ test_CreateFile();
+ test_ReadWrite();
+ test_passing_undefined();
+ finish();
+};
+
+function test_init() {
+ info("Starting test_init");
+ importScripts("resource://gre/modules/osfile.jsm");
+}
+
+function test_OpenClose() {
+ info("Starting test_OpenClose");
+ is(typeof OS.Win.File.CreateFile, "function", "OS.Win.File.CreateFile is a function");
+ is(OS.Win.File.CloseHandle(OS.Constants.Win.INVALID_HANDLE_VALUE), true, "CloseHandle returns true given the invalid handle");
+ is(OS.Win.File.FindClose(OS.Constants.Win.INVALID_HANDLE_VALUE), true, "FindClose returns true given the invalid handle");
+ isnot(OS.Constants.Win.GENERIC_READ, undefined, "GENERIC_READ exists");
+ isnot(OS.Constants.Win.FILE_SHARE_READ, undefined, "FILE_SHARE_READ exists");
+ isnot(OS.Constants.Win.FILE_ATTRIBUTE_NORMAL, undefined, "FILE_ATTRIBUTE_NORMAL exists");
+ let file = OS.Win.File.CreateFile(
+ "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js",
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null);
+ info("test_OpenClose: Passed open");
+ isnot(file, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_OpenClose: file opened");
+ let result = OS.Win.File.CloseHandle(file);
+ isnot(result, 0, "test_OpenClose: close succeeded");
+
+ file = OS.Win.File.CreateFile(
+ "\\I do not exist",
+ OS.Constants.Win.GENERIC_READ,
+ OS.Constants.Win.FILE_SHARE_READ,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null);
+ is(file, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_OpenClose: cannot open non-existing file");
+ is(ctypes.winLastError, OS.Constants.Win.ERROR_FILE_NOT_FOUND, "test_OpenClose: error is ERROR_FILE_NOT_FOUND");
+}
+
+function test_CreateFile()
+{
+ info("Starting test_CreateFile");
+ let file = OS.Win.File.CreateFile(
+ "test.tmp",
+ OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE,
+ OS.Constants.Win.FILE_SHARE_READ | OS.Constants.FILE_SHARE_WRITE,
+ null,
+ OS.Constants.Win.CREATE_ALWAYS,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null);
+ isnot(file, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_CreateFile: opening succeeded");
+ let result = OS.Win.File.CloseHandle(file);
+ isnot(result, 0, "test_CreateFile: close succeeded");
+}
+
+function test_GetCurrentDirectory()
+{
+ let array = new (ctypes.ArrayType(ctypes.char16_t, 4096))();
+ let result = OS.Win.File.GetCurrentDirectory(4096, array);
+ ok(result < array.length, "test_GetCurrentDirectory: length sufficient");
+ ok(result > 0, "test_GetCurrentDirectory: length != 0");
+}
+
+function test_ReadWrite()
+{
+ info("Starting test_ReadWrite");
+ let output_name = "osfile_copy.tmp";
+ // Copy file
+ let input = OS.Win.File.CreateFile(
+ "chrome\\toolkit\\components\\osfile\\tests\\mochi\\worker_test_osfile_win.js",
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null);
+ isnot(input, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_ReadWrite: input file opened");
+ let output = OS.Win.File.CreateFile(
+ "osfile_copy.tmp",
+ OS.Constants.Win.GENERIC_READ | OS.Constants.Win.GENERIC_WRITE,
+ 0,
+ null,
+ OS.Constants.Win.CREATE_ALWAYS,
+ OS.Constants.Win.FILE_ATTRIBUTE_NORMAL,
+ null);
+ isnot(output, OS.Constants.Win.INVALID_HANDLE_VALUE, "test_ReadWrite: output file opened");
+ let array = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes_read = new ctypes.uint32_t(0);
+ let bytes_read_ptr = bytes_read.address();
+ log("We have a pointer for bytes read: "+bytes_read_ptr);
+ let bytes_written = new ctypes.uint32_t(0);
+ let bytes_written_ptr = bytes_written.address();
+ log("We have a pointer for bytes written: "+bytes_written_ptr);
+ log("test_ReadWrite: buffer and pointers ready");
+ let result;
+ while (true) {
+ log("test_ReadWrite: reading");
+ result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null);
+ isnot (result, 0, "test_ReadWrite: read success");
+ let write_from = 0;
+ let bytes_left = bytes_read;
+ log("test_ReadWrite: read chunk complete " + bytes_left.value);
+ if (bytes_left.value == 0) {
+ break;
+ }
+ while (bytes_left.value > 0) {
+ log("test_ReadWrite: writing "+bytes_left.value);
+ let ptr = array.addressOfElement(write_from);
+ // Note: |WriteFile| launches an exception in case of error
+ result = OS.Win.File.WriteFile(output, array, bytes_left, bytes_written_ptr, null);
+ isnot (result, 0, "test_ReadWrite: write success");
+ write_from += bytes_written;
+ bytes_left -= bytes_written;
+ }
+ }
+ info("test_ReadWrite: copy complete");
+
+ // Compare files
+ result = OS.Win.File.SetFilePointer(input, 0, null, OS.Constants.Win.FILE_BEGIN);
+ isnot (result, OS.Constants.Win.INVALID_SET_FILE_POINTER, "test_ReadWrite: input reset");
+
+ result = OS.Win.File.SetFilePointer(output, 0, null, OS.Constants.Win.FILE_BEGIN);
+ isnot (result, OS.Constants.Win.INVALID_SET_FILE_POINTER, "test_ReadWrite: output reset");
+
+ let array2 = new (ctypes.ArrayType(ctypes.char, 4096))();
+ let bytes_read2 = new ctypes.uint32_t(0);
+ let bytes_read2_ptr = bytes_read2.address();
+ let pos = 0;
+ while (true) {
+ result = OS.Win.File.ReadFile(input, array, 4096, bytes_read_ptr, null);
+ isnot(result, 0, "test_ReadWrite: input read succeeded");
+
+ result = OS.Win.File.ReadFile(output, array2, 4096, bytes_read2_ptr, null);
+ isnot(result, 0, "test_ReadWrite: output read succeeded");
+
+ is(bytes_read.value > 0, bytes_read2.value > 0,
+ "Both files contain data or neither does " + bytes_read.value + ", " + bytes_read2.value);
+ if (bytes_read.value == 0) {
+ break;
+ }
+ let bytes;
+ if (bytes_read.value != bytes_read2.value) {
+ // This would be surprising, but theoretically possible with a
+ // remote file system, I believe.
+ bytes = Math.min(bytes_read.value, bytes_read2.value);
+ pos += bytes;
+ result = OS.Win.File.SetFilePointer(input, pos, null, OS.Constants.Win.FILE_BEGIN);
+ isnot(result, 0, "test_ReadWrite: input seek succeeded");
+
+ result = OS.Win.File.SetFilePointer(output, pos, null, OS.Constants.Win.FILE_BEGIN);
+ isnot(result, 0, "test_ReadWrite: output seek succeeded");
+
+ } else {
+ bytes = bytes_read.value;
+ pos += bytes;
+ }
+ for (let i = 0; i < bytes; ++i) {
+ if (array[i] != array2[i]) {
+ ok(false, "Files do not match at position " + i
+ + " ("+array[i] + "/"+array2[i] + ")");
+ }
+ }
+ }
+ info("test_ReadWrite test complete");
+ result = OS.Win.File.CloseHandle(input);
+ isnot(result, 0, "test_ReadWrite: inpout close succeeded");
+ result = OS.Win.File.CloseHandle(output);
+ isnot(result, 0, "test_ReadWrite: outpout close succeeded");
+ result = OS.Win.File.DeleteFile(output_name);
+ isnot(result, 0, "test_ReadWrite: output remove succeeded");
+ info("test_ReadWrite cleanup complete");
+}
+
+function test_passing_undefined()
+{
+ info("Testing that an exception gets thrown when an FFI function is passed undefined");
+ let exceptionRaised = false;
+
+ try {
+ let file = OS.Win.File.CreateFile(
+ undefined,
+ OS.Constants.Win.GENERIC_READ,
+ 0,
+ null,
+ OS.Constants.Win.OPEN_EXISTING,
+ 0,
+ null);
+ } catch(e if e instanceof TypeError && e.message.indexOf("CreateFile") > -1) {
+ exceptionRaised = true;
+ }
+
+ ok(exceptionRaised, "test_passing_undefined: exception gets thrown")
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/.eslintrc.js b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/osfile/tests/xpcshell/head.js b/toolkit/components/osfile/tests/xpcshell/head.js
new file mode 100644
index 000000000..eef29962a
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/head.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu, interfaces: Ci} = Components;
+
+var {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+// Bug 1014484 can only be reproduced by loading OS.File first from the
+// CommonJS loader, so we do not want OS.File to be loaded eagerly for
+// all the tests in this directory.
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+var {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+var {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+
+Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+/**
+ * As add_task, but execute the test both with native operations and
+ * without.
+ */
+function add_test_pair(generator) {
+ add_task(function*() {
+ do_print("Executing test " + generator.name + " with native operations");
+ Services.prefs.setBoolPref("toolkit.osfile.native", true);
+ return Task.spawn(generator);
+ });
+ add_task(function*() {
+ do_print("Executing test " + generator.name + " without native operations");
+ Services.prefs.setBoolPref("toolkit.osfile.native", false);
+ return Task.spawn(generator);
+ });
+}
+
+/**
+ * Fetch asynchronously the contents of a file using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} path The _absolute_ path to the file.
+ * @return {promise}
+ * @resolves {string} The contents of the file.
+ */
+function reference_fetch_file(path, test) {
+ do_print("Fetching file " + path);
+ let deferred = Promise.defer();
+ let file = new FileUtils.File(path);
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function(stream, status) {
+ if (!Components.isSuccessCode(status)) {
+ deferred.reject(status);
+ return;
+ }
+ let result, reject;
+ try {
+ result = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (x) {
+ reject = x;
+ }
+ stream.close();
+ if (reject) {
+ deferred.reject(reject);
+ } else {
+ deferred.resolve(result);
+ }
+ });
+
+ return deferred.promise;
+};
+
+/**
+ * Compare asynchronously the contents two files using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} a The _absolute_ path to the first file.
+ * @param {string} b The _absolute_ path to the second file.
+ *
+ * @resolves {null}
+ */
+function reference_compare_files(a, b, test) {
+ return Task.spawn(function*() {
+ do_print("Comparing files " + a + " and " + b);
+ let a_contents = yield reference_fetch_file(a, test);
+ let b_contents = yield reference_fetch_file(b, test);
+ do_check_eq(a_contents, b_contents);
+ });
+};
diff --git a/toolkit/components/osfile/tests/xpcshell/test_available_free_space.js b/toolkit/components/osfile/tests/xpcshell/test_available_free_space.js
new file mode 100644
index 000000000..08e67763b
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_available_free_space.js
@@ -0,0 +1,38 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+ run_next_test();
+}
+
+/**
+ * Test OS.File.getAvailableFreeSpace
+ */
+add_task(function() {
+ // Set up profile. We will use profile path to query for available free
+ // space.
+ do_get_profile();
+
+ let dir = OS.Constants.Path.profileDir;
+
+ // Sanity checking for the test
+ do_check_true((yield OS.File.exists(dir)));
+
+ // Query for available bytes for user
+ let availableBytes = yield OS.File.getAvailableFreeSpace(dir);
+
+ do_check_true(!!availableBytes);
+ do_check_true(availableBytes > 0);
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_compression.js b/toolkit/components/osfile/tests/xpcshell/test_compression.js
new file mode 100644
index 000000000..b40235615
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_compression.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+add_task(function test_compress_lz4() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "compression.lz");
+ let length = 1024;
+ let array = new Uint8Array(length);
+ for (let i = 0; i < array.byteLength; ++i) {
+ array[i] = i;
+ }
+ let arrayAsString = Array.prototype.join.call(array);
+
+ do_print("Writing data with lz4 compression");
+ let bytes = yield OS.File.writeAtomic(path, array, { compression: "lz4" });
+ do_print("Compressed " + length + " bytes into " + bytes);
+
+ do_print("Reading back with lz4 decompression");
+ let decompressed = yield OS.File.read(path, { compression: "lz4" });
+ do_print("Decompressed into " + decompressed.byteLength + " bytes");
+ do_check_eq(arrayAsString, Array.prototype.join.call(decompressed));
+});
+
+add_task(function test_uncompressed() {
+ do_print("Writing data without compression");
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_compression.tmp");
+ let array = new Uint8Array(1024);
+ for (let i = 0; i < array.byteLength; ++i) {
+ array[i] = i;
+ }
+ let bytes = yield OS.File.writeAtomic(path, array); // No compression
+
+ let exn;
+ // Force decompression, reading should fail
+ try {
+ yield OS.File.read(path, { compression: "lz4" });
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ // Check the exception message (and that it contains the file name)
+ do_check_true(exn.message.indexOf(`Invalid header (no magic number) - Data: ${ path }`) != -1);
+});
+
+add_task(function test_no_header() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "no_header.tmp");
+ let array = new Uint8Array(8).fill(0,0); // Small array with no header
+
+ do_print("Writing data with no header");
+
+ let bytes = yield OS.File.writeAtomic(path, array); // No compression
+ let exn;
+ // Force decompression, reading should fail
+ try {
+ yield OS.File.read(path, { compression: "lz4" });
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ // Check the exception message (and that it contains the file name)
+ do_check_true(exn.message.indexOf(`Buffer is too short (no header) - Data: ${ path }`) != -1);
+});
+
+add_task(function test_invalid_content() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir, "invalid_content.tmp");
+ let arr1 = new Uint8Array([109, 111, 122, 76, 122, 52, 48, 0]);
+ let arr2 = new Uint8Array(248).fill(1,0);
+
+ let array = new Uint8Array(arr1.length + arr2.length);
+ array.set(arr1);
+ array.set(arr2, arr1.length);
+
+ do_print("Writing invalid data (with a valid header and only ones after that)");
+
+ let bytes = yield OS.File.writeAtomic(path, array); // No compression
+ let exn;
+ // Force decompression, reading should fail
+ try {
+ yield OS.File.read(path, { compression: "lz4" });
+ } catch (ex) {
+ exn = ex;
+ }
+ do_check_true(!!exn);
+ // Check the exception message (and that it contains the file name)
+ do_check_true(exn.message.indexOf(`Invalid content: Decompression stopped at 0 - Data: ${ path }`) != -1);
+});
+
+add_task(function() {
+ do_test_finished();
+}); \ No newline at end of file
diff --git a/toolkit/components/osfile/tests/xpcshell/test_constants.js b/toolkit/components/osfile/tests/xpcshell/test_constants.js
new file mode 100644
index 000000000..e92f33ab8
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_constants.js
@@ -0,0 +1,31 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+
+function run_test() {
+ run_next_test();
+}
+
+// Test that OS.Constants is defined correctly.
+add_task(function* check_definition() {
+ do_check_true(OS.Constants!=null);
+ do_check_true(!!OS.Constants.Win || !!OS.Constants.libc);
+ do_check_true(OS.Constants.Path!=null);
+ do_check_true(OS.Constants.Sys!=null);
+ //check system name
+ if (OS.Constants.Sys.Name == "Gonk") {
+ // Services.appinfo.OS doesn't know the difference between Gonk and Android
+ do_check_eq(Services.appinfo.OS, "Android");
+ } else {
+ do_check_eq(Services.appinfo.OS, OS.Constants.Sys.Name);
+ }
+
+ //check if using DEBUG build
+ if (Components.classes["@mozilla.org/xpcom/debug;1"].getService(Components.interfaces.nsIDebug2).isDebugBuild == true) {
+ do_check_true(OS.Constants.Sys.DEBUG);
+ } else {
+ do_check_true(typeof(OS.Constants.Sys.DEBUG) == 'undefined');
+ }
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_creationDate.js b/toolkit/components/osfile/tests/xpcshell/test_creationDate.js
new file mode 100644
index 000000000..9c4fa1dfc
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_creationDate.js
@@ -0,0 +1,31 @@
+"use strict";
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * Test to ensure that deprecation warning is issued on use
+ * of creationDate.
+ */
+add_task(function test_deprecatedCreationDate () {
+ let deferred = Promise.defer();
+ let consoleListener = {
+ observe: function (aMessage) {
+ if(aMessage.message.indexOf("Field 'creationDate' is deprecated.") > -1) {
+ do_print("Deprecation message printed");
+ do_check_true(true);
+ Services.console.unregisterListener(consoleListener);
+ deferred.resolve();
+ }
+ }
+ };
+ let currentDir = yield OS.File.getCurrentDirectory();
+ let path = OS.Path.join(currentDir, "test_creationDate.js");
+
+ Services.console.registerListener(consoleListener);
+ (yield OS.File.stat(path)).creationDate;
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_duration.js b/toolkit/components/osfile/tests/xpcshell/test_duration.js
new file mode 100644
index 000000000..305c03da8
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_duration.js
@@ -0,0 +1,91 @@
+var {OS} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+var {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
+
+/**
+ * Test optional duration reporting that can be used for telemetry.
+ */
+add_task(function* duration() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+ // Options structure passed to a OS.File copy method.
+ let copyOptions = {
+ // This field should be overridden with the actual duration
+ // measurement.
+ outExecutionDuration: null
+ };
+ let currentDir = yield OS.File.getCurrentDirectory();
+ let pathSource = OS.Path.join(currentDir, "test_duration.js");
+ let copyFile = pathSource + ".bak";
+ function testOptions(options, name) {
+ do_print("Checking outExecutionDuration for operation: " + name);
+ do_print(name + ": Gathered method duration time: " +
+ options.outExecutionDuration + "ms");
+ // Making sure that duration was updated.
+ do_check_eq(typeof options.outExecutionDuration, "number");
+ do_check_true(options.outExecutionDuration >= 0);
+ };
+ // Testing duration of OS.File.copy.
+ yield OS.File.copy(pathSource, copyFile, copyOptions);
+ testOptions(copyOptions, "OS.File.copy");
+ yield OS.File.remove(copyFile);
+
+ // Trying an operation where options are cloned.
+ let pathDest = OS.Path.join(OS.Constants.Path.tmpDir,
+ "osfile async test read writeAtomic.tmp");
+ let tmpPath = pathDest + ".tmp";
+ let readOptions = {
+ outExecutionDuration: null
+ };
+ let contents = yield OS.File.read(pathSource, undefined, readOptions);
+ testOptions(readOptions, "OS.File.read");
+ // Options structure passed to a OS.File writeAtomic method.
+ let writeAtomicOptions = {
+ // This field should be first initialized with the actual
+ // duration measurement then progressively incremented.
+ outExecutionDuration: null,
+ tmpPath: tmpPath
+ };
+ yield OS.File.writeAtomic(pathDest, contents, writeAtomicOptions);
+ testOptions(writeAtomicOptions, "OS.File.writeAtomic");
+ yield OS.File.remove(pathDest);
+
+ do_print("Ensuring that we can use outExecutionDuration to accumulate durations");
+
+ let ARBITRARY_BASE_DURATION = 5;
+ copyOptions = {
+ // This field should now be incremented with the actual duration
+ // measurement.
+ outExecutionDuration: ARBITRARY_BASE_DURATION
+ };
+ let backupDuration = ARBITRARY_BASE_DURATION;
+ // Testing duration of OS.File.copy.
+ yield OS.File.copy(pathSource, copyFile, copyOptions);
+
+ do_check_true(copyOptions.outExecutionDuration >= backupDuration);
+
+ backupDuration = copyOptions.outExecutionDuration;
+ yield OS.File.remove(copyFile, copyOptions);
+ do_check_true(copyOptions.outExecutionDuration >= backupDuration);
+
+ // Trying an operation where options are cloned.
+ // Options structure passed to a OS.File writeAtomic method.
+ writeAtomicOptions = {
+ // This field should be overridden with the actual duration
+ // measurement.
+ outExecutionDuration: copyOptions.outExecutionDuration,
+ tmpPath: tmpPath
+ };
+ backupDuration = writeAtomicOptions.outExecutionDuration;
+
+ yield OS.File.writeAtomic(pathDest, contents, writeAtomicOptions);
+ do_check_true(copyOptions.outExecutionDuration >= backupDuration);
+ OS.File.remove(pathDest);
+
+ // Testing an operation that doesn't take arguments at all
+ let file = yield OS.File.open(pathSource);
+ yield file.stat();
+ yield file.close();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_exception.js b/toolkit/components/osfile/tests/xpcshell/test_exception.js
new file mode 100644
index 000000000..1282adb3e
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_exception.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that functions throw the appropriate exceptions.
+ */
+
+"use strict";
+
+var EXISTING_FILE = do_get_file("xpcshell.ini").path;
+
+
+// Tests on |open|
+
+add_test_pair(function test_typeerror() {
+ let exn;
+ try {
+ let fd = yield OS.File.open("/tmp", {no_such_key: 1});
+ do_print("Fd: " + fd);
+ } catch (ex) {
+ exn = ex;
+ }
+ do_print("Exception: " + exn);
+ do_check_true(exn.constructor.name == "TypeError");
+});
+
+// Tests on |read|
+
+add_test_pair(function* test_bad_encoding() {
+ do_print("Testing with a wrong encoding");
+ try {
+ yield OS.File.read(EXISTING_FILE, { encoding: "baby-speak-encoded" });
+ do_throw("Should have thrown with an ex.becauseInvalidArgument");
+ } catch (ex if ex.becauseInvalidArgument) {
+ do_print("Wrong encoding caused the correct exception");
+ }
+
+ try {
+ yield OS.File.read(EXISTING_FILE, { encoding: 4 });
+ do_throw("Should have thrown a TypeError");
+ } catch (ex if ex.constructor.name == "TypeError") {
+ // Note that TypeError doesn't carry across compartments
+ do_print("Non-string encoding caused the correct exception");
+ }
+ });
+
+add_test_pair(function* test_bad_compression() {
+ do_print("Testing with a non-existing compression");
+ try {
+ yield OS.File.read(EXISTING_FILE, { compression: "mmmh-crunchy" });
+ do_throw("Should have thrown with an ex.becauseInvalidArgument");
+ } catch (ex if ex.becauseInvalidArgument) {
+ do_print("Wrong encoding caused the correct exception");
+ }
+
+ do_print("Testing with a bad type for option compression");
+ try {
+ yield OS.File.read(EXISTING_FILE, { compression: 5 });
+ do_throw("Should have thrown a TypeError");
+ } catch (ex if ex.constructor.name == "TypeError") {
+ // Note that TypeError doesn't carry across compartments
+ do_print("Non-string encoding caused the correct exception");
+ }
+});
+
+add_test_pair(function* test_bad_bytes() {
+ do_print("Testing with a bad type for option bytes");
+ try {
+ yield OS.File.read(EXISTING_FILE, { bytes: "five" });
+ do_throw("Should have thrown a TypeError");
+ } catch (ex if ex.constructor.name == "TypeError") {
+ // Note that TypeError doesn't carry across compartments
+ do_print("Non-number bytes caused the correct exception");
+ }
+});
+
+add_test_pair(function* read_non_existent() {
+ do_print("Testing with a non-existent file");
+ try {
+ yield OS.File.read("I/do/not/exist");
+ do_throw("Should have thrown with an ex.becauseNoSuchFile");
+ } catch (ex if ex.becauseNoSuchFile) {
+ do_print("Correct exceptions");
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js
new file mode 100644
index 000000000..3ec42065b
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_file_URL_conversion.js
@@ -0,0 +1,114 @@
+/* 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/. */
+
+function run_test() {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Components.utils.import("resource://gre/modules/osfile.jsm");
+ Components.utils.import("resource://gre/modules/FileUtils.jsm");
+
+ let isWindows = ("@mozilla.org/windows-registry-key;1" in Components.classes);
+
+ // Test cases for filePathToURI
+ let paths = isWindows ? [
+ 'C:\\',
+ 'C:\\test',
+ 'C:\\test\\',
+ 'C:\\test%2f',
+ 'C:\\test\\test\\test',
+ 'C:\\test;+%',
+ 'C:\\test?action=index\\',
+ 'C:\\test\ test',
+ '\\\\C:\\a\\b\\c',
+ '\\\\Server\\a\\b\\c',
+
+ // note that per http://support.microsoft.com/kb/177506 (under more info),
+ // the following characters are allowed on Windows:
+ 'C:\\char^',
+ 'C:\\char&',
+ 'C:\\char\'',
+ 'C:\\char@',
+ 'C:\\char{',
+ 'C:\\char}',
+ 'C:\\char[',
+ 'C:\\char]',
+ 'C:\\char,',
+ 'C:\\char$',
+ 'C:\\char=',
+ 'C:\\char!',
+ 'C:\\char-',
+ 'C:\\char#',
+ 'C:\\char(',
+ 'C:\\char)',
+ 'C:\\char%',
+ 'C:\\char.',
+ 'C:\\char+',
+ 'C:\\char~',
+ 'C:\\char_'
+ ] : [
+ '/',
+ '/test',
+ '/test/',
+ '/test%2f',
+ '/test/test/test',
+ '/test;+%',
+ '/test?action=index/',
+ '/test\ test',
+ '/punctuation/;,/?:@&=+$-_.!~*\'()[]"#',
+ '/CasePreserving'
+ ];
+
+ // some additional URIs to test, beyond those generated from paths
+ let uris = isWindows ? [
+ 'file:///C:/test/',
+ 'file://localhost/C:/test',
+ 'file:///c:/test/test.txt',
+ //'file:///C:/foo%2f', // trailing, encoded slash
+ 'file:///C:/%3f%3F',
+ 'file:///C:/%3b%3B',
+ 'file:///C:/%3c%3C', // not one of the special-cased ? or ;
+ 'file:///C:/%78', // 'x', not usually uri encoded
+ 'file:///C:/test#frag', // a fragment identifier
+ 'file:///C:/test?action=index' // an actual query component
+ ] : [
+ 'file:///test/',
+ 'file://localhost/test',
+ 'file:///test/test.txt',
+ 'file:///foo%2f', // trailing, encoded slash
+ 'file:///%3f%3F',
+ 'file:///%3b%3B',
+ 'file:///%3c%3C', // not one of the special-cased ? or ;
+ 'file:///%78', // 'x', not usually uri encoded
+ 'file:///test#frag', // a fragment identifier
+ 'file:///test?action=index' // an actual query component
+ ];
+
+ for (let path of paths) {
+ // convert that to a uri using FileUtils and Services, which toFileURI is trying to model
+ let file = FileUtils.File(path);
+ let uri = Services.io.newFileURI(file).spec;
+ do_check_eq(uri, OS.Path.toFileURI(path));
+
+ // keep the resulting URI to try the reverse, except for "C:\" for which the
+ // behavior of nsIFileURL and OS.File is inconsistent
+ if (path != "C:\\") {
+ uris.push(uri);
+ }
+ }
+
+ for (let uri of uris) {
+ // convert URIs to paths with nsIFileURI, which fromFileURI is trying to model
+ let path = Services.io.newURI(uri, null, null).QueryInterface(Components.interfaces.nsIFileURL).file.path;
+ do_check_eq(path, OS.Path.fromFileURI(uri));
+ }
+
+ // check that non-file URLs aren't allowed
+ let thrown = false;
+ try {
+ OS.Path.fromFileURI('http://test.com')
+ } catch (e) {
+ do_check_eq(e.message, "fromFileURI expects a file URI");
+ thrown = true;
+ }
+ do_check_true(thrown);
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_loader.js b/toolkit/components/osfile/tests/xpcshell/test_loader.js
new file mode 100644
index 000000000..dcfa819be
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_loader.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test that OS.File can be loaded using the CommonJS loader.
+ */
+
+var { Loader } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js', {});
+
+function run_test() {
+ run_next_test();
+}
+
+
+add_task(function*() {
+ let dataDir = Services.io.newFileURI(do_get_file("test_loader/", true)).spec + "/";
+ let loader = Loader.Loader({
+ paths: {'': dataDir }
+ });
+
+ let require = Loader.Require(loader, Loader.Module('module_test_loader', 'foo'));
+ do_print("Require is ready");
+ try {
+ require('module_test_loader');
+ } catch (error) {
+ dump('Bootstrap error: ' +
+ (error.message ? error.message : String(error)) + '\n' +
+ (error.stack || error.fileName + ': ' + error.lineNumber) + '\n');
+
+ throw error;
+ }
+
+ do_print("Require has worked");
+});
+
diff --git a/toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js b/toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js
new file mode 100644
index 000000000..18356d6ad
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_loader/module_test_loader.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Load OS.File from a module loaded with the CommonJS/addon-sdk loader
+
+var {Cu} = require("chrome");
+Cu.import('resource://gre/modules/osfile.jsm');
diff --git a/toolkit/components/osfile/tests/xpcshell/test_logging.js b/toolkit/components/osfile/tests/xpcshell/test_logging.js
new file mode 100644
index 000000000..133909e0b
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_logging.js
@@ -0,0 +1,74 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * Tests logging by passing OS.Shared.LOG both an object with its own
+ * toString method, and one with the default.
+ */
+function run_test() {
+ do_test_pending();
+ let messageCount = 0;
+
+ do_print("Test starting");
+
+ // Create a console listener.
+ let consoleListener = {
+ observe: function (aMessage) {
+ //Ignore unexpected messages.
+ if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
+ return;
+ }
+ // This is required, as printing to the |Services.console|
+ // while in the observe function causes an exception.
+ do_execute_soon(function() {
+ do_print("Observing message " + aMessage.message);
+ if (aMessage.message.indexOf("TEST OS") < 0) {
+ return;
+ }
+
+ ++messageCount;
+ if(messageCount == 1) {
+ do_check_eq(aMessage.message, "TEST OS {\"name\":\"test\"}\n");
+ }
+ if(messageCount == 2) {
+ do_check_eq(aMessage.message, "TEST OS name is test\n");
+ toggleConsoleListener(false);
+ do_test_finished();
+ }
+ });
+ }
+ };
+
+ // Set/Unset the console listener.
+ function toggleConsoleListener (pref) {
+ do_print("Setting console listener: " + pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log", pref);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", pref);
+ Services.console[pref ? "registerListener" : "unregisterListener"](
+ consoleListener);
+ }
+
+ toggleConsoleListener(true);
+
+ let objectDefault = {name: "test"};
+ let CustomToString = function() {
+ this.name = "test";
+ };
+ CustomToString.prototype.toString = function() {
+ return "name is " + this.name;
+ };
+ let objectCustom = new CustomToString();
+
+ do_print(OS.Shared.LOG.toSource());
+
+ do_print("Logging 1");
+ OS.Shared.LOG(objectDefault);
+
+ do_print("Logging 2");
+ OS.Shared.LOG(objectCustom);
+ // Once both messages are observed OS.Shared.DEBUG, and OS.Shared.TEST
+ // are reset to false.
+}
+
diff --git a/toolkit/components/osfile/tests/xpcshell/test_makeDir.js b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js
new file mode 100644
index 000000000..5b9740a7f
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_makeDir.js
@@ -0,0 +1,142 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var Path = OS.Path;
+var profileDir;
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * Test OS.File.makeDir
+ */
+
+add_task(function init() {
+ // Set up profile. We create the directory in the profile, because the profile
+ // is removed after every test run.
+ do_get_profile();
+ profileDir = OS.Constants.Path.profileDir;
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+});
+
+/**
+ * Basic use
+ */
+
+add_task(function* test_basic() {
+ let dir = Path.join(profileDir, "directory");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Make a directory
+ yield OS.File.makeDir(dir);
+
+ //check if the directory exists
+ yield OS.File.stat(dir);
+
+ // Make a directory that already exists, this should succeed
+ yield OS.File.makeDir(dir);
+
+ // Make a directory with ignoreExisting
+ yield OS.File.makeDir(dir, {ignoreExisting: true});
+
+ // Make a directory with ignoreExisting false
+ let exception = null;
+ try {
+ yield OS.File.makeDir(dir, {ignoreExisting: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseExists);
+});
+
+// Make a root directory that already exists
+add_task(function* test_root() {
+ if (OS.Constants.Win) {
+ yield OS.File.makeDir("C:");
+ yield OS.File.makeDir("C:\\");
+ } else {
+ yield OS.File.makeDir("/");
+ }
+});
+
+/**
+ * Creating subdirectories
+ */
+add_task(function test_option_from() {
+ let dir = Path.join(profileDir, "a", "b", "c");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Make a directory
+ yield OS.File.makeDir(dir, {from: profileDir});
+
+ //check if the directory exists
+ yield OS.File.stat(dir);
+
+ // Make a directory that already exists, this should succeed
+ yield OS.File.makeDir(dir);
+
+ // Make a directory with ignoreExisting
+ yield OS.File.makeDir(dir, {ignoreExisting: true});
+
+ // Make a directory with ignoreExisting false
+ let exception = null;
+ try {
+ yield OS.File.makeDir(dir, {ignoreExisting: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseExists);
+
+ // Make a directory without |from| and fail
+ let dir2 = Path.join(profileDir, "g", "h", "i");
+ exception = null;
+ try {
+ yield OS.File.makeDir(dir2);
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseNoSuchFile);
+
+ // Test edge cases on paths
+
+ let dir3 = Path.join(profileDir, "d", "", "e", "f");
+ do_check_false((yield OS.File.exists(dir3)));
+ yield OS.File.makeDir(dir3, {from: profileDir});
+ do_check_true((yield OS.File.exists(dir3)));
+
+ let dir4;
+ if (OS.Constants.Win) {
+ // Test that we can create a directory recursively even
+ // if we have too many "\\".
+ dir4 = profileDir + "\\\\g";
+ } else {
+ dir4 = profileDir + "////g";
+ }
+ do_check_false((yield OS.File.exists(dir4)));
+ yield OS.File.makeDir(dir4, {from: profileDir});
+ do_check_true((yield OS.File.exists(dir4)));
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_open.js b/toolkit/components/osfile/tests/xpcshell/test_open.js
new file mode 100644
index 000000000..78772ad09
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_open.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+/**
+ * Test OS.File.open for reading:
+ * - with an existing file (should succeed);
+ * - with a non-existing file (should fail);
+ * - with inconsistent arguments (should fail).
+ */
+add_task(function() {
+ // Attempt to open a file that does not exist, ensure that it yields the
+ // appropriate error.
+ try {
+ let fd = yield OS.File.open(OS.Path.join(".", "This file does not exist"));
+ do_check_true(false, "File opening 1 succeeded (it should fail)");
+ } catch (err if err instanceof OS.File.Error && err.becauseNoSuchFile) {
+ do_print("File opening 1 failed " + err);
+ }
+
+ // Attempt to open a file with the wrong args, so that it fails before
+ // serialization, ensure that it yields the appropriate error.
+ do_print("Attempting to open a file with wrong arguments");
+ try {
+ let fd = yield OS.File.open(1, 2, 3);
+ do_check_true(false, "File opening 2 succeeded (it should fail)" + fd);
+ } catch (err) {
+ do_print("File opening 2 failed " + err);
+ do_check_false(err instanceof OS.File.Error,
+ "File opening 2 returned something that is not a file error");
+ do_check_true(err.constructor.name == "TypeError",
+ "File opening 2 returned a TypeError");
+ }
+
+ // Attempt to open a file correctly
+ do_print("Attempting to open a file correctly");
+ let openedFile = yield OS.File.open(OS.Path.join(do_get_cwd().path, "test_open.js"));
+ do_print("File opened correctly");
+
+ do_print("Attempting to close a file correctly");
+ yield openedFile.close();
+
+ do_print("Attempting to close a file again");
+ yield openedFile.close();
+});
+
+/**
+ * Test the error thrown by OS.File.open when attempting to open a directory
+ * that does not exist.
+ */
+add_task(function test_error_attributes () {
+
+ let dir = OS.Path.join(do_get_profile().path, "test_osfileErrorAttrs");
+ let fpath = OS.Path.join(dir, "test_error_attributes.txt");
+
+ try {
+ yield OS.File.open(fpath, {truncate: true}, {});
+ do_check_true(false, "Opening path suceeded (it should fail) " + fpath);
+ } catch (err) {
+ do_check_true(err instanceof OS.File.Error);
+ do_check_true(err.becauseNoSuchFile);
+ }
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js
new file mode 100644
index 000000000..0f86b2ea8
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async.js
@@ -0,0 +1,16 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+/**
+ * A trivial test ensuring that we can call osfile from xpcshell.
+ * (see bug 808161)
+ */
+
+function run_test() {
+ do_test_pending();
+ OS.File.getCurrentDirectory().then(
+ do_test_finished,
+ do_test_finished
+ );
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js
new file mode 100644
index 000000000..0aef2c58a
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_append.js
@@ -0,0 +1,122 @@
+"use strict";
+
+do_print("starting tests");
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * A test to check that the |append| mode flag is correctly implemented.
+ * (see bug 925865)
+ */
+
+function setup_mode(mode) {
+ // Complete mode.
+ let realMode = {
+ read: true,
+ write: true
+ };
+ for (let k in mode) {
+ realMode[k] = mode[k];
+ }
+ return realMode;
+}
+
+// Test append mode.
+function test_append(mode) {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_append.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ mode = setup_mode(mode);
+ mode.append = true;
+ if (mode.trunc) {
+ // Pre-fill file with some data to see if |trunc| actually works.
+ yield OS.File.writeAtomic(path, new Uint8Array(500));
+ }
+ let file = yield OS.File.open(path, mode);
+ try {
+ yield file.write(new Uint8Array(1000));
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.read(100);
+ // Should be at offset 100, length 1000 now.
+ yield file.write(new Uint8Array(100));
+ // Should be at offset 1100, length 1100 now.
+ let stat = yield file.stat();
+ do_check_eq(1100, stat.size);
+ } finally {
+ yield file.close();
+ }
+ } catch(ex) {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ }
+}
+
+// Test no-append mode.
+function test_no_append(mode) {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_noappend.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ mode = setup_mode(mode);
+ mode.append = false;
+ if (mode.trunc) {
+ // Pre-fill file with some data to see if |trunc| actually works.
+ yield OS.File.writeAtomic(path, new Uint8Array(500));
+ }
+ let file = yield OS.File.open(path, mode);
+ try {
+ yield file.write(new Uint8Array(1000));
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.read(100);
+ // Should be at offset 100, length 1000 now.
+ yield file.write(new Uint8Array(100));
+ // Should be at offset 200, length 1000 now.
+ let stat = yield file.stat();
+ do_check_eq(1000, stat.size);
+ } finally {
+ yield file.close();
+ }
+ } finally {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ }
+}
+
+var test_flags = [
+ {},
+ {create:true},
+ {trunc:true}
+];
+function run_test() {
+ do_test_pending();
+
+ for (let t of test_flags) {
+ add_task(test_append.bind(null, t));
+ add_task(test_no_append.bind(null, t));
+ }
+ add_task(do_test_finished);
+
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js
new file mode 100644
index 000000000..68fa9152c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_bytes.js
@@ -0,0 +1,39 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * Test to ensure that {bytes:} in options to |write| is correctly
+ * preserved.
+ */
+add_task(function* test_bytes() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_bytes.tmp");
+ let file = yield OS.File.open(path, {trunc: true, read: true, write: true});
+ try {
+ try {
+ // 1. Test write, by supplying {bytes:} options smaller than the actual
+ // buffer.
+ yield file.write(new Uint8Array(2048), {bytes: 1024});
+ do_check_eq((yield file.stat()).size, 1024);
+
+ // 2. Test that passing nullish values for |options| still works.
+ yield file.setPosition(0, OS.File.POS_END);
+ yield file.write(new Uint8Array(1024), null);
+ yield file.write(new Uint8Array(1024), undefined);
+ do_check_eq((yield file.stat()).size, 3072);
+ } finally {
+ yield file.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js
new file mode 100644
index 000000000..9c52c8a80
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_copy.js
@@ -0,0 +1,113 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * A file that we know exists and that can be used for reading.
+ */
+var EXISTING_FILE = "test_osfile_async_copy.js";
+
+/**
+ * Fetch asynchronously the contents of a file using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} path The _absolute_ path to the file.
+ * @return {promise}
+ * @resolves {string} The contents of the file.
+ */
+var reference_fetch_file = function reference_fetch_file(path) {
+ let promise = Promise.defer();
+ let file = new FileUtils.File(path);
+ NetUtil.asyncFetch({
+ uri: NetUtil.newURI(file),
+ loadUsingSystemPrincipal: true
+ }, function(stream, status) {
+ if (!Components.isSuccessCode(status)) {
+ promise.reject(status);
+ return;
+ }
+ let result, reject;
+ try {
+ result = NetUtil.readInputStreamToString(stream, stream.available());
+ } catch (x) {
+ reject = x;
+ }
+ stream.close();
+ if (reject) {
+ promise.reject(reject);
+ } else {
+ promise.resolve(result);
+ }
+ });
+
+ return promise.promise;
+};
+
+/**
+ * Compare asynchronously the contents two files using xpcom.
+ *
+ * Used for comparing xpcom-based results to os.file-based results.
+ *
+ * @param {string} a The _absolute_ path to the first file.
+ * @param {string} b The _absolute_ path to the second file.
+ *
+ * @resolves {null}
+ */
+var reference_compare_files = function reference_compare_files(a, b) {
+ let a_contents = yield reference_fetch_file(a);
+ let b_contents = yield reference_fetch_file(b);
+ // Not using do_check_eq to avoid dumping the whole file to the log.
+ // It is OK to === compare here, as both variables contain a string.
+ do_check_true(a_contents === b_contents);
+};
+
+/**
+ * Test to ensure that OS.File.copy works.
+ */
+function test_copymove(options = {}) {
+ let source = OS.Path.join((yield OS.File.getCurrentDirectory()),
+ EXISTING_FILE);
+ let dest = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_copy_dest.tmp");
+ let dest2 = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_copy_dest2.tmp");
+ try {
+ // 1. Test copy.
+ yield OS.File.copy(source, dest, options);
+ yield reference_compare_files(source, dest);
+ // 2. Test subsequent move.
+ yield OS.File.move(dest, dest2);
+ yield reference_compare_files(source, dest2);
+ // 3. Check that the moved file was really moved.
+ do_check_eq((yield OS.File.exists(dest)), false);
+ } finally {
+ try {
+ yield OS.File.remove(dest);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+ try {
+ yield OS.File.remove(dest2);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+ }
+}
+
+// Regular copy test.
+add_task(test_copymove);
+// Userland copy test.
+add_task(test_copymove.bind(null, {unixUserland: true}));
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js
new file mode 100644
index 000000000..9ed087f4e
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_flush.js
@@ -0,0 +1,30 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+/**
+ * Test to ensure that |File.prototype.flush| is available in the async API.
+ */
+
+add_task(function test_flush() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_flush.tmp");
+ let file = yield OS.File.open(path, {trunc: true, write: true});
+ try {
+ try {
+ yield file.flush();
+ } finally {
+ yield file.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js
new file mode 100644
index 000000000..a9ac776b0
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_largefiles.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/ctypes.jsm");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * A test to check that .getPosition/.setPosition work with large files.
+ * (see bug 952997)
+ */
+
+// Test setPosition/getPosition.
+function test_setPosition(forward, current, backward) {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_largefiles.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ let file = yield OS.File.open(path, {write:true, append:false});
+ try {
+ let pos = 0;
+
+ // 1. seek forward from start
+ do_print("Moving forward: " + forward);
+ yield file.setPosition(forward, OS.File.POS_START);
+ pos += forward;
+ do_check_eq((yield file.getPosition()), pos);
+
+ // 2. seek forward from current position
+ do_print("Moving current: " + current);
+ yield file.setPosition(current, OS.File.POS_CURRENT);
+ pos += current;
+ do_check_eq((yield file.getPosition()), pos);
+
+ // 3. seek backward from current position
+ do_print("Moving current backward: " + backward);
+ yield file.setPosition(-backward, OS.File.POS_CURRENT);
+ pos -= backward;
+ do_check_eq((yield file.getPosition()), pos);
+
+ } finally {
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.close();
+ }
+ } catch(ex) {
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ do_throw(ex);
+ }
+}
+
+// Test setPosition/getPosition expected failures.
+function test_setPosition_failures() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_largefiles.tmp");
+
+ // Clear any left-over files from previous runs.
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore
+ }
+
+ try {
+ let file = yield OS.File.open(path, {write:true, append:false});
+ try {
+ let pos = 0;
+
+ // 1. Use an invalid position value
+ try {
+ yield file.setPosition(0.5, OS.File.POS_START);
+ do_throw("Shouldn't have succeeded");
+ } catch (ex) {
+ do_check_true(ex.toString().includes("can't pass"));
+ }
+ // Since setPosition should have bailed, it shouldn't have moved the
+ // file pointer at all.
+ do_check_eq((yield file.getPosition()), 0);
+
+ // 2. Use an invalid position value
+ try {
+ yield file.setPosition(0xffffffff + 0.5, OS.File.POS_START);
+ do_throw("Shouldn't have succeeded");
+ } catch (ex) {
+ do_check_true(ex.toString().includes("can't pass"));
+ }
+ // Since setPosition should have bailed, it shouldn't have moved the
+ // file pointer at all.
+ do_check_eq((yield file.getPosition()), 0);
+
+ // 3. Use a position that cannot be represented as a double
+ try {
+ // Not all numbers after 9007199254740992 can be represented as a
+ // double. E.g. in js 9007199254740992 + 1 == 9007199254740992
+ yield file.setPosition(9007199254740992, OS.File.POS_START);
+ yield file.setPosition(1, OS.File.POS_CURRENT);
+ do_throw("Shouldn't have succeeded");
+ } catch (ex) {
+ do_print(ex.toString());
+ do_check_true(!!ex);
+ }
+
+ } finally {
+ yield file.setPosition(0, OS.File.POS_START);
+ yield file.close();
+ try {
+ yield OS.File.remove(path);
+ } catch (ex if ex.becauseNoSuchFile) {
+ // ignore.
+ }
+ }
+ } catch(ex) {
+ do_throw(ex);
+ }
+}
+
+function run_test() {
+ // First verify stuff works for small values.
+ add_task(test_setPosition.bind(null, 0, 100, 50));
+ add_task(test_setPosition.bind(null, 1000, 100, 50));
+ add_task(test_setPosition.bind(null, 1000, -100, -50));
+
+ if (OS.Constants.Win || ctypes.off_t.size >= 8) {
+ // Now verify stuff still works for large values.
+ // 1. Multiple small seeks, which add up to > MAXINT32
+ add_task(test_setPosition.bind(null, 0x7fffffff, 0x7fffffff, 0));
+ // 2. Plain large seek, that should end up at 0 again.
+ // 0xffffffff also happens to be the INVALID_SET_FILE_POINTER value on
+ // Windows, so this also tests the error handling
+ add_task(test_setPosition.bind(null, 0, 0xffffffff, 0xffffffff));
+ // 3. Multiple large seeks that should end up > MAXINT32.
+ add_task(test_setPosition.bind(null, 0xffffffff, 0xffffffff, 0xffffffff));
+ // 5. Multiple large seeks with negative offsets.
+ add_task(test_setPosition.bind(null, 0xffffffff, -0x7fffffff, 0x7fffffff));
+
+ // 6. Check failures
+ add_task(test_setPosition_failures);
+ }
+
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
new file mode 100644
index 000000000..17d3afa7c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setDates.js
@@ -0,0 +1,211 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * A test to ensure that OS.File.setDates and OS.File.prototype.setDates are
+ * working correctly.
+ * (see bug 924916)
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+// Non-prototypical tests, operating on path names.
+add_task(function* test_nonproto() {
+ // First, create a file we can mess with.
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setDates_nonproto.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ // 1. Try to set some well known dates.
+ // We choose multiples of 2000ms, because the time stamp resolution of
+ // the underlying OS might not support something more precise.
+ const accDate = 2000;
+ const modDate = 4000;
+ {
+ yield OS.File.setDates(path, accDate, modDate);
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.1 Try to omit modificationDate (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield OS.File.setDates(path, accDate);
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.2 Try to omit accessDate as well (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield OS.File.setDates(path);
+ let stat = yield OS.File.stat(path);
+ do_check_neq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 3. Repeat 1., but with Date objects this time
+ {
+ yield OS.File.setDates(path, new Date(accDate), new Date(modDate));
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 4. Check that invalid params will cause an exception/rejection.
+ {
+ for (let p of ["invalid", new Uint8Array(1), NaN]) {
+ try {
+ yield OS.File.setDates(path, p, modDate);
+ do_throw("Invalid access date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield OS.File.setDates(path, accDate, p);
+ do_throw("Invalid modification date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield OS.File.setDates(path, p, p);
+ do_throw("Invalid dates should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ }
+ }
+ } finally {
+ // Remove the temp file again
+ yield OS.File.remove(path);
+ }
+});
+
+// Prototypical tests, operating on |File| handles.
+add_task(function* test_proto() {
+ if (OS.Constants.Sys.Name == "Android" || OS.Constants.Sys.Name == "Gonk") {
+ do_print("File.prototype.setDates is not implemented for Android/B2G");
+ do_check_eq(OS.File.prototype.setDates, undefined);
+ return;
+ }
+
+ // First, create a file we can mess with.
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setDates_proto.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, {write: true});
+
+ try {
+ // 1. Try to set some well known dates.
+ // We choose multiples of 2000ms, because the time stamp resolution of
+ // the underlying OS might not support something more precise.
+ const accDate = 2000;
+ const modDate = 4000;
+ {
+ yield fd.setDates(accDate, modDate);
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.1 Try to omit modificationDate (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield fd.setDates(accDate);
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 2.2 Try to omit accessDate as well (which should then default to
+ // |Date.now()|, expect for resolution differences).
+ {
+ yield fd.setDates();
+ let stat = yield fd.stat();
+ do_check_neq(accDate, stat.lastAccessDate.getTime());
+ do_check_neq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 3. Repeat 1., but with Date objects this time
+ {
+ yield fd.setDates(new Date(accDate), new Date(modDate));
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+
+ // 4. Check that invalid params will cause an exception/rejection.
+ {
+ for (let p of ["invalid", new Uint8Array(1), NaN]) {
+ try {
+ yield fd.setDates(p, modDate);
+ do_throw("Invalid access date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield fd.setDates(accDate, p);
+ do_throw("Invalid modification date should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ try {
+ yield fd.setDates(p, p);
+ do_throw("Invalid dates should have thrown for: " + p);
+ } catch (ex) {
+ let stat = yield fd.stat();
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ }
+ }
+ } finally {
+ yield fd.close();
+ }
+ } finally {
+ // Remove the temp file again
+ yield OS.File.remove(path);
+ }
+});
+
+// Tests setting dates on directories.
+add_task(function* test_dirs() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setDates_dir");
+ yield OS.File.makeDir(path);
+
+ try {
+ // 1. Try to set some well known dates.
+ // We choose multiples of 2000ms, because the time stamp resolution of
+ // the underlying OS might not support something more precise.
+ const accDate = 2000;
+ const modDate = 4000;
+ {
+ yield OS.File.setDates(path, accDate, modDate);
+ let stat = yield OS.File.stat(path);
+ do_check_eq(accDate, stat.lastAccessDate.getTime());
+ do_check_eq(modDate, stat.lastModificationDate.getTime());
+ }
+ } finally {
+ yield OS.File.removeEmptyDir(path);
+ }
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js
new file mode 100644
index 000000000..ab8bf7dd9
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_async_setPermissions.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * A test to ensure that OS.File.setPermissions and
+ * OS.File.prototype.setPermissions are all working correctly.
+ * (see bug 1001849)
+ * These functions are currently Unix-specific. The manifest skips
+ * the test on Windows.
+ */
+
+/**
+ * Helper function for test logging: prints a POSIX file permission mode as an
+ * octal number, with a leading '0' per C (not JS) convention. When the
+ * numeric value is 0o777 or lower, it is padded on the left with zeroes to
+ * four digits wide.
+ * Sample outputs: 0022, 0644, 04755.
+ */
+function format_mode(mode) {
+ if (mode <= 0o777) {
+ return ("0000" + mode.toString(8)).slice(-4);
+ } else {
+ return "0" + mode.toString(8);
+ }
+}
+
+const _umask = OS.Constants.Sys.umask;
+do_print("umask: " + format_mode(_umask));
+
+/**
+ * Compute the mode that a file should have after applying the umask,
+ * whatever it happens to be.
+ */
+function apply_umask(mode) {
+ return mode & ~_umask;
+}
+
+// Sequence of setPermission parameters and expected file mode. The first test
+// checks the permissions when the file is first created.
+var testSequence = [
+ [null, apply_umask(0o600)],
+ [{ unixMode: 0o4777 }, apply_umask(0o4777)],
+ [{ unixMode: 0o4777, unixHonorUmask: false }, 0o4777],
+ [{ unixMode: 0o4777, unixHonorUmask: true }, apply_umask(0o4777)],
+ [undefined, apply_umask(0o600)],
+ [{ unixMode: 0o666 }, apply_umask(0o666)],
+ [{ unixMode: 0o600 }, apply_umask(0o600)],
+ [{ unixMode: 0 }, 0],
+ [{}, apply_umask(0o600)],
+];
+
+// Test application to paths.
+add_task(function* test_path_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setPermissions_path.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ for (let [options, expectedMode] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield OS.File.setPermissions(path, options);
+ }
+
+ let stat = yield OS.File.stat(path);
+ do_check_eq(format_mode(stat.unixMode), format_mode(expectedMode));
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+// Test application to open files.
+add_task(function* test_file_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_async_setPermissions_file.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, { write: true });
+ try {
+ for (let [options, expectedMode] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield fd.setPermissions(options);
+ }
+
+ let stat = yield fd.stat();
+ do_check_eq(format_mode(stat.unixMode), format_mode(expectedMode));
+ }
+ } finally {
+ yield fd.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js
new file mode 100644
index 000000000..5740f7f1a
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_closed.js
@@ -0,0 +1,48 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_test_pending();
+ run_next_test();
+}
+
+add_task(function test_closed() {
+ OS.Shared.DEBUG = true;
+ let currentDir = yield OS.File.getCurrentDirectory();
+ do_print("Open a file, ensure that we can call stat()");
+ let path = OS.Path.join(currentDir, "test_osfile_closed.js");
+ let file = yield OS.File.open(path);
+ yield file.stat();
+ do_check_true(true);
+
+ yield file.close();
+
+ do_print("Ensure that we cannot stat() on closed file");
+ let exn;
+ try {
+ yield file.stat();
+ } catch (ex) {
+ exn = ex;
+ }
+ do_print("Ensure that this raises the correct error");
+ do_check_true(!!exn);
+ do_check_true(exn instanceof OS.File.Error);
+ do_check_true(exn.becauseClosed);
+
+ do_print("Ensure that we cannot read() on closed file");
+ exn = null;
+ try {
+ yield file.read();
+ } catch (ex) {
+ exn = ex;
+ }
+ do_print("Ensure that this raises the correct error");
+ do_check_true(!!exn);
+ do_check_true(exn instanceof OS.File.Error);
+ do_check_true(exn.becauseClosed);
+
+});
+
+add_task(do_test_finished);
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js
new file mode 100644
index 000000000..a1c319eca
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_error.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* testFileError_with_writeAtomic() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "testFileError.tmp");
+ yield File.remove(path);
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ let exception;
+ try {
+ yield File.writeAtomic(path, DEFAULT_CONTENTS, { noOverwrite: true });
+ } catch (ex) {
+ exception = ex;
+ }
+ do_check_true(exception instanceof File.Error);
+ do_check_true(exception.path == path);
+});
+
+add_task(function* testFileError_with_makeDir() {
+ let path = Path.join(Constants.Path.tmpDir,
+ "directory");
+ yield File.removeDir(path);
+ yield File.makeDir(path);
+ let exception;
+ try {
+ yield File.makeDir(path, { ignoreExisting: false });
+ } catch (ex) {
+ exception = ex;
+ }
+ do_check_true(exception instanceof File.Error);
+ do_check_true(exception.path == path);
+});
+
+add_task(function* testFileError_with_move() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let sourcePath = Path.join(Constants.Path.tmpDir,
+ "src.tmp");
+ let destPath = Path.join(Constants.Path.tmpDir,
+ "dest.tmp");
+ yield File.remove(sourcePath);
+ yield File.remove(destPath);
+ yield File.writeAtomic(sourcePath, DEFAULT_CONTENTS);
+ yield File.writeAtomic(destPath, DEFAULT_CONTENTS);
+ let exception;
+ try {
+ yield File.move(sourcePath, destPath, { noOverwrite: true });
+ } catch (ex) {
+ exception = ex;
+ }
+ do_print(exception);
+ do_check_true(exception instanceof File.Error);
+ do_check_true(exception.path == sourcePath);
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js
new file mode 100644
index 000000000..e32c37224
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js
@@ -0,0 +1,100 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+// We want the actual global to get at the internals since Scheduler is not
+// exported.
+var AsyncFrontGlobal = Components.utils.import(
+ "resource://gre/modules/osfile/osfile_async_front.jsm",
+ null);
+var Scheduler = AsyncFrontGlobal.Scheduler;
+
+/**
+ * Verify that Scheduler.kill() interacts with other OS.File requests correctly,
+ * and that no requests are lost. This is relevant because on B2G we
+ * auto-kill the worker periodically, making it very possible for valid requests
+ * to be interleaved with the automatic kill().
+ *
+ * This test is being created with the fix for Bug 1125989 where `kill` queue
+ * management was found to be buggy. It is a glass-box test that explicitly
+ * re-creates the observed failure situation; it is not guaranteed to prevent
+ * all future regressions. The following is a detailed explanation of the test
+ * for your benefit if this test ever breaks or you are wondering what was the
+ * point of all this. You might want to skim the code below first.
+ *
+ * OS.File maintains a `queue` of operations to be performed. This queue is
+ * nominally implemented as a chain of promises. Every time a new job is
+ * OS.File.push()ed, it effectively becomes the new `queue` promise. (An
+ * extra promise is interposed with a rejection handler to avoid the rejection
+ * cascading, but that does not matter for our purposes.)
+ *
+ * The flaw in `kill` was that it would wait for the `queue` to complete before
+ * replacing `queue`. As a result, another OS.File operation could use `push`
+ * (by way of OS.File.post()) to also use .then() on the same `queue` promise.
+ * Accordingly, assuming that promise was not yet resolved (due to a pending
+ * OS.File request), when it was resolved, both the task scheduled in `kill`
+ * and in `post` would be triggered. Both of those tasks would run until
+ * encountering a call to worker.post().
+ *
+ * Re-creating this race is not entirely trivial because of the large number of
+ * promises used by the code causing control flow to repeatedly be deferred. In
+ * a slightly simpler world we could run the follwing in the same turn of the
+ * event loop and trigger the problem.
+ * - any OS.File request
+ * - Scheduler.kill()
+ * - any OS.File request
+ *
+ * However, we need the Scheduler.kill task to reach the point where it is
+ * waiting on the same `queue` that another task has been scheduled against.
+ * Since the `kill` task yields on the `killQueue` promise prior to yielding
+ * on `queue`, however, some turns of the event loop are required. Happily,
+ * for us, as discussed above, the problem triggers when we have two promises
+ * scheduled on the `queue`, so we can just wait to schedule the second OS.File
+ * request on the queue. (Note that because of the additional then() added to
+ * eat rejections, there is an important difference between the value of
+ * `queue` and the value returned by the first OS.File request.)
+ */
+add_task(function* test_kill_race() {
+ // Ensure the worker has been created and that SET_DEBUG has taken effect.
+ // We have chosen OS.File.exists for our tests because it does not trigger
+ // a rejection and we absolutely do not care what the operation is other
+ // than it does not invoke a native fast-path.
+ yield OS.File.exists('foo.foo');
+
+ do_print('issuing first request');
+ let firstRequest = OS.File.exists('foo.bar');
+ let secondRequest;
+ let secondResolved = false;
+
+ // As noted in our big block comment, we want to wait to schedule the
+ // second request so that it races `kill`'s call to `worker.post`. Having
+ // ourselves wait on the same promise, `queue`, and registering ourselves
+ // before we issue the kill request means we will get run before the `kill`
+ // task resumes and allow us to precisely create the desired race.
+ Scheduler.queue.then(function() {
+ do_print('issuing second request');
+ secondRequest = OS.File.exists('foo.baz');
+ secondRequest.then(function() {
+ secondResolved = true;
+ });
+ });
+
+ do_print('issuing kill request');
+ let killRequest = Scheduler.kill({ reset: true, shutdown: false });
+
+ // Wait on the killRequest so that we can schedule a new OS.File request
+ // after it completes...
+ yield killRequest;
+ // ...because our ordering guarantee ensures that there is at most one
+ // worker (and this usage here should not be vulnerable even with the
+ // bug present), so when this completes the secondRequest has either been
+ // resolved or lost.
+ yield OS.File.exists('foo.goz');
+
+ ok(secondResolved,
+ 'The second request was resolved so we avoided the bug. Victory!');
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js
new file mode 100644
index 000000000..990d722f5
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_win_async_setPermissions.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * A test to ensure that OS.File.setPermissions and
+ * OS.File.prototype.setPermissions are all working correctly.
+ * (see bug 1022816)
+ * The manifest tests on Windows.
+ */
+
+// Sequence of setPermission parameters.
+var testSequence = [
+ [ { winAttributes: { readOnly: true, system: true, hidden: true } },
+ { readOnly: true, system: true, hidden: true } ],
+ [ { winAttributes: { readOnly: false } },
+ { readOnly: false, system: true, hidden: true } ],
+ [ { winAttributes: { system: false } },
+ { readOnly: false, system: false, hidden: true } ],
+ [ { winAttributes: { hidden: false } },
+ { readOnly: false, system: false, hidden: false } ],
+ [ { winAttributes: {readOnly: true, system: false, hidden: false} },
+ { readOnly: true, system: false, hidden: false } ],
+ [ { winAttributes: {readOnly: false, system: true, hidden: false} },
+ { readOnly: false, system: true, hidden: false } ],
+ [ { winAttributes: {readOnly: false, system: false, hidden: true} },
+ { readOnly: false, system: false, hidden: true } ],
+];
+
+// Test application to paths.
+add_task(function* test_path_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_path.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ for (let [options, attributesExpected] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield OS.File.setPermissions(path, options);
+ }
+
+ let stat = yield OS.File.stat(path);
+ do_print("Got stat winAttributes: " + JSON.stringify(stat.winAttributes));
+
+ do_check_eq(stat.winAttributes.readOnly, attributesExpected.readOnly);
+ do_check_eq(stat.winAttributes.system, attributesExpected.system);
+ do_check_eq(stat.winAttributes.hidden, attributesExpected.hidden);
+
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+// Test application to open files.
+add_task(function* test_file_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_file.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, { write: true });
+ try {
+ for (let [options, attributesExpected] of testSequence) {
+ if (options !== null) {
+ do_print("Setting permissions to " + JSON.stringify(options));
+ yield fd.setPermissions(options);
+ }
+
+ let stat = yield fd.stat();
+ do_print("Got stat winAttributes: " + JSON.stringify(stat.winAttributes));
+ do_check_eq(stat.winAttributes.readOnly, attributesExpected.readOnly);
+ do_check_eq(stat.winAttributes.system, attributesExpected.system);
+ do_check_eq(stat.winAttributes.hidden, attributesExpected.hidden);
+ }
+ } finally {
+ yield fd.close();
+ }
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+// Test application to Check setPermissions on a non-existant file path.
+add_task(function* test_non_existant_file_path_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_path.tmp");
+ Assert.rejects(OS.File.setPermissions(path, {winAttributes: {readOnly: true}}),
+ /The system cannot find the file specified/,
+ "setPermissions failed as expected on a non-existant file path");
+});
+
+// Test application to Check setPermissions on a invalid file handle.
+add_task(function* test_closed_file_handle_setPermissions() {
+ let path = OS.Path.join(OS.Constants.Path.tmpDir,
+ "test_osfile_win_async_setPermissions_path.tmp");
+ yield OS.File.writeAtomic(path, new Uint8Array(1));
+
+ try {
+ let fd = yield OS.File.open(path, { write: true });
+ yield fd.close();
+ Assert.rejects(fd.setPermissions(path, {winAttributes: {readOnly: true}}),
+ /The handle is invalid/,
+ "setPermissions failed as expected on a invalid file handle");
+ } finally {
+ yield OS.File.remove(path);
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js
new file mode 100644
index 000000000..adf345b0c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_backupTo_option.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/**
+ * Remove all temporary files and back up files, including
+ * test_backupTo_option_with_tmpPath.tmp
+ * test_backupTo_option_with_tmpPath.tmp.backup
+ * test_backupTo_option_without_tmpPath.tmp
+ * test_backupTo_option_without_tmpPath.tmp.backup
+ * test_non_backupTo_option.tmp
+ * test_non_backupTo_option.tmp.backup
+ * test_backupTo_option_without_destination_file.tmp
+ * test_backupTo_option_without_destination_file.tmp.backup
+ * test_backupTo_option_with_backup_file.tmp
+ * test_backupTo_option_with_backup_file.tmp.backup
+ */
+function clearFiles() {
+ return Task.spawn(function () {
+ let files = ["test_backupTo_option_with_tmpPath.tmp",
+ "test_backupTo_option_without_tmpPath.tmp",
+ "test_non_backupTo_option.tmp",
+ "test_backupTo_option_without_destination_file.tmp",
+ "test_backupTo_option_with_backup_file.tmp"];
+ for (let file of files) {
+ let path = Path.join(Constants.Path.tmpDir, file);
+ yield File.remove(path);
+ yield File.remove(path + ".backup");
+ }
+ });
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* init() {
+ yield clearFiles();
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| specified
+ * destination file exists
+ * @result destination file will be backed up
+ */
+add_task(function* test_backupTo_option_with_tmpPath() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_with_tmpPath.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ yield File.writeAtomic(path, WRITE_CONTENTS, { tmpPath: path + ".tmp",
+ backupTo: path + ".backup" });
+ do_check_true((yield File.exists(path + ".backup")));
+ let contents = yield File.read(path + ".backup");
+ do_check_eq(DEFAULT_CONTENTS, (new TextDecoder()).decode(contents));
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| not specified
+ * destination file exists
+ * @result destination file will be backed up
+ */
+add_task(function* test_backupTo_option_without_tmpPath() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_without_tmpPath.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ yield File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" });
+ do_check_true((yield File.exists(path + ".backup")));
+ let contents = yield File.read(path + ".backup");
+ do_check_eq(DEFAULT_CONTENTS, (new TextDecoder()).decode(contents));
+});
+
+/**
+ * test when
+ * |backupTo| not specified
+ * |tmpPath| not specified
+ * destination file exists
+ * @result destination file will not be backed up
+ */
+add_task(function* test_non_backupTo_option() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_non_backupTo_option.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+ yield File.writeAtomic(path, WRITE_CONTENTS);
+ do_check_false((yield File.exists(path + ".backup")));
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| not specified
+ * destination file not exists
+ * @result no back up file exists
+ */
+add_task(function* test_backupTo_option_without_destination_file() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_without_destination_file.tmp");
+ yield File.remove(path);
+ yield File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" });
+ do_check_false((yield File.exists(path + ".backup")));
+});
+
+/**
+ * test when
+ * |backupTo| specified
+ * |tmpPath| not specified
+ * backup file exists
+ * destination file exists
+ * @result destination file will be backed up
+ */
+add_task(function* test_backupTo_option_with_backup_file() {
+ let DEFAULT_CONTENTS = "default contents" + Math.random();
+ let WRITE_CONTENTS = "abc" + Math.random();
+ let path = Path.join(Constants.Path.tmpDir,
+ "test_backupTo_option_with_backup_file.tmp");
+ yield File.writeAtomic(path, DEFAULT_CONTENTS);
+
+ yield File.writeAtomic(path + ".backup", new Uint8Array(1000));
+
+ yield File.writeAtomic(path, WRITE_CONTENTS, { backupTo: path + ".backup" });
+ do_check_true((yield File.exists(path + ".backup")));
+ let contents = yield File.read(path + ".backup");
+ do_check_eq(DEFAULT_CONTENTS, (new TextDecoder()).decode(contents));
+});
+
+add_task(function* cleanup() {
+ yield clearFiles();
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js
new file mode 100644
index 000000000..a32a690e6
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_writeAtomic_zerobytes.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+var SHARED_PATH;
+
+add_task(function* init() {
+ do_get_profile();
+ SHARED_PATH = OS.Path.join(OS.Constants.Path.profileDir, "test_osfile_write_zerobytes.tmp");
+});
+
+add_test_pair(function* test_osfile_writeAtomic_zerobytes() {
+ let encoder = new TextEncoder();
+ let string1 = "";
+ let outbin = encoder.encode(string1);
+ yield OS.File.writeAtomic(SHARED_PATH, outbin);
+
+ let decoder = new TextDecoder();
+ let bin = yield OS.File.read(SHARED_PATH);
+ let string2 = decoder.decode(bin);
+ // Checking if writeAtomic supports writing encoded zero-byte strings
+ Assert.equal(string2, string1, "Read the expected (empty) string.");
+});
+
+function run_test() {
+ run_next_test();
+} \ No newline at end of file
diff --git a/toolkit/components/osfile/tests/xpcshell/test_path.js b/toolkit/components/osfile/tests/xpcshell/test_path.js
new file mode 100644
index 000000000..76a507ee3
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_path.js
@@ -0,0 +1,159 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+Services.prefs.setBoolPref("toolkit.osfile.test.syslib_necessary", false);
+ // We don't need libc/kernel32.dll for this test
+
+var ImportWin = {};
+var ImportUnix = {};
+Components.utils.import("resource://gre/modules/osfile/ospath_win.jsm", ImportWin);
+Components.utils.import("resource://gre/modules/osfile/ospath_unix.jsm", ImportUnix);
+
+var Win = ImportWin;
+var Unix = ImportUnix;
+
+function do_check_fail(f)
+{
+ try {
+ let result = f();
+ do_print("Failed do_check_fail: " + result);
+ do_check_true(false);
+ } catch (ex) {
+ do_check_true(true);
+ }
+};
+
+function run_test()
+{
+ do_print("Testing Windows paths");
+
+ do_print("Backslash-separated, no drive");
+ do_check_eq(Win.basename("a\\b"), "b");
+ do_check_eq(Win.basename("a\\b\\"), "");
+ do_check_eq(Win.basename("abc"), "abc");
+ do_check_eq(Win.dirname("a\\b"), "a");
+ do_check_eq(Win.dirname("a\\b\\"), "a\\b");
+ do_check_eq(Win.dirname("a\\\\\\\\b"), "a");
+ do_check_eq(Win.dirname("abc"), ".");
+ do_check_eq(Win.normalize("\\a\\b\\c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f");
+ do_check_eq(Win.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f");
+ do_check_fail(() => Win.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
+
+ do_check_eq(Win.join("\\tmp", "foo", "bar"), "\\tmp\\foo\\bar", "join \\tmp,foo,bar");
+ do_check_eq(Win.join("\\tmp", "\\foo", "bar"), "\\foo\\bar", "join \\tmp,\\foo,bar");
+ do_check_eq(Win.winGetDrive("\\tmp"), null);
+ do_check_eq(Win.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null);
+ do_check_eq(Win.winGetDrive("\\"), null);
+
+
+ do_print("Backslash-separated, with a drive");
+ do_check_eq(Win.basename("c:a\\b"), "b");
+ do_check_eq(Win.basename("c:a\\b\\"), "");
+ do_check_eq(Win.basename("c:abc"), "abc");
+ do_check_eq(Win.dirname("c:a\\b"), "c:a");
+ do_check_eq(Win.dirname("c:a\\b\\"), "c:a\\b");
+ do_check_eq(Win.dirname("c:a\\\\\\\\b"), "c:a");
+ do_check_eq(Win.dirname("c:abc"), "c:");
+ let options = {
+ winNoDrive: true
+ };
+ do_check_eq(Win.dirname("c:a\\b", options), "a");
+ do_check_eq(Win.dirname("c:a\\b\\", options), "a\\b");
+ do_check_eq(Win.dirname("c:a\\\\\\\\b", options), "a");
+ do_check_eq(Win.dirname("c:abc", options), ".");
+ do_check_eq(Win.join("c:", "abc"), "c:\\abc", "join c:,abc");
+
+ do_check_eq(Win.normalize("c:"), "c:\\");
+ do_check_eq(Win.normalize("c:\\"), "c:\\");
+ do_check_eq(Win.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
+ do_check_eq(Win.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
+ do_check_fail(() => Win.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
+
+ do_check_eq(Win.join("c:\\", "foo"), "c:\\foo", "join c:\,foo");
+ do_check_eq(Win.join("c:\\tmp", "foo", "bar"), "c:\\tmp\\foo\\bar", "join c:\\tmp,foo,bar");
+ do_check_eq(Win.join("c:\\tmp", "\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,\\foo,bar");
+ do_check_eq(Win.join("c:\\tmp", "c:\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:\\foo,bar");
+ do_check_eq(Win.join("c:\\tmp", "c:foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:foo,bar");
+ do_check_eq(Win.winGetDrive("c:"), "c:");
+ do_check_eq(Win.winGetDrive("c:\\"), "c:");
+ do_check_eq(Win.winGetDrive("c:abc"), "c:");
+ do_check_eq(Win.winGetDrive("c:abc\\d\\e\\f\\g"), "c:");
+ do_check_eq(Win.winGetDrive("c:\\abc"), "c:");
+ do_check_eq(Win.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:");
+
+ do_print("Forwardslash-separated, no drive");
+ do_check_eq(Win.normalize("/a/b/c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("/a/b////c"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("/a/b/c///"), "\\a\\b\\c");
+ do_check_eq(Win.normalize("/a/b/c/../../../d/e/f"), "\\d\\e\\f");
+ do_check_eq(Win.normalize("a/b/c/../../../d/e/f"), "d\\e\\f");
+
+ do_print("Forwardslash-separated, with a drive");
+ do_check_eq(Win.normalize("c:/"), "c:\\");
+ do_check_eq(Win.normalize("c:/a/b/c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:/a/b////c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:////a/b/c"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:/a/b/c///"), "c:\\a\\b\\c");
+ do_check_eq(Win.normalize("c:/a/b/c/../../../d/e/f"), "c:\\d\\e\\f");
+ do_check_eq(Win.normalize("c:a/b/c/../../../d/e/f"), "c:\\d\\e\\f");
+
+ do_print("Backslash-separated, UNC-style");
+ do_check_eq(Win.basename("\\\\a\\b"), "b");
+ do_check_eq(Win.basename("\\\\a\\b\\"), "");
+ do_check_eq(Win.basename("\\\\abc"), "");
+ do_check_eq(Win.dirname("\\\\a\\b"), "\\\\a");
+ do_check_eq(Win.dirname("\\\\a\\b\\"), "\\\\a\\b");
+ do_check_eq(Win.dirname("\\\\a\\\\\\\\b"), "\\\\a");
+ do_check_eq(Win.dirname("\\\\abc"), "\\\\abc");
+ do_check_eq(Win.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c");
+ do_check_eq(Win.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c");
+ do_check_eq(Win.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c");
+ do_check_eq(Win.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f");
+ do_check_fail(() => Win.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f"));
+
+ do_check_eq(Win.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar");
+ do_check_eq(Win.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar");
+ do_check_eq(Win.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar");
+ do_check_eq(Win.winGetDrive("\\\\"), null);
+ do_check_eq(Win.winGetDrive("\\\\c"), "\\\\c");
+ do_check_eq(Win.winGetDrive("\\\\c\\abc"), "\\\\c");
+
+ do_print("Testing unix paths");
+ do_check_eq(Unix.basename("a/b"), "b");
+ do_check_eq(Unix.basename("a/b/"), "");
+ do_check_eq(Unix.basename("abc"), "abc");
+ do_check_eq(Unix.dirname("a/b"), "a");
+ do_check_eq(Unix.dirname("a/b/"), "a/b");
+ do_check_eq(Unix.dirname("a////b"), "a");
+ do_check_eq(Unix.dirname("abc"), ".");
+ do_check_eq(Unix.normalize("/a/b/c"), "/a/b/c");
+ do_check_eq(Unix.normalize("/a/b////c"), "/a/b/c");
+ do_check_eq(Unix.normalize("////a/b/c"), "/a/b/c");
+ do_check_eq(Unix.normalize("/a/b/c///"), "/a/b/c");
+ do_check_eq(Unix.normalize("/a/b/c/../../../d/e/f"), "/d/e/f");
+ do_check_eq(Unix.normalize("a/b/c/../../../d/e/f"), "d/e/f");
+ do_check_fail(() => Unix.normalize("/a/b/c/../../../../d/e/f"));
+
+ do_check_eq(Unix.join("/tmp", "foo", "bar"), "/tmp/foo/bar", "join /tmp,foo,bar");
+ do_check_eq(Unix.join("/tmp", "/foo", "bar"), "/foo/bar", "join /tmp,/foo,bar");
+
+ do_print("Testing the presence of ospath.jsm");
+ let Scope = {};
+ try {
+ Components.utils.import("resource://gre/modules/osfile/ospath.jsm", Scope);
+ } catch (ex) {
+ // Can't load ospath
+ }
+ do_check_true(!!Scope.basename);
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_path_constants.js b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
new file mode 100644
index 000000000..c0057c750
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_path_constants.js
@@ -0,0 +1,83 @@
+/* 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/. */
+
+"use strict";
+
+Cu.import("resource://gre/modules/ctypes.jsm", this);
+Cu.import("resource://testing-common/AppData.jsm", this);
+
+
+function run_test() {
+ run_next_test();
+}
+
+function compare_paths(ospath, key) {
+ let file;
+ try {
+ file = Services.dirsvc.get(key, Components.interfaces.nsIFile);
+ } catch(ex) {}
+
+ if (file) {
+ do_check_true(!!ospath);
+ do_check_eq(ospath, file.path);
+ } else {
+ do_print("WARNING: " + key + " is not defined. Test may not be testing anything!");
+ do_check_false(!!ospath);
+ }
+}
+
+// Some path constants aren't set up until the profile is available. This
+// test verifies that behavior.
+add_task(function* test_before_after_profile() {
+ do_check_null(OS.Constants.Path.profileDir);
+ do_check_null(OS.Constants.Path.localProfileDir);
+ do_check_null(OS.Constants.Path.userApplicationDataDir);
+
+ do_get_profile();
+ do_check_true(!!OS.Constants.Path.profileDir);
+ do_check_true(!!OS.Constants.Path.localProfileDir);
+
+ // UAppData is still null because the xpcshell profile doesn't set it up.
+ // This test is mostly here to fail in case behavior of do_get_profile() ever
+ // changes. We want to know if our assumptions no longer hold!
+ do_check_null(OS.Constants.Path.userApplicationDataDir);
+
+ yield makeFakeAppDir();
+ do_check_true(!!OS.Constants.Path.userApplicationDataDir);
+
+ // FUTURE: verify AppData too (bug 964291).
+});
+
+// Test simple paths
+add_task(function* test_simple_paths() {
+ do_check_true(!!OS.Constants.Path.tmpDir);
+ compare_paths(OS.Constants.Path.tmpDir, "TmpD");
+
+});
+
+// Test presence of paths that only exist on Desktop platforms
+add_task(function* test_desktop_paths() {
+ if (OS.Constants.Sys.Name == "Android" || OS.Constants.Sys.Name == "Gonk") {
+ return;
+ }
+ do_check_true(!!OS.Constants.Path.desktopDir);
+ do_check_true(!!OS.Constants.Path.homeDir);
+
+ compare_paths(OS.Constants.Path.homeDir, "Home");
+ compare_paths(OS.Constants.Path.desktopDir, "Desk");
+ compare_paths(OS.Constants.Path.userApplicationDataDir, "UAppData");
+
+ compare_paths(OS.Constants.Path.winAppDataDir, "AppData");
+ compare_paths(OS.Constants.Path.winStartMenuProgsDir, "Progs");
+
+ compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir");
+ compare_paths(OS.Constants.Path.macLocalApplicationsDir, "LocApp");
+ compare_paths(OS.Constants.Path.macTrashDir, "Trsh");
+});
+
+// Open libxul
+add_task(function* test_libxul() {
+ ctypes.open(OS.Constants.Path.libxul);
+ do_print("Linked to libxul");
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_queue.js b/toolkit/components/osfile/tests/xpcshell/test_queue.js
new file mode 100644
index 000000000..c9d23eabc
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_queue.js
@@ -0,0 +1,38 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+// Check if Scheduler.queue returned by OS.File.queue is resolved initially.
+add_task(function* check_init() {
+ yield OS.File.queue;
+ do_print("Function resolved");
+});
+
+// Check if Scheduler.queue returned by OS.File.queue is resolved
+// after an operation is successful.
+add_task(function* check_success() {
+ do_print("Attempting to open a file correctly");
+ let openedFile = yield OS.File.open(OS.Path.join(do_get_cwd().path, "test_queue.js"));
+ do_print("File opened correctly");
+ yield OS.File.queue;
+ do_print("Function resolved");
+});
+
+// Check if Scheduler.queue returned by OS.File.queue is resolved
+// after an operation fails.
+add_task(function* check_failure() {
+ let exception;
+ try {
+ do_print("Attempting to open a non existing file");
+ yield OS.File.open(OS.Path.join(".", "Bigfoot"));
+ } catch (err) {
+ exception = err;
+ yield OS.File.queue;
+ }
+ do_check_true(exception!=null);
+ do_print("Function resolved");
+}); \ No newline at end of file
diff --git a/toolkit/components/osfile/tests/xpcshell/test_read_write.js b/toolkit/components/osfile/tests/xpcshell/test_read_write.js
new file mode 100644
index 000000000..00235ed8c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_read_write.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu} = Components;
+
+var SHARED_PATH;
+
+var EXISTING_FILE = do_get_file("xpcshell.ini").path;
+
+add_task(function* init() {
+ do_get_profile();
+ SHARED_PATH = OS.Path.join(OS.Constants.Path.profileDir, "test_osfile_read.tmp");
+});
+
+
+// Check that OS.File.read() is executed after the previous operation
+add_test_pair(function* ordering() {
+ let string1 = "Initial state " + Math.random();
+ let string2 = "After writing " + Math.random();
+ yield OS.File.writeAtomic(SHARED_PATH, string1);
+ OS.File.writeAtomic(SHARED_PATH, string2);
+ let string3 = yield OS.File.read(SHARED_PATH, { encoding: "utf-8" });
+ do_check_eq(string3, string2);
+});
+
+add_test_pair(function* read_write_all() {
+ let DEST_PATH = SHARED_PATH + Math.random();
+ let TMP_PATH = DEST_PATH + ".tmp";
+
+ let test_with_options = function(options, suffix) {
+ return Task.spawn(function*() {
+ do_print("Running test read_write_all with options " + JSON.stringify(options));
+ let TEST = "read_write_all " + suffix;
+
+ let optionsBackup = JSON.parse(JSON.stringify(options));
+
+ // Check that read + writeAtomic performs a correct copy
+ let currentDir = yield OS.File.getCurrentDirectory();
+ let pathSource = OS.Path.join(currentDir, EXISTING_FILE);
+ let contents = yield OS.File.read(pathSource);
+ do_check_true(!!contents); // Content is not empty
+ let bytesRead = contents.byteLength;
+
+ let bytesWritten = yield OS.File.writeAtomic(DEST_PATH, contents, options);
+ do_check_eq(bytesRead, bytesWritten); // Correct number of bytes written
+
+ // Check that options are not altered
+ do_check_eq(JSON.stringify(options), JSON.stringify(optionsBackup));
+ yield reference_compare_files(pathSource, DEST_PATH, TEST);
+
+ // Check that temporary file was removed or never created exist
+ do_check_false(new FileUtils.File(TMP_PATH).exists());
+
+ // Check that writeAtomic fails if noOverwrite is true and the destination
+ // file already exists!
+ contents = new Uint8Array(300);
+ let view = new Uint8Array(contents.buffer, 10, 200);
+ try {
+ let opt = JSON.parse(JSON.stringify(options));
+ opt.noOverwrite = true;
+ yield OS.File.writeAtomic(DEST_PATH, view, opt);
+ do_throw("With noOverwrite, writeAtomic should have refused to overwrite file (" + suffix + ")");
+ } catch (err if err instanceof OS.File.Error && err.becauseExists) {
+ do_print("With noOverwrite, writeAtomic correctly failed (" + suffix + ")");
+ }
+ yield reference_compare_files(pathSource, DEST_PATH, TEST);
+
+ // Check that temporary file was removed or never created
+ do_check_false(new FileUtils.File(TMP_PATH).exists());
+
+ // Now write a subset
+ let START = 10;
+ let LENGTH = 100;
+ contents = new Uint8Array(300);
+ for (var i = 0; i < contents.byteLength; i++)
+ contents[i] = i % 256;
+ view = new Uint8Array(contents.buffer, START, LENGTH);
+ bytesWritten = yield OS.File.writeAtomic(DEST_PATH, view, options);
+ do_check_eq(bytesWritten, LENGTH);
+
+ let array2 = yield OS.File.read(DEST_PATH);
+ do_check_eq(LENGTH, array2.length);
+ for (var i = 0; i < LENGTH; i++)
+ do_check_eq(array2[i], (i + START) % 256);
+
+ // Cleanup.
+ yield OS.File.remove(DEST_PATH);
+ yield OS.File.remove(TMP_PATH);
+ });
+ };
+
+ yield test_with_options({tmpPath: TMP_PATH}, "Renaming, not flushing");
+ yield test_with_options({tmpPath: TMP_PATH, flush: true}, "Renaming, flushing");
+ yield test_with_options({}, "Not renaming, not flushing");
+ yield test_with_options({flush: true}, "Not renaming, flushing");
+});
+
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_remove.js b/toolkit/components/osfile/tests/xpcshell/test_remove.js
new file mode 100644
index 000000000..c8dc33054
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_remove.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+ run_next_test();
+}
+
+add_task(function* test_ignoreAbsent() {
+ let absent_file_name = "test_osfile_front_absent.tmp";
+
+ // Removing absent files should throw if "ignoreAbsent" is true.
+ yield Assert.rejects(OS.File.remove(absent_file_name, {ignoreAbsent: false}),
+ "OS.File.remove throws if there is no such file.");
+
+ // Removing absent files should not throw if "ignoreAbsent" is true or not
+ // defined.
+ let exception = null;
+ try {
+ yield OS.File.remove(absent_file_name, {ignoreAbsent: true});
+ yield OS.File.remove(absent_file_name);
+ } catch (ex) {
+ exception = ex;
+ }
+ Assert.ok(!exception, "OS.File.remove should not throw when not requested.");
+});
+
+add_task(function* test_ignoreAbsent_directory_missing() {
+ let absent_file_name = OS.Path.join("absent_parent", "test.tmp");
+
+ // Removing absent files should throw if "ignoreAbsent" is true.
+ yield Assert.rejects(OS.File.remove(absent_file_name, {ignoreAbsent: false}),
+ "OS.File.remove throws if there is no such file.");
+
+ // Removing files from absent directories should not throw if "ignoreAbsent"
+ // is true or not defined.
+ let exception = null;
+ try {
+ yield OS.File.remove(absent_file_name, {ignoreAbsent: true});
+ yield OS.File.remove(absent_file_name);
+ } catch (ex) {
+ exception = ex;
+ }
+ Assert.ok(!exception, "OS.File.remove should not throw when not requested.");
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js
new file mode 100644
index 000000000..41ad0eb8c
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_removeDir.js
@@ -0,0 +1,177 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+ run_next_test();
+}
+
+add_task(function() {
+ // Set up profile. We create the directory in the profile, because the profile
+ // is removed after every test run.
+ do_get_profile();
+
+ let file = OS.Path.join(OS.Constants.Path.profileDir, "file");
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
+ let file1 = OS.Path.join(dir, "file1");
+ let file2 = OS.Path.join(dir, "file2");
+ let subDir = OS.Path.join(dir, "subdir");
+ let fileInSubDir = OS.Path.join(subDir, "file");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove non-existent directory
+ let exception = null;
+ try {
+ yield OS.File.removeDir(dir, {ignoreAbsent: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+
+ // Remove non-existent directory with ignoreAbsent
+ yield OS.File.removeDir(dir, {ignoreAbsent: true});
+ yield OS.File.removeDir(dir);
+
+ // Remove file with ignoreAbsent: false
+ yield OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" });
+ exception = null;
+ try {
+ yield OS.File.removeDir(file, {ignoreAbsent: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+
+ // Remove empty directory
+ yield OS.File.makeDir(dir);
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove directory that contains one file
+ yield OS.File.makeDir(dir);
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove directory that contains multiple files
+ yield OS.File.makeDir(dir);
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ yield OS.File.writeAtomic(file2, "content", { tmpPath: file2 + ".tmp" });
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove directory that contains a file and a directory
+ yield OS.File.makeDir(dir);
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ yield OS.File.makeDir(subDir);
+ yield OS.File.writeAtomic(fileInSubDir, "content", { tmpPath: fileInSubDir + ".tmp" });
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+});
+
+add_task(function* test_unix_symlink() {
+ // Windows does not implement OS.File.unixSymLink()
+ if (OS.Constants.Win) {
+ return;
+ }
+
+ // Android / B2G file systems typically don't support symlinks.
+ if (OS.Constants.Sys.Name == "Android") {
+ return;
+ }
+
+ let file = OS.Path.join(OS.Constants.Path.profileDir, "file");
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
+ let file1 = OS.Path.join(dir, "file1");
+
+ // This test will create the following directory structure:
+ // <profileDir>/file (regular file)
+ // <profileDir>/file.link => file (symlink)
+ // <profileDir>/directory (directory)
+ // <profileDir>/linkdir => directory (directory)
+ // <profileDir>/directory/file1 (regular file)
+ // <profileDir>/directory3 (directory)
+ // <profileDir>/directory3/file3 (directory)
+ // <profileDir>/directory/link2 => ../directory3 (regular file)
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ yield OS.File.writeAtomic(file, "content", { tmpPath: file + ".tmp" });
+ do_check_true((yield OS.File.exists(file)));
+ let info = yield OS.File.stat(file, {unixNoFollowingLinks: true});
+ do_check_false(info.isDir);
+ do_check_false(info.isSymLink);
+
+ yield OS.File.unixSymLink(file, file + ".link");
+ do_check_true((yield OS.File.exists(file + ".link")));
+ info = yield OS.File.stat(file + ".link", {unixNoFollowingLinks: true});
+ do_check_false(info.isDir);
+ do_check_true(info.isSymLink);
+ info = yield OS.File.stat(file + ".link");
+ do_check_false(info.isDir);
+ do_check_false(info.isSymLink);
+ yield OS.File.remove(file + ".link");
+ do_check_false((yield OS.File.exists(file + ".link")));
+
+ yield OS.File.makeDir(dir);
+ do_check_true((yield OS.File.exists(dir)));
+ info = yield OS.File.stat(dir, {unixNoFollowingLinks: true});
+ do_check_true(info.isDir);
+ do_check_false(info.isSymLink);
+
+ let link = OS.Path.join(OS.Constants.Path.profileDir, "linkdir");
+
+ yield OS.File.unixSymLink(dir, link);
+ do_check_true((yield OS.File.exists(link)));
+ info = yield OS.File.stat(link, {unixNoFollowingLinks: true});
+ do_check_false(info.isDir);
+ do_check_true(info.isSymLink);
+ info = yield OS.File.stat(link);
+ do_check_true(info.isDir);
+ do_check_false(info.isSymLink);
+
+ let dir3 = OS.Path.join(OS.Constants.Path.profileDir, "directory3");
+ let file3 = OS.Path.join(dir3, "file3");
+ let link2 = OS.Path.join(dir, "link2");
+
+ yield OS.File.writeAtomic(file1, "content", { tmpPath: file1 + ".tmp" });
+ do_check_true((yield OS.File.exists(file1)));
+ yield OS.File.makeDir(dir3);
+ do_check_true((yield OS.File.exists(dir3)));
+ yield OS.File.writeAtomic(file3, "content", { tmpPath: file3 + ".tmp" });
+ do_check_true((yield OS.File.exists(file3)));
+ yield OS.File.unixSymLink("../directory3", link2);
+ do_check_true((yield OS.File.exists(link2)));
+
+ yield OS.File.removeDir(link);
+ do_check_false((yield OS.File.exists(link)));
+ do_check_true((yield OS.File.exists(file1)));
+ yield OS.File.removeDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+ do_check_true((yield OS.File.exists(file3)));
+ yield OS.File.removeDir(dir3);
+ do_check_false((yield OS.File.exists(dir3)));
+
+ // This task will be executed only on Unix-like systems.
+ // Please do not add tests independent to operating systems here
+ // or implement symlink() on Windows.
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js
new file mode 100644
index 000000000..95f8d5cd1
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_removeEmptyDir.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", false);
+});
+
+function run_test() {
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+
+ run_next_test();
+}
+
+/**
+ * Test OS.File.removeEmptyDir
+ */
+add_task(function() {
+ // Set up profile. We create the directory in the profile, because the profile
+ // is removed after every test run.
+ do_get_profile();
+
+ let dir = OS.Path.join(OS.Constants.Path.profileDir, "directory");
+
+ // Sanity checking for the test
+ do_check_false((yield OS.File.exists(dir)));
+
+ // Remove non-existent directory
+ yield OS.File.removeEmptyDir(dir);
+
+ // Remove non-existent directory with ignoreAbsent
+ yield OS.File.removeEmptyDir(dir, {ignoreAbsent: true});
+
+ // Remove non-existent directory with ignoreAbsent false
+ let exception = null;
+ try {
+ yield OS.File.removeEmptyDir(dir, {ignoreAbsent: false});
+ } catch (ex) {
+ exception = ex;
+ }
+
+ do_check_true(!!exception);
+ do_check_true(exception instanceof OS.File.Error);
+ do_check_true(exception.becauseNoSuchFile);
+
+ // Remove empty directory
+ yield OS.File.makeDir(dir);
+ yield OS.File.removeEmptyDir(dir);
+ do_check_false((yield OS.File.exists(dir)));
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/test_reset.js b/toolkit/components/osfile/tests/xpcshell/test_reset.js
new file mode 100644
index 000000000..f1e1b14d1
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_reset.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var Path = OS.Constants.Path;
+
+add_task(function* init() {
+ do_get_profile();
+});
+
+add_task(function* reset_before_launching() {
+ do_print("Reset without launching OS.File, it shouldn't break");
+ yield OS.File.resetWorker();
+});
+
+add_task(function* transparent_reset() {
+ for (let i = 1; i < 3; ++i) {
+ do_print("Do stome stuff before and after " + i + " reset(s), " +
+ "it shouldn't break");
+ let CONTENT = "some content " + i;
+ let path = OS.Path.join(Path.profileDir, "tmp");
+ yield OS.File.writeAtomic(path, CONTENT);
+ for (let j = 0; j < i; ++j) {
+ yield OS.File.resetWorker();
+ }
+ let data = yield OS.File.read(path);
+ let string = (new TextDecoder()).decode(data);
+ do_check_eq(string, CONTENT);
+ }
+});
+
+add_task(function* file_open_cannot_reset() {
+ let TEST_FILE = OS.Path.join(Path.profileDir, "tmp-" + Math.random());
+ do_print("Leaking file descriptor " + TEST_FILE + ", we shouldn't be able to reset");
+ let openedFile = yield OS.File.open(TEST_FILE, { create: true} );
+ let thrown = false;
+ try {
+ yield OS.File.resetWorker();
+ } catch (ex if ex.message.indexOf(OS.Path.basename(TEST_FILE)) != -1 ) {
+ thrown = true;
+ }
+ do_check_true(thrown);
+
+ do_print("Closing the file, we should now be able to reset");
+ yield openedFile.close();
+ yield OS.File.resetWorker();
+});
+
+add_task(function* dir_open_cannot_reset() {
+ let TEST_DIR = yield OS.File.getCurrentDirectory();
+ do_print("Leaking directory " + TEST_DIR + ", we shouldn't be able to reset");
+ let iterator = new OS.File.DirectoryIterator(TEST_DIR);
+ let thrown = false;
+ try {
+ yield OS.File.resetWorker();
+ } catch (ex if ex.message.indexOf(OS.Path.basename(TEST_DIR)) != -1 ) {
+ thrown = true;
+ }
+ do_check_true(thrown);
+
+ do_print("Closing the directory, we should now be able to reset");
+ yield iterator.close();
+ yield OS.File.resetWorker();
+});
+
+add_task(function* race_against_itself() {
+ do_print("Attempt to get resetWorker() to race against itself");
+ // Arbitrary operation, just to wake up the worker
+ try {
+ yield OS.File.read("/foo");
+ } catch (ex) {
+ }
+
+ let all = [];
+ for (let i = 0; i < 100; ++i) {
+ all.push(OS.File.resetWorker());
+ }
+
+ yield Promise.all(all);
+});
+
+
+add_task(function* finish_with_a_reset() {
+ do_print("Reset without waiting for the result");
+ // Arbitrary operation, just to wake up the worker
+ try {
+ yield OS.File.read("/foo");
+ } catch (ex) {
+ }
+ // Now reset
+ /*don't yield*/ OS.File.resetWorker();
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_shutdown.js b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js
new file mode 100644
index 000000000..667965d9e
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_shutdown.js
@@ -0,0 +1,98 @@
+Components.utils.import("resource://gre/modules/Services.jsm", this);
+Components.utils.import("resource://gre/modules/Promise.jsm", this);
+Components.utils.import("resource://gre/modules/Task.jsm", this);
+Components.utils.import("resource://gre/modules/osfile.jsm", this);
+
+add_task(function init() {
+ do_get_profile();
+});
+
+/**
+ * Test logging of file descriptors leaks.
+ */
+add_task(function system_shutdown() {
+
+ // Test that unclosed files cause warnings
+ // Test that unclosed directories cause warnings
+ // Test that closed files do not cause warnings
+ // Test that closed directories do not cause warnings
+ function testLeaksOf(resource, topic) {
+ return Task.spawn(function() {
+ let deferred = Promise.defer();
+
+ // Register observer
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ Services.prefs.setBoolPref("toolkit.osfile.log", true);
+ Services.prefs.setBoolPref("toolkit.osfile.log.redirect", true);
+ Services.prefs.setCharPref("toolkit.osfile.test.shutdown.observer", topic);
+
+ let observer = function(aMessage) {
+ try {
+ do_print("Got message: " + aMessage);
+ if (!(aMessage instanceof Components.interfaces.nsIConsoleMessage)) {
+ return;
+ }
+ let message = aMessage.message;
+ do_print("Got message: " + message);
+ if (message.indexOf("TEST OS Controller WARNING") < 0) {
+ return;
+ }
+ do_print("Got message: " + message + ", looking for resource " + resource);
+ if (message.indexOf(resource) < 0) {
+ return;
+ }
+ do_print("Resource: " + resource + " found");
+ do_execute_soon(deferred.resolve);
+ } catch (ex) {
+ do_execute_soon(function() {
+ deferred.reject(ex);
+ });
+ }
+ };
+ Services.console.registerListener(observer);
+ Services.obs.notifyObservers(null, topic, null);
+ do_timeout(1000, function() {
+ do_print("Timeout while waiting for resource: " + resource);
+ deferred.reject("timeout");
+ });
+
+ let resolved = false;
+ try {
+ yield deferred.promise;
+ resolved = true;
+ } catch (ex if ex == "timeout") {
+ resolved = false;
+ }
+ Services.console.unregisterListener(observer);
+ Services.prefs.clearUserPref("toolkit.osfile.log");
+ Services.prefs.clearUserPref("toolkit.osfile.log.redirect");
+ Services.prefs.clearUserPref("toolkit.osfile.test.shutdown.observer");
+ Services.prefs.clearUserPref("toolkit.async_shutdown.testing", true);
+
+ throw new Task.Result(resolved);
+ });
+ }
+
+ let TEST_DIR = OS.Path.join((yield OS.File.getCurrentDirectory()), "..");
+ do_print("Testing for leaks of directory iterator " + TEST_DIR);
+ let iterator = new OS.File.DirectoryIterator(TEST_DIR);
+ do_print("At this stage, we leak the directory");
+ do_check_true((yield testLeaksOf(TEST_DIR, "test.shutdown.dir.leak")));
+ yield iterator.close();
+ do_print("At this stage, we don't leak the directory anymore");
+ do_check_false((yield testLeaksOf(TEST_DIR, "test.shutdown.dir.noleak")));
+
+ let TEST_FILE = OS.Path.join(OS.Constants.Path.profileDir, "test");
+ do_print("Testing for leaks of file descriptor: " + TEST_FILE);
+ let openedFile = yield OS.File.open(TEST_FILE, { create: true} );
+ do_print("At this stage, we leak the file");
+ do_check_true((yield testLeaksOf(TEST_FILE, "test.shutdown.file.leak")));
+ yield openedFile.close();
+ do_print("At this stage, we don't leak the file anymore");
+ do_check_false((yield testLeaksOf(TEST_FILE, "test.shutdown.file.leak.2")));
+});
+
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_telemetry.js b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js
new file mode 100644
index 000000000..dc5104443
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_telemetry.js
@@ -0,0 +1,63 @@
+"use strict";
+
+var {OS: {File, Path, Constants}} = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+var {Services} = Components.utils.import("resource://gre/modules/Services.jsm", {});
+
+// Ensure that we have a profile but that the OS.File worker is not launched
+add_task(function* init() {
+ do_get_profile();
+ yield File.resetWorker();
+});
+
+function getCount(histogram) {
+ if (histogram == null) {
+ return 0;
+ }
+
+ let total = 0;
+ for (let i of histogram.counts) {
+ total += i;
+ }
+ return total;
+}
+
+// Ensure that launching the OS.File worker adds data to the relevant
+// histograms
+add_task(function* test_startup() {
+ let LAUNCH = "OSFILE_WORKER_LAUNCH_MS";
+ let READY = "OSFILE_WORKER_READY_MS";
+
+ let before = Services.telemetry.histogramSnapshots;
+
+ // Launch the OS.File worker
+ yield File.getCurrentDirectory();
+
+ let after = Services.telemetry.histogramSnapshots;
+
+
+ do_print("Ensuring that we have recorded measures for histograms");
+ do_check_eq(getCount(after[LAUNCH]), getCount(before[LAUNCH]) + 1);
+ do_check_eq(getCount(after[READY]), getCount(before[READY]) + 1);
+
+ do_print("Ensuring that launh <= ready");
+ do_check_true(after[LAUNCH].sum <= after[READY].sum);
+});
+
+// Ensure that calling writeAtomic adds data to the relevant histograms
+add_task(function* test_writeAtomic() {
+ let LABEL = "OSFILE_WRITEATOMIC_JANK_MS";
+
+ let before = Services.telemetry.histogramSnapshots;
+
+ // Perform a write.
+ let path = Path.join(Constants.Path.profileDir, "test_osfile_telemetry.tmp");
+ yield File.writeAtomic(path, LABEL, { tmpPath: path + ".tmp" } );
+
+ let after = Services.telemetry.histogramSnapshots;
+
+ do_check_eq(getCount(after[LABEL]), getCount(before[LABEL]) + 1);
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/osfile/tests/xpcshell/test_unique.js b/toolkit/components/osfile/tests/xpcshell/test_unique.js
new file mode 100644
index 000000000..8aa81b803
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_unique.js
@@ -0,0 +1,88 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+ do_get_profile();
+ run_next_test();
+}
+
+function testFiles(filename) {
+ return Task.spawn(function() {
+ const MAX_TRIES = 10;
+ let profileDir = OS.Constants.Path.profileDir;
+ let path = OS.Path.join(profileDir, filename);
+
+ // Ensure that openUnique() uses the file name if there is no file with that name already.
+ let openedFile = yield OS.File.openUnique(path);
+ do_print("\nCreate new file: " + openedFile.path);
+ yield openedFile.file.close();
+ let exists = yield OS.File.exists(openedFile.path);
+ do_check_true(exists);
+ do_check_eq(path, openedFile.path);
+ let fileInfo = yield OS.File.stat(openedFile.path);
+ do_check_true(fileInfo.size == 0);
+
+ // Ensure that openUnique() creates a new file name using a HEX number, as the original name is already taken.
+ openedFile = yield OS.File.openUnique(path);
+ do_print("\nCreate unique HEX file: " + openedFile.path);
+ yield openedFile.file.close();
+ exists = yield OS.File.exists(openedFile.path);
+ do_check_true(exists);
+ fileInfo = yield OS.File.stat(openedFile.path);
+ do_check_true(fileInfo.size == 0);
+
+ // Ensure that openUnique() generates different file names each time, using the HEX number algorithm
+ let filenames = new Set();
+ for (let i=0; i < MAX_TRIES; i++) {
+ openedFile = yield OS.File.openUnique(path);
+ yield openedFile.file.close();
+ filenames.add(openedFile.path);
+ }
+
+ do_check_eq(filenames.size, MAX_TRIES);
+
+ // Ensure that openUnique() creates a new human readable file name using, as the original name is already taken.
+ openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+ do_print("\nCreate unique Human Readable file: " + openedFile.path);
+ yield openedFile.file.close();
+ exists = yield OS.File.exists(openedFile.path);
+ do_check_true(exists);
+ fileInfo = yield OS.File.stat(openedFile.path);
+ do_check_true(fileInfo.size == 0);
+
+ // Ensure that openUnique() generates different human readable file names each time
+ filenames = new Set();
+ for (let i=0; i < MAX_TRIES; i++) {
+ openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+ yield openedFile.file.close();
+ filenames.add(openedFile.path);
+ }
+
+ do_check_eq(filenames.size, MAX_TRIES);
+
+ let exn;
+ try {
+ for (let i=0; i < 100; i++) {
+ openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+ yield openedFile.file.close();
+ }
+ } catch (ex) {
+ exn = ex;
+ }
+
+ do_print("Ensure that this raises the correct error");
+ do_check_true(!!exn);
+ do_check_true(exn instanceof OS.File.Error);
+ do_check_true(exn.becauseExists);
+ });
+}
+
+add_task(function test_unique() {
+ OS.Shared.DEBUG = true;
+ // Tests files with extension
+ yield testFiles("dummy_unique_file.txt");
+ // Tests files with no extension
+ yield testFiles("dummy_unique_file_no_ext");
+});
diff --git a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..58b106d3d
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,51 @@
+[DEFAULT]
+head = head.js
+tail =
+
+support-files =
+ test_loader/module_test_loader.js
+
+[test_available_free_space.js]
+[test_compression.js]
+[test_constants.js]
+[test_creationDate.js]
+[test_duration.js]
+[test_exception.js]
+[test_file_URL_conversion.js]
+[test_loader.js]
+[test_logging.js]
+[test_makeDir.js]
+[test_open.js]
+[test_osfile_async.js]
+[test_osfile_async_append.js]
+[test_osfile_async_bytes.js]
+[test_osfile_async_copy.js]
+[test_osfile_async_flush.js]
+[test_osfile_async_largefiles.js]
+[test_osfile_async_setDates.js]
+# Unimplemented on Windows (bug 1022816).
+# Spurious failure on Android test farm due to non-POSIX behavior of
+# filesystem backing /mnt/sdcard (not worth trying to fix).
+[test_osfile_async_setPermissions.js]
+skip-if = os == "win" || os == "android"
+[test_osfile_closed.js]
+[test_osfile_error.js]
+[test_osfile_kill.js]
+# Windows test
+[test_osfile_win_async_setPermissions.js]
+skip-if = os != "win"
+[test_osfile_writeAtomic_backupTo_option.js]
+[test_osfile_writeAtomic_zerobytes.js]
+[test_path.js]
+[test_path_constants.js]
+[test_queue.js]
+[test_read_write.js]
+requesttimeoutfactor = 4
+[test_remove.js]
+[test_removeDir.js]
+requesttimeoutfactor = 4
+[test_removeEmptyDir.js]
+[test_reset.js]
+[test_shutdown.js]
+[test_telemetry.js]
+[test_unique.js]