diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /toolkit/components/osfile | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/components/osfile')
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] |